From 34898f365de8d7fc5af04b16270944c129a97053 Mon Sep 17 00:00:00 2001 From: Stephan Min Date: Fri, 15 Sep 2023 15:48:34 -0400 Subject: [PATCH 01/10] add redeemables submodule --- .gitmodules | 3 + .../.github/pull_request_template.md | 25 + lib/redeemables/.github/workflows/test.yml | 45 + lib/redeemables/.gitignore | 14 + lib/redeemables/.gitmodules | 21 + lib/redeemables/LICENSE | 21 + lib/redeemables/README.md | 78 ++ lib/redeemables/foundry.toml | 14 + .../DeployAndConfigureExampleCampaign.s.sol | 77 + lib/redeemables/src/ERC1155Redeemable.sol | 8 + lib/redeemables/src/ERC721Redeemable.sol | 14 + .../src/RedeemableContractOfferer.sol | 705 ++++++++++ .../src/interfaces/IDynamicTraits.sol | 9 + .../src/interfaces/IERC1155Receiver.sol | 47 + .../interfaces/IERC1155RedemptionMintable.sol | 8 + .../interfaces/IERC721RedemptionMintable.sol | 8 + lib/redeemables/src/interfaces/IERC7XXX.sol | 28 + .../interfaces/IRedeemableContractOfferer.sol | 32 + lib/redeemables/src/lib/DynamicTraits.sol | 34 + .../src/lib/ERC1155RedemptionMintable.sol | 38 + .../src/lib/ERC721RedemptionMintable.sol | 46 + .../ERC721RedemptionMintableWithCounter.sol | 49 + .../src/lib/RedeemableErrorsAndEvents.sol | 38 + lib/redeemables/src/lib/RedeemableStructs.sol | 27 + lib/redeemables/src/lib/SignedRedeem.sol | 93 ++ .../src/lib/SignedRedeemContractOfferer.sol | 88 ++ .../src/lib/SignedRedeemErrorsAndEvents.sol | 7 + .../test/RedeemViaSeaport-1155.t.sol | 1233 +++++++++++++++++ .../test/RedeemViaSeaport-721.t.sol | 1173 ++++++++++++++++ lib/redeemables/test/RedeemViaTransfer.t.sol | 120 ++ lib/redeemables/test/Revert.t.sol | 361 +++++ lib/redeemables/test/utils/ArithmeticUtil.sol | 24 + lib/redeemables/test/utils/BaseOrderTest.sol | 289 ++++ .../test/utils/BaseSeaportTest.sol | 97 ++ .../test/utils/DifferentialTest.sol | 37 + .../test/utils/mocks/ERC1155Recipient.sol | 24 + .../test/utils/mocks/ERC721Recipient.sol | 10 + .../test/utils/mocks/MockERC1271Wallet.sol | 16 + .../utils/mocks/MockERC721DynamicTraits.sol | 59 + .../test/utils/mocks/TestERC1155.sol | 16 + .../test/utils/mocks/TestERC20.sol | 59 + .../test/utils/mocks/TestERC721.sol | 23 + 42 files changed, 5118 insertions(+) create mode 100644 lib/redeemables/.github/pull_request_template.md create mode 100644 lib/redeemables/.github/workflows/test.yml create mode 100644 lib/redeemables/.gitignore create mode 100644 lib/redeemables/.gitmodules create mode 100644 lib/redeemables/LICENSE create mode 100644 lib/redeemables/README.md create mode 100644 lib/redeemables/foundry.toml create mode 100644 lib/redeemables/script/DeployAndConfigureExampleCampaign.s.sol create mode 100644 lib/redeemables/src/ERC1155Redeemable.sol create mode 100644 lib/redeemables/src/ERC721Redeemable.sol create mode 100644 lib/redeemables/src/RedeemableContractOfferer.sol create mode 100644 lib/redeemables/src/interfaces/IDynamicTraits.sol create mode 100644 lib/redeemables/src/interfaces/IERC1155Receiver.sol create mode 100644 lib/redeemables/src/interfaces/IERC1155RedemptionMintable.sol create mode 100644 lib/redeemables/src/interfaces/IERC721RedemptionMintable.sol create mode 100644 lib/redeemables/src/interfaces/IERC7XXX.sol create mode 100644 lib/redeemables/src/interfaces/IRedeemableContractOfferer.sol create mode 100644 lib/redeemables/src/lib/DynamicTraits.sol create mode 100644 lib/redeemables/src/lib/ERC1155RedemptionMintable.sol create mode 100644 lib/redeemables/src/lib/ERC721RedemptionMintable.sol create mode 100644 lib/redeemables/src/lib/ERC721RedemptionMintableWithCounter.sol create mode 100644 lib/redeemables/src/lib/RedeemableErrorsAndEvents.sol create mode 100644 lib/redeemables/src/lib/RedeemableStructs.sol create mode 100644 lib/redeemables/src/lib/SignedRedeem.sol create mode 100644 lib/redeemables/src/lib/SignedRedeemContractOfferer.sol create mode 100644 lib/redeemables/src/lib/SignedRedeemErrorsAndEvents.sol create mode 100644 lib/redeemables/test/RedeemViaSeaport-1155.t.sol create mode 100644 lib/redeemables/test/RedeemViaSeaport-721.t.sol create mode 100644 lib/redeemables/test/RedeemViaTransfer.t.sol create mode 100644 lib/redeemables/test/Revert.t.sol create mode 100644 lib/redeemables/test/utils/ArithmeticUtil.sol create mode 100644 lib/redeemables/test/utils/BaseOrderTest.sol create mode 100644 lib/redeemables/test/utils/BaseSeaportTest.sol create mode 100644 lib/redeemables/test/utils/DifferentialTest.sol create mode 100644 lib/redeemables/test/utils/mocks/ERC1155Recipient.sol create mode 100644 lib/redeemables/test/utils/mocks/ERC721Recipient.sol create mode 100644 lib/redeemables/test/utils/mocks/MockERC1271Wallet.sol create mode 100644 lib/redeemables/test/utils/mocks/MockERC721DynamicTraits.sol create mode 100644 lib/redeemables/test/utils/mocks/TestERC1155.sol create mode 100644 lib/redeemables/test/utils/mocks/TestERC20.sol create mode 100644 lib/redeemables/test/utils/mocks/TestERC721.sol diff --git a/.gitmodules b/.gitmodules index 5b64a62..f59df4c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "lib/solarray"] path = lib/solarray url = https://github.com/evmcheb/solarray +[submodule "redeemables"] + path = redeemables + url = https://github.com/ProjectOpenSea/redeemables.git diff --git a/lib/redeemables/.github/pull_request_template.md b/lib/redeemables/.github/pull_request_template.md new file mode 100644 index 0000000..3c4cb66 --- /dev/null +++ b/lib/redeemables/.github/pull_request_template.md @@ -0,0 +1,25 @@ + + +## Motivation + + + +## Solution + + diff --git a/lib/redeemables/.github/workflows/test.yml b/lib/redeemables/.github/workflows/test.yml new file mode 100644 index 0000000..2f3d5fb --- /dev/null +++ b/lib/redeemables/.github/workflows/test.yml @@ -0,0 +1,45 @@ +name: Test CI + +on: + push: + branches: [main] + tags: ["*"] + pull_request: + types: [opened, reopened, synchronize] + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Forge build + run: | + forge --version + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test + + - name: Run Forge format + run: | + forge fmt + [ -z "`git status --porcelain`" ] && echo "No diff for format" || { echo "Diff exists for format"; exit 1; } + id: lint diff --git a/lib/redeemables/.gitignore b/lib/redeemables/.gitignore new file mode 100644 index 0000000..bf61470 --- /dev/null +++ b/lib/redeemables/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores broadcast logs +/broadcast + +# Docs +docs/ + +# Dotenv file +.env + +.vscode diff --git a/lib/redeemables/.gitmodules b/lib/redeemables/.gitmodules new file mode 100644 index 0000000..5e5a87f --- /dev/null +++ b/lib/redeemables/.gitmodules @@ -0,0 +1,21 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "lib/solady"] + path = lib/solady + url = git@github.com:Vectorized/solady.git +[submodule "lib/solarray"] + path = lib/solarray + url = https://github.com/evmcheb/solarray +[submodule "lib/seaport-types"] + path = lib/seaport-types + url = https://github.com/ProjectOpenSea/seaport-types +[submodule "lib/seaport-sol"] + path = lib/seaport-sol + url = https://github.com/ProjectOpenSea/seaport-sol +[submodule "lib/seaport-core"] + path = lib/seaport-core + url = https://github.com/ProjectOpenSea/seaport-core +[submodule "lib/murky"] + path = lib/murky + url = https://github.com/dmfxyz/murky.git diff --git a/lib/redeemables/LICENSE b/lib/redeemables/LICENSE new file mode 100644 index 0000000..2d35427 --- /dev/null +++ b/lib/redeemables/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Ozone Networks, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/redeemables/README.md b/lib/redeemables/README.md new file mode 100644 index 0000000..10923c0 --- /dev/null +++ b/lib/redeemables/README.md @@ -0,0 +1,78 @@ +# Redeemables + +EVM smart contracts for redeemables and dynamic traits. + +## Foundry + +To install Foundry (assuming a Linux or macOS system): + +```bash +curl -L https://foundry.paradigm.xyz | bash +``` + +This will download foundryup. To start Foundry, run: + +```bash +foundryup +``` + +To install dependencies: + +``` +forge install +``` + +To run tests: + +``` +forge test +``` + +To run format: + +``` +forge fmt +``` + + + +The following modifiers are also available: + +- Level 2 (-vv): Logs emitted during tests are also displayed. +- Level 3 (-vvv): Stack traces for failing tests are also displayed. +- Level 4 (-vvvv): Stack traces for all tests are displayed, and setup traces for failing tests are displayed. +- Level 5 (-vvvvv): Stack traces and setup traces are always displayed. + +```bash +forge test -vv +``` + +For more information on foundry testing and use, see [Foundry Book installation instructions](https://book.getfoundry.sh/getting-started/installation). + +## Contributing + +Contributions are welcome by anyone interested in writing more tests, improving readability, optimizing for gas efficiency, or extending the protocol with new features. + +When making a pull request, ensure that: + +- All tests pass. +- Code coverage remains at 100% (coverage tests must currently be written in hardhat). +- All new code adheres to the style guide: + - All lint checks pass. + - Code is thoroughly commented with natspec where relevant. +- If making a change to the contracts: + - Gas snapshots are provided and demonstrate an improvement (or an acceptable deficit given other improvements). + - Reference contracts are modified correspondingly if relevant. + - New tests (ideally via foundry) are included for all new features or code paths. +- If making a modification to third-party dependencies, `yarn audit` passes. +- A descriptive summary of the PR has been provided. + +## License + +[MIT](LICENSE) Copyright 2023 Ozone Networks, Inc. diff --git a/lib/redeemables/foundry.toml b/lib/redeemables/foundry.toml new file mode 100644 index 0000000..2c7fb1a --- /dev/null +++ b/lib/redeemables/foundry.toml @@ -0,0 +1,14 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +solc_version = '0.8.19' +remappings = [ + 'ds-test/=lib/forge-std/lib/ds-test/src/', + 'solady/=lib/solady/', + 'seaport-core/=lib/seaport-core/', + 'seaport-sol/=lib/seaport-sol/', + 'seaport-types/=lib/seaport-types/' +] + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/lib/redeemables/script/DeployAndConfigureExampleCampaign.s.sol b/lib/redeemables/script/DeployAndConfigureExampleCampaign.s.sol new file mode 100644 index 0000000..891de17 --- /dev/null +++ b/lib/redeemables/script/DeployAndConfigureExampleCampaign.s.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Script.sol"; + +import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; +import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {RedeemableContractOfferer} from "../src/RedeemableContractOfferer.sol"; +import {CampaignParams} from "../src/lib/RedeemableStructs.sol"; +import {ERC721RedemptionMintable} from "../src/lib/ERC721RedemptionMintable.sol"; +import {TestERC721} from "../test/utils/mocks/TestERC721.sol"; + +contract DeployAndConfigureExampleCampaign is Script { + // Addresses: Seaport + address seaport = 0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC; + address conduit = 0x1E0049783F008A0085193E00003D00cd54003c71; + bytes32 conduitKey = 0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000; + + address constant _BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD; + + function run() external { + vm.startBroadcast(); + + RedeemableContractOfferer offerer = new RedeemableContractOfferer( + conduit, + conduitKey, + seaport + ); + TestERC721 redeemableToken = new TestERC721(); + ERC721RedemptionMintable redemptionToken = new ERC721RedemptionMintable( + address(offerer), + address(redeemableToken) + ); + + // Configure the campaign. + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + CampaignParams memory params = CampaignParams({ + offer: offer, + consideration: consideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1_000_000), + maxCampaignRedemptions: 1_000, + manager: msg.sender + }); + offerer.createCampaign(params, "ipfs://QmdChMVnMSq4U6oVKhud7wUSEZGnwuMuTY5rUQx57Ayp6H"); + + // Mint tokens 1 and 5 to redeem for tokens 1 and 5. + redeemableToken.mint(msg.sender, 1); + redeemableToken.mint(msg.sender, 5); + + // Let's redeem them! + uint256 campaignId = 1; + bytes32 redemptionHash = bytes32(0); + bytes memory data = abi.encode(campaignId, redemptionHash); + redeemableToken.safeTransferFrom(msg.sender, address(offerer), 1, data); + redeemableToken.safeTransferFrom(msg.sender, address(offerer), 5, data); + } +} diff --git a/lib/redeemables/src/ERC1155Redeemable.sol b/lib/redeemables/src/ERC1155Redeemable.sol new file mode 100644 index 0000000..9f862fe --- /dev/null +++ b/lib/redeemables/src/ERC1155Redeemable.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ERC1155} from "solady/src/tokens/ERC1155.sol"; +import {IERCDynamicTraits} from "./interfaces/IDynamicTraits.sol"; +import {SignedRedeem} from "./lib/SignedRedeem.sol"; + +// contract ERC1155Redeemable is ERC1155, IERCDynamicTraits, SignedRedeem {} diff --git a/lib/redeemables/src/ERC721Redeemable.sol b/lib/redeemables/src/ERC721Redeemable.sol new file mode 100644 index 0000000..7c9204f --- /dev/null +++ b/lib/redeemables/src/ERC721Redeemable.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ERC721} from "solady/src/tokens/ERC721.sol"; +import {DynamicTraits} from "./lib/DynamicTraits.sol"; +import {SignedRedeem} from "./lib/SignedRedeem.sol"; +import {RedeemableErrorsAndEvents} from "./lib/RedeemableErrorsAndEvents.sol"; + +// contract ERC721Redeemable is +// ERC721, +// RedeemableErrorsAndEvents, +// DynamicTraits, +// SignedRedeem +// {} diff --git a/lib/redeemables/src/RedeemableContractOfferer.sol b/lib/redeemables/src/RedeemableContractOfferer.sol new file mode 100644 index 0000000..33b9bff --- /dev/null +++ b/lib/redeemables/src/RedeemableContractOfferer.sol @@ -0,0 +1,705 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ContractOffererInterface} from "seaport-types/src/interfaces/ContractOffererInterface.sol"; +import {SeaportInterface} from "seaport-types/src/interfaces/SeaportInterface.sol"; +import {ItemType, OrderType} from "seaport-types/src/lib/ConsiderationEnums.sol"; +import { + AdvancedOrder, + CriteriaResolver, + OrderParameters, + OfferItem, + ConsiderationItem, + ReceivedItem, + Schema, + SpentItem +} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {ERC20} from "solady/src/tokens/ERC20.sol"; +import {ERC721} from "solady/src/tokens/ERC721.sol"; +import {ERC1155} from "solady/src/tokens/ERC1155.sol"; +import {IERC721Receiver} from "seaport-types/src/interfaces/IERC721Receiver.sol"; +import {IERC1155Receiver} from "./interfaces/IERC1155Receiver.sol"; +import {IERC721RedemptionMintable} from "./interfaces/IERC721RedemptionMintable.sol"; +import {IERC1155RedemptionMintable} from "./interfaces/IERC1155RedemptionMintable.sol"; +import {SignedRedeemContractOfferer} from "./lib/SignedRedeemContractOfferer.sol"; +import {RedeemableErrorsAndEvents} from "./lib/RedeemableErrorsAndEvents.sol"; +import {CampaignParams} from "./lib/RedeemableStructs.sol"; + +/** + * @title RedeemablesContractOfferer + * @author ryanio, stephankmin + * @notice A Seaport contract offerer that allows users to burn to redeem off chain redeemables. + */ +contract RedeemableContractOfferer is + ContractOffererInterface, + RedeemableErrorsAndEvents, + SignedRedeemContractOfferer +{ + /// @dev The Seaport address allowed to interact with this contract offerer. + address internal immutable _SEAPORT; + + /// @dev The conduit address to allow as an operator for this contract for newly minted tokens. + address internal immutable _CONDUIT; + + bytes32 internal immutable _CONDUIT_KEY; + + /// @dev Counter for next campaign id. + uint256 private _nextCampaignId = 1; + + /// @dev The campaign parameters by campaign id. + mapping(uint256 campaignId => CampaignParams params) private _campaignParams; + + /// @dev The campaign URIs by campaign id. + mapping(uint256 campaignId => string campaignURI) private _campaignURIs; + + /// @dev The total current redemptions by campaign id. + mapping(uint256 campaignId => uint256 count) private _totalRedemptions; + + constructor(address conduit, bytes32 conduitKey, address seaport) { + _CONDUIT = conduit; + _CONDUIT_KEY = conduitKey; + _SEAPORT = seaport; + } + + function createCampaign(CampaignParams calldata params, string calldata uri) + external + returns (uint256 campaignId) + { + // Revert if there are no consideration items, since the redemption should require at least something. + if (params.consideration.length == 0) revert NoConsiderationItems(); + + // Revert if startTime is past endTime. + if (params.startTime > params.endTime) revert InvalidTime(); + + // Revert if any of the consideration item recipients is the zero address. The 0xdead address should be used instead. + for (uint256 i = 0; i < params.consideration.length;) { + if (params.consideration[i].recipient == address(0)) { + revert ConsiderationItemRecipientCannotBeZeroAddress(); + } + unchecked { + ++i; + } + } + + // Check for and set token approvals for the campaign. + _setTokenApprovals(params); + + // Set the campaign params for the next campaignId. + _campaignParams[_nextCampaignId] = params; + + // Set the campaign URI for the next campaignId. + _campaignURIs[_nextCampaignId] = uri; + + // Set the correct current campaignId to return before incrementing + // the next campaignId. + campaignId = _nextCampaignId; + + // Increment the next campaignId. + _nextCampaignId++; + + emit CampaignUpdated(campaignId, params, _campaignURIs[campaignId]); + } + + function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata uri) external { + if (campaignId == 0 || campaignId >= _nextCampaignId) { + revert InvalidCampaignId(); + } + + // Revert if there are no consideration items, since the redemption should require at least something. + if (params.consideration.length == 0) revert NoConsiderationItems(); + + // Revert if startTime is past endTime. + if (params.startTime > params.endTime) revert InvalidTime(); + + // Revert if msg.sender is not the manager. + address existingManager = _campaignParams[campaignId].manager; + if (params.manager != msg.sender && (existingManager != address(0) && existingManager != params.manager)) { + revert NotManager(); + } + + // Revert if any of the consideration item recipients is the zero address. The 0xdead address should be used instead. + for (uint256 i = 0; i < params.consideration.length;) { + if (params.consideration[i].recipient == address(0)) { + revert ConsiderationItemRecipientCannotBeZeroAddress(); + } + unchecked { + ++i; + } + } + + // Check for and set token approvals for the campaign. + _setTokenApprovals(params); + + // Set the campaign params for the given campaignId. + _campaignParams[campaignId] = params; + + // Update campaign uri if it was provided. + if (bytes(uri).length != 0) { + _campaignURIs[campaignId] = uri; + } + + emit CampaignUpdated(campaignId, params, _campaignURIs[campaignId]); + } + + function _setTokenApprovals(CampaignParams memory params) internal { + // Allow Seaport and the conduit as operators on behalf of this contract for offer items to be minted and transferred. + for (uint256 i = 0; i < params.offer.length;) { + // Native items do not need to be approved. + if (params.offer[i].itemType == ItemType.NATIVE) { + revert InvalidNativeOfferItem(); + } + // ERC721 and ERC1155 have the same function signatures for isApprovedForAll and setApprovalForAll. + else if (params.offer[i].itemType >= ItemType.ERC721) { + if (!ERC721(params.offer[i].token).isApprovedForAll(_CONDUIT, address(this))) { + ERC721(params.offer[i].token).setApprovalForAll(_CONDUIT, true); + } + // Set the maximum approval amount for ERC20 tokens. + } else { + ERC20(params.offer[i].token).approve(_CONDUIT, type(uint256).max); + } + unchecked { + ++i; + } + } + + // Allow Seaport and the conduit as operators on behalf of this contract for consideration items to be transferred in the onReceived hooks. + for (uint256 i = 0; i < params.consideration.length;) { + // ERC721 and ERC1155 have the same function signatures for isApprovedForAll and setApprovalForAll. + if (params.consideration[i].itemType >= ItemType.ERC721) { + if (!ERC721(params.consideration[i].token).isApprovedForAll(_CONDUIT, address(this))) { + ERC721(params.consideration[i].token).setApprovalForAll(_CONDUIT, true); + } + // Set the maximum approval amount for ERC20 tokens. + } else { + ERC20(params.consideration[i].token).approve(_CONDUIT, type(uint256).max); + } + unchecked { + ++i; + } + } + } + + function updateCampaignURI(uint256 campaignId, string calldata uri) external { + CampaignParams storage params = _campaignParams[campaignId]; + + if (params.manager != msg.sender) revert NotManager(); + + _campaignURIs[campaignId] = uri; + + emit CampaignUpdated(campaignId, params, uri); + } + + /** + * @dev Generates an order with the specified minimum and maximum spent + * items, and optional context (supplied as extraData). + * + * @param fulfiller The address of the fulfiller. + * @param minimumReceived The minimum items that the caller must receive. + * @param maximumSpent The maximum items the caller is willing to spend. + * @param context Additional context of the order. + * + * @return offer A tuple containing the offer items. + * @return consideration An array containing the consideration items. + */ + function generateOrder( + address fulfiller, + SpentItem[] calldata minimumReceived, + SpentItem[] calldata maximumSpent, + bytes calldata context // encoded based on the schemaID + ) external override returns (SpentItem[] memory offer, ReceivedItem[] memory consideration) { + // Derive the offer and consideration with effects. + (offer, consideration) = _createOrder(fulfiller, minimumReceived, maximumSpent, context, true); + } + + /** + * @dev Ratifies an order with the specified offer, consideration, and + * optional context (supplied as extraData). + * + * @custom:param offer The offer items. + * @custom:param consideration The consideration items. + * @custom:param context Additional context of the order. + * @custom:param orderHashes The hashes to ratify. + * @custom:param contractNonce The nonce of the contract. + * + * @return ratifyOrderMagicValue The magic value returned by the contract + * offerer. + */ + function ratifyOrder( + SpentItem[] calldata, /* offer */ + ReceivedItem[] calldata, /* consideration */ + bytes calldata, /* context */ // encoded based on the schemaID + bytes32[] calldata, /* orderHashes */ + uint256 /* contractNonce */ + ) external pure override returns (bytes4) { + assembly { + // Return the RatifyOrder magic value. + mstore(0, 0xf4dd92ce) + return(0x1c, 32) + } + } + + /** + * @dev View function to preview an order generated in response to a minimum + * set of received items, maximum set of spent items, and context + * (supplied as extraData). + * + * @custom:param caller The address of the caller (e.g. Seaport). + * @param fulfiller The address of the fulfiller (e.g. the account + * calling Seaport). + * @param minimumReceived The minimum items that the caller is willing to + * receive. + * @param maximumSpent The maximum items caller is willing to spend. + * @param context Additional context of the order. + * + * @return offer A tuple containing the offer items. + * @return consideration A tuple containing the consideration items. + */ + function previewOrder( + address, /* caller */ + address fulfiller, + SpentItem[] calldata minimumReceived, + SpentItem[] calldata maximumSpent, + bytes calldata context // encoded based on the schemaID + ) external view override returns (SpentItem[] memory offer, ReceivedItem[] memory consideration) { + // To avoid the solidity compiler complaining about calling a non-view + // function here (_createOrder), we will cast it as a view and use it. + // This is okay because we are not modifying any state when passing + // withEffects=false. + function( + address, + SpentItem[] memory, + SpentItem[] memory, + bytes calldata, + bool + ) internal view returns (SpentItem[] memory, ReceivedItem[] memory) fn; + function( + address, + SpentItem[] memory, + SpentItem[] memory, + bytes calldata, + bool + ) + internal + returns ( + SpentItem[] memory, + ReceivedItem[] memory + ) fn2 = _createOrder; + assembly { + fn := fn2 + } + + // Derive the offer and consideration without effects. + (offer, consideration) = fn(fulfiller, minimumReceived, maximumSpent, context, false); + } + + /** + * @dev Gets the metadata for this contract offerer. + * + * @return name The name of the contract offerer. + * @return schemas The schemas supported by the contract offerer. + */ + function getSeaportMetadata() + external + pure + override + returns ( + string memory name, + Schema[] memory schemas // map to Seaport Improvement Proposal IDs + ) + { + schemas = new Schema[](0); + return ("RedeemablesContractOfferer", schemas); + } + + function supportsInterface(bytes4 interfaceId) external view virtual returns (bool) { + return interfaceId == type(ContractOffererInterface).interfaceId + || interfaceId == type(IERC721Receiver).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId; + } + + function _createOrder( + address fulfiller, + SpentItem[] memory minimumReceived, + SpentItem[] memory maximumSpent, + bytes calldata context, + bool withEffects + ) internal returns (SpentItem[] memory offer, ReceivedItem[] memory consideration) { + // Get the campaign. + uint256 campaignId = uint256(bytes32(context[0:32])); + CampaignParams storage params = _campaignParams[campaignId]; + + // Declare an error buffer; first check is that caller is Seaport or the token contract. + uint256 errorBuffer = _cast(msg.sender != _SEAPORT && msg.sender != params.consideration[0].token); + + // Check the redemption is active. + errorBuffer |= _cast(_isInactive(params.startTime, params.endTime)) << 1; + + // Check max total redemptions would not be exceeded. + errorBuffer |= _cast(_totalRedemptions[campaignId] + maximumSpent.length > params.maxCampaignRedemptions) << 2; + + // Get the redemption hash. + bytes32 redemptionHash = bytes32(context[32:64]); + + // Check the signature is valid if required. + if (params.signer != address(0)) { + uint256 salt = uint256(bytes32(context[64:96])); + bytes memory signature = context[96:]; + // _verifySignature will revert if the signature is invalid or digest is already used. + _verifySignature(params.signer, fulfiller, maximumSpent, redemptionHash, salt, signature, withEffects); + } + + if (errorBuffer > 0) { + if (errorBuffer << 255 != 0) { + revert InvalidCaller(msg.sender); + } else if (errorBuffer << 254 != 0) { + revert NotActive(block.timestamp, params.startTime, params.endTime); + } else if (errorBuffer << 253 != 0) { + revert MaxCampaignRedemptionsReached( + _totalRedemptions[campaignId] + maximumSpent.length, params.maxCampaignRedemptions + ); + // TODO: do we need this error? + // } else if (errorBuffer << 252 != 0) { + // revert InvalidConsiderationLength( + // maximumSpent.length, + // params.consideration.length + // ); + } else if (errorBuffer << 252 != 0) { + revert InvalidConsiderationItem(maximumSpent[0].token, params.consideration[0].token); + } else { + // todo more validation errors + } + } + + // Set the offer from the params. + offer = new SpentItem[](params.offer.length); + for (uint256 i = 0; i < params.offer.length;) { + OfferItem memory offerItem = params.offer[i]; + + uint256 tokenId = IERC721RedemptionMintable(offerItem.token).mintRedemption(address(this), maximumSpent); + + // Set the itemType without criteria. + ItemType itemType = offerItem.itemType == ItemType.ERC721_WITH_CRITERIA + ? ItemType.ERC721 + : offerItem.itemType == ItemType.ERC1155_WITH_CRITERIA ? ItemType.ERC1155 : offerItem.itemType; + + offer[i] = SpentItem({ + itemType: itemType, + token: offerItem.token, + identifier: tokenId, + amount: offerItem.startAmount // TODO: do we need to calculate amount based on timestamp? + }); + unchecked { + ++i; + } + } + + // Set the consideration from the params. + consideration = new ReceivedItem[](params.consideration.length); + for (uint256 i = 0; i < params.consideration.length;) { + ConsiderationItem memory considerationItem = params.consideration[i]; + + // TODO: make helper getItemTypeWithoutCriteria + ItemType itemType; + uint256 identifier; + + // If consideration item is wildcard criteria item, set itemType to ERC721 + // and identifier to the maximumSpent item identifier. + if ( + (considerationItem.itemType == ItemType.ERC721_WITH_CRITERIA) + && (considerationItem.identifierOrCriteria == 0) + ) { + itemType = ItemType.ERC721; + identifier = maximumSpent[i].identifier; + } else if ( + (considerationItem.itemType == ItemType.ERC1155_WITH_CRITERIA) + && (considerationItem.identifierOrCriteria == 0) + ) { + itemType = ItemType.ERC1155; + identifier = maximumSpent[i].identifier; + } else { + itemType = considerationItem.itemType; + identifier = considerationItem.identifierOrCriteria; + } + + consideration[i] = ReceivedItem({ + itemType: itemType, + token: considerationItem.token, + identifier: identifier, + amount: considerationItem.startAmount, + recipient: considerationItem.recipient + }); + unchecked { + ++i; + } + } + + // If withEffects is true then make state changes. + if (withEffects) { + // Increment total redemptions. + _totalRedemptions[campaignId] += maximumSpent.length; + + SpentItem[] memory spent = new SpentItem[](consideration.length); + for (uint256 i = 0; i < consideration.length;) { + spent[i] = SpentItem({ + itemType: consideration[i].itemType, + token: consideration[i].token, + identifier: consideration[i].identifier, + amount: consideration[i].amount + }); + unchecked { + ++i; + } + } + + // Emit Redemption event. + emit Redemption(campaignId, redemptionHash); + } + } + + function onERC721Received( + address, + /* operator */ + address from, + uint256 tokenId, + bytes calldata data + ) external returns (bytes4) { + if (from == address(0)) { + return IERC721Receiver.onERC721Received.selector; + } + + // Get the campaign. + uint256 campaignId = uint256(bytes32(data[0:32])); + CampaignParams storage params = _campaignParams[campaignId]; + + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: params.offer[0].token, + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: msg.sender, + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1, + recipient: payable(address(0x000000000000000000000000000000000000dEaD)) + }); + + OrderParameters memory parameters = OrderParameters({ + offerer: address(this), + zone: address(0), + offer: offer, + consideration: consideration, + orderType: OrderType.CONTRACT, + startTime: block.timestamp, + endTime: block.timestamp + 10, // TODO: fix + zoneHash: bytes32(0), // TODO: fix + salt: uint256(0), // TODO: fix + conduitKey: _CONDUIT_KEY, + totalOriginalConsiderationItems: consideration.length + }); + + AdvancedOrder memory order = + AdvancedOrder({parameters: parameters, numerator: 1, denominator: 1, signature: "", extraData: data}); + + SeaportInterface(_SEAPORT).fulfillAdvancedOrder(order, new CriteriaResolver[](0), _CONDUIT_KEY, from); + + return IERC721Receiver.onERC721Received.selector; + } + + function onERC1155Received( + address, + /* operator */ + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external returns (bytes4) { + if (from == address(0)) { + return IERC1155Receiver.onERC1155Received.selector; + } + + // Get the campaign. + uint256 campaignId = uint256(bytes32(data[0:32])); + CampaignParams storage params = _campaignParams[campaignId]; + + SpentItem[] memory minimumReceived = new SpentItem[](1); + minimumReceived[0] = SpentItem({ + itemType: ItemType.ERC721, + token: params.offer[0].token, + identifier: params.offer[0].identifierOrCriteria, + amount: params.offer[0].startAmount + }); + + SpentItem[] memory maximumSpent = new SpentItem[](1); + maximumSpent[0] = SpentItem({itemType: ItemType.ERC1155, token: msg.sender, identifier: id, amount: value}); + + // _createOrder will revert if any validations fail. + _createOrder(from, minimumReceived, maximumSpent, data, true); + + // Transfer the token to the consideration item recipient. + address recipient = _getConsiderationRecipient(params.consideration, msg.sender); + ERC1155(msg.sender).safeTransferFrom(address(this), recipient, id, value, ""); + + // Transfer the newly minted token to the fulfiller. + ERC721(params.offer[0].token).safeTransferFrom(address(this), from, id, ""); + + return IERC1155Receiver.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, /* operator */ + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4) { + if (from == address(0)) { + return IERC1155Receiver.onERC1155BatchReceived.selector; + } + + if (ids.length != values.length) revert RedeemMismatchedLengths(); + + // Get the campaign. + uint256 campaignId = uint256(bytes32(data[0:32])); + CampaignParams storage params = _campaignParams[campaignId]; + + SpentItem[] memory minimumReceived = new SpentItem[](1); + minimumReceived[0] = SpentItem({ + itemType: ItemType.ERC721, + token: params.offer[0].token, + identifier: params.offer[0].identifierOrCriteria, + amount: params.offer[0].startAmount + }); + + SpentItem[] memory maximumSpent = new SpentItem[](ids.length); + for (uint256 i = 0; i < ids.length;) { + maximumSpent[i] = + SpentItem({itemType: ItemType.ERC1155, token: msg.sender, identifier: ids[i], amount: values[i]}); + unchecked { + ++i; + } + } + + // _createOrder will revert if any validations fail. + _createOrder(from, minimumReceived, maximumSpent, data, true); + + // Transfer the tokens to the consideration item recipient. + address recipient = _getConsiderationRecipient(params.consideration, msg.sender); + ERC1155(msg.sender).safeBatchTransferFrom(address(this), recipient, ids, values, ""); + + // Transfer the newly minted token to the fulfiller. + ERC721(params.offer[0].token).safeTransferFrom(address(this), from, ids[0]); + + return IERC1155Receiver.onERC1155BatchReceived.selector; + } + + function getCampaign(uint256 campaignId) + external + view + returns (CampaignParams memory params, string memory uri, uint256 totalRedemptions) + { + if (campaignId >= _nextCampaignId) revert InvalidCampaignId(); + params = _campaignParams[campaignId]; + uri = _campaignURIs[campaignId]; + totalRedemptions = _totalRedemptions[campaignId]; + } + + function _getConsiderationRecipient(ConsiderationItem[] storage consideration, address token) + internal + view + returns (address) + { + for (uint256 i = 0; i < consideration.length;) { + if (consideration[i].token == token) { + return consideration[i].recipient; + } + unchecked { + ++i; + } + } + revert ConsiderationRecipientNotFound(token); + } + + function _isInactive(uint256 startTime, uint256 endTime) internal view returns (bool inactive) { + // Using the same check for time boundary from Seaport. + // startTime <= block.timestamp < endTime + assembly { + inactive := or(iszero(gt(endTime, timestamp())), gt(startTime, timestamp())) + } + } + + function _isValidTokenAddress(CampaignParams memory params, address token) internal pure returns (bool valid) { + for (uint256 i = 0; i < params.consideration.length;) { + if (params.consideration[i].token == token) { + valid = true; + break; + } + unchecked { + ++i; + } + } + } + + /** + * @notice Internal utility function to remove a uint from a supplied + * enumeration. + * + * @param toRemove The uint to remove. + * @param enumeration The enumerated uints to parse. + */ + function _removeFromEnumeration(uint256 toRemove, uint256[] storage enumeration) internal { + // Cache the length. + uint256 enumerationLength = enumeration.length; + for (uint256 i = 0; i < enumerationLength;) { + // Check if the enumerated element is the one we are deleting. + if (enumeration[i] == toRemove) { + // Swap with the last element. + enumeration[i] = enumeration[enumerationLength - 1]; + // Delete the (now duplicated) last element. + enumeration.pop(); + // Exit the loop. + break; + } + unchecked { + ++i; + } + } + } + + /** + * @notice Internal utility function to cast uint types to address + * to dedupe the need for multiple implementations of + * `_removeFromEnumeration`. + * + * @param fnIn The fn with uint input. + * + * @return fnOut The fn with address input. + */ + function _asAddressArray(function(uint256, uint256[] storage) internal fnIn) + internal + pure + returns (function(address, address[] storage) internal fnOut) + { + assembly { + fnOut := fnIn + } + } + + /** + * @dev Internal pure function to cast a `bool` value to a `uint256` value. + * + * @param b The `bool` value to cast. + * + * @return u The `uint256` value. + */ + function _cast(bool b) internal pure returns (uint256 u) { + assembly { + u := b + } + } +} diff --git a/lib/redeemables/src/interfaces/IDynamicTraits.sol b/lib/redeemables/src/interfaces/IDynamicTraits.sol new file mode 100644 index 0000000..45d831c --- /dev/null +++ b/lib/redeemables/src/interfaces/IDynamicTraits.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IERCDynamicTraits { + event TraitUpdated(uint256 indexed tokenId, bytes32 indexed traitKey, bytes32 oldValue, bytes32 newValue); + event TraitBulkUpdated(uint256 indexed fromTokenId, uint256 indexed toTokenId, bytes32 indexed traitKeyPattern); + + function getTrait(uint256 tokenId, bytes32 traitKey) external view returns (bytes32); +} diff --git a/lib/redeemables/src/interfaces/IERC1155Receiver.sol b/lib/redeemables/src/interfaces/IERC1155Receiver.sol new file mode 100644 index 0000000..ef19c11 --- /dev/null +++ b/lib/redeemables/src/interfaces/IERC1155Receiver.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IERC1155Receiver { + /** + * @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/lib/redeemables/src/interfaces/IERC1155RedemptionMintable.sol b/lib/redeemables/src/interfaces/IERC1155RedemptionMintable.sol new file mode 100644 index 0000000..a57e8c2 --- /dev/null +++ b/lib/redeemables/src/interfaces/IERC1155RedemptionMintable.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; + +interface IERC1155RedemptionMintable { + function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256 tokenId); +} diff --git a/lib/redeemables/src/interfaces/IERC721RedemptionMintable.sol b/lib/redeemables/src/interfaces/IERC721RedemptionMintable.sol new file mode 100644 index 0000000..0bc2cfe --- /dev/null +++ b/lib/redeemables/src/interfaces/IERC721RedemptionMintable.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; + +interface IERC721RedemptionMintable { + function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256 tokenId); +} diff --git a/lib/redeemables/src/interfaces/IERC7XXX.sol b/lib/redeemables/src/interfaces/IERC7XXX.sol new file mode 100644 index 0000000..92fc7bd --- /dev/null +++ b/lib/redeemables/src/interfaces/IERC7XXX.sol @@ -0,0 +1,28 @@ +interface IERC7XXX { + /* Events */ + event TraitUpdated(uint256 indexed tokenId, bytes32 indexed traitKey, bytes32 value); + event TraitUpdatedBulkConsecutive(uint256 fromTokenId, uint256 toTokenId, bytes32 indexed traitKeyPattern); + event TraitUpdatedBulkList(uint256[] tokenIds, bytes32 indexed traitKeyPattern); + event TraitLabelsURIUpdated(string uri); + + /* Getters */ + function getTrait(bytes32 traitKey, uint256 tokenId) external view returns (bytes32); + + // function getTotalTraitKeys() external view returns (uint256); + + // function getTraitKeyAt(uint256 index) external view returns (bytes32); + + // function getTraitLabelsURI() external view returns (string memory); + + // TODO to consider: + // function getTraitKeys() external view returns (bytes32[] memory); + + function getTraits(bytes32 traitKey, uint256[] calldata tokenIds) external view returns (bytes32[] memory); + + /* Setters */ + function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) external; + + function supportsInterface(bytes4 interfaceId) external view returns (bool); + + // function setTraitLabelsURI(string calldata uri) external; +} diff --git a/lib/redeemables/src/interfaces/IRedeemableContractOfferer.sol b/lib/redeemables/src/interfaces/IRedeemableContractOfferer.sol new file mode 100644 index 0000000..e261bbb --- /dev/null +++ b/lib/redeemables/src/interfaces/IRedeemableContractOfferer.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { + OfferItem, + ConsiderationItem, + SpentItem, + AdvancedOrder, + OrderParameters, + CriteriaResolver, + FulfillmentComponent +} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {CampaignParams, TraitRedemption} from "../lib/RedeemableStructs.sol"; + +interface IRedeemableContractOfferer { + /* Events */ + event CampaignUpdated(uint256 indexed campaignId, CampaignParams params, string URI); + event Redemption(uint256 indexed campaignId, bytes32 redemptionHash); + + /* Getters */ + function getCampaign(uint256 campaignId) + external + view + returns (CampaignParams memory params, string memory uri, uint256 totalRedemptions); + + /* Setters */ + function createCampaign(CampaignParams calldata params, string calldata uri) + external + returns (uint256 campaignId); + + function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata uri) external; +} diff --git a/lib/redeemables/src/lib/DynamicTraits.sol b/lib/redeemables/src/lib/DynamicTraits.sol new file mode 100644 index 0000000..d735b39 --- /dev/null +++ b/lib/redeemables/src/lib/DynamicTraits.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ECDSA} from "solady/src/utils/ECDSA.sol"; +import {IERCDynamicTraits} from "../interfaces/IDynamicTraits.sol"; +import {RedeemableErrorsAndEvents} from "./RedeemableErrorsAndEvents.sol"; + +contract DynamicTraits is IERCDynamicTraits, RedeemableErrorsAndEvents { + mapping(uint256 tokenId => mapping(bytes32 traitKey => bytes32 traitValue)) internal _traits; + + function getTrait(uint256 tokenId, bytes32 traitKey) public view virtual override returns (bytes32) { + return _traits[tokenId][traitKey]; + } + + function _setTrait(uint256 tokenId, bytes32 traitKey, bytes32 newValue) internal { + bytes32 oldValue = _traits[tokenId][traitKey]; + + if (oldValue == newValue) { + revert TraitValueUnchanged(traitKey, oldValue); + } + + _traits[tokenId][traitKey] = newValue; + + emit TraitUpdated(tokenId, traitKey, oldValue, newValue); + } + + function _setTraitBulk(uint256 fromTokenId, uint256 toTokenId, bytes32 traitKey, bytes32 newValue) internal { + for (uint256 tokenId = fromTokenId; tokenId <= toTokenId; tokenId++) { + _traits[tokenId][traitKey] = newValue; + } + + emit TraitBulkUpdated(fromTokenId, toTokenId, traitKey); + } +} diff --git a/lib/redeemables/src/lib/ERC1155RedemptionMintable.sol b/lib/redeemables/src/lib/ERC1155RedemptionMintable.sol new file mode 100644 index 0000000..11cf204 --- /dev/null +++ b/lib/redeemables/src/lib/ERC1155RedemptionMintable.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ERC1155} from "solady/src/tokens/ERC1155.sol"; +import {IERC1155RedemptionMintable} from "../interfaces/IERC1155RedemptionMintable.sol"; +import {SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; + +contract ERC1155RedemptionMintable is ERC1155, IERC1155RedemptionMintable { + address internal immutable _REDEEMABLE_CONTRACT_OFFERER; + address internal immutable _REDEEM_TOKEN; + + /// @dev Revert if the sender of mintRedemption is not the redeemable contract offerer. + error InvalidSender(); + + /// @dev Revert if the redemption spent is not the required token. + error InvalidRedemption(); + + constructor(address redeemableContractOfferer, address redeemToken) { + _REDEEMABLE_CONTRACT_OFFERER = redeemableContractOfferer; + _REDEEM_TOKEN = redeemToken; + } + + function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256 tokenId) { + if (msg.sender != _REDEEMABLE_CONTRACT_OFFERER) revert InvalidSender(); + + SpentItem memory spentItem = spent[0]; + if (spentItem.token != _REDEEM_TOKEN) revert InvalidRedemption(); + + // Mint the same token ID redeemed and same amount redeemed. + _mint(to, spentItem.identifier, spentItem.amount, ""); + + return spentItem.identifier; + } + + function uri(uint256 id) public pure override returns (string memory) { + return string(abi.encodePacked("https://example.com/", id)); + } +} diff --git a/lib/redeemables/src/lib/ERC721RedemptionMintable.sol b/lib/redeemables/src/lib/ERC721RedemptionMintable.sol new file mode 100644 index 0000000..5714b5e --- /dev/null +++ b/lib/redeemables/src/lib/ERC721RedemptionMintable.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ERC721} from "solady/src/tokens/ERC721.sol"; +import {IERC721RedemptionMintable} from "../interfaces/IERC721RedemptionMintable.sol"; +import {SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; + +contract ERC721RedemptionMintable is ERC721, IERC721RedemptionMintable { + address internal immutable _REDEEMABLE_CONTRACT_OFFERER; + address internal immutable _REDEEM_TOKEN; + + /// @dev Revert if the sender of mintRedemption is not the redeemable contract offerer. + error InvalidSender(); + + /// @dev Revert if the redemption spent is not the required token. + error InvalidRedemption(); + + constructor(address redeemableContractOfferer, address redeemToken) { + _REDEEMABLE_CONTRACT_OFFERER = redeemableContractOfferer; + _REDEEM_TOKEN = redeemToken; + } + + function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256 tokenId) { + if (msg.sender != _REDEEMABLE_CONTRACT_OFFERER) revert InvalidSender(); + + SpentItem memory spentItem = spent[0]; + if (spentItem.token != _REDEEM_TOKEN) revert InvalidRedemption(); + + // Mint the same token ID redeemed. + _mint(to, spentItem.identifier); + + return spentItem.identifier; + } + + function name() public pure override returns (string memory) { + return "ERC721RedemptionMintable"; + } + + function symbol() public pure override returns (string memory) { + return "721RM"; + } + + function tokenURI(uint256 tokenId) public pure override returns (string memory) { + return string(abi.encodePacked("https://example.com/", tokenId)); + } +} diff --git a/lib/redeemables/src/lib/ERC721RedemptionMintableWithCounter.sol b/lib/redeemables/src/lib/ERC721RedemptionMintableWithCounter.sol new file mode 100644 index 0000000..62a0968 --- /dev/null +++ b/lib/redeemables/src/lib/ERC721RedemptionMintableWithCounter.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ERC721} from "solady/src/tokens/ERC721.sol"; +import {IERC721RedemptionMintable} from "../interfaces/IERC721RedemptionMintable.sol"; +import {SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; + +contract ERC721RedemptionMintableWithCounter is ERC721, IERC721RedemptionMintable { + address internal immutable _REDEEMABLE_CONTRACT_OFFERER; + address internal immutable _REDEEM_TOKEN; + uint256 internal _tokenIdCounter; + + /// @dev Revert if the sender of mintRedemption is not the redeemable contract offerer. + error InvalidSender(); + + /// @dev Revert if the redemption spent is not the required token. + error InvalidRedemption(); + + constructor(address redeemableContractOfferer, address redeemToken) { + _REDEEMABLE_CONTRACT_OFFERER = redeemableContractOfferer; + _REDEEM_TOKEN = redeemToken; + } + + function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256 tokenId) { + if (msg.sender != _REDEEMABLE_CONTRACT_OFFERER) revert InvalidSender(); + + SpentItem memory spentItem = spent[0]; + if (spentItem.token != _REDEEM_TOKEN) revert InvalidRedemption(); + + // Mint the token. + _mint(to, _tokenIdCounter); + + tokenId = _tokenIdCounter; + + _tokenIdCounter++; + } + + function name() public pure override returns (string memory) { + return "ERC721RedemptionMintable"; + } + + function symbol() public pure override returns (string memory) { + return "721RM"; + } + + function tokenURI(uint256 tokenId) public pure override returns (string memory) { + return string(abi.encodePacked("https://example.com/", tokenId)); + } +} diff --git a/lib/redeemables/src/lib/RedeemableErrorsAndEvents.sol b/lib/redeemables/src/lib/RedeemableErrorsAndEvents.sol new file mode 100644 index 0000000..0c477a3 --- /dev/null +++ b/lib/redeemables/src/lib/RedeemableErrorsAndEvents.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {CampaignParams} from "./RedeemableStructs.sol"; + +interface RedeemableErrorsAndEvents { + /// Configuration errors + error NotManager(); + error InvalidTime(); + error NoConsiderationItems(); + error ConsiderationItemRecipientCannotBeZeroAddress(); + + /// Redemption errors + error InvalidCampaignId(); + error CampaignAlreadyExists(); + error InvalidCaller(address caller); + error NotActive(uint256 currentTimestamp, uint256 startTime, uint256 endTime); + error MaxRedemptionsReached(uint256 total, uint256 max); + error MaxCampaignRedemptionsReached(uint256 total, uint256 max); + error RedeemMismatchedLengths(); + error TraitValueUnchanged(bytes32 traitKey, bytes32 value); + error InvalidConsiderationLength(uint256 got, uint256 want); + error InvalidConsiderationItem(address got, address want); + error InvalidOfferLength(uint256 got, uint256 want); + error InvalidNativeOfferItem(); + error InvalidOwner(); + error InvalidRequiredValue(bytes32 got, bytes32 want); + error InvalidSubstandard(uint256 substandard); + error InvalidTraitRedemption(); + error InvalidTraitRedemptionToken(address token); + error ConsiderationRecipientNotFound(address token); + error RedemptionValuesAreImmutable(); + + /// Events + event CampaignUpdated(uint256 indexed campaignId, CampaignParams params, string uri); + event Redemption(uint256 indexed campaignId, bytes32 redemptionHash); +} diff --git a/lib/redeemables/src/lib/RedeemableStructs.sol b/lib/redeemables/src/lib/RedeemableStructs.sol new file mode 100644 index 0000000..dd6d2b4 --- /dev/null +++ b/lib/redeemables/src/lib/RedeemableStructs.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; + +struct CampaignParams { + uint32 startTime; + uint32 endTime; + uint32 maxCampaignRedemptions; + address manager; + address signer; + OfferItem[] offer; + ConsiderationItem[] consideration; +} + +struct TraitRedemption { + uint8 substandard; + address token; + uint256 identifier; + bytes32 traitKey; + bytes32 traitValue; + bytes32 substandardValue; +} + +struct RedemptionContext { + SpentItem[] spent; +} diff --git a/lib/redeemables/src/lib/SignedRedeem.sol b/lib/redeemables/src/lib/SignedRedeem.sol new file mode 100644 index 0000000..6891dbc --- /dev/null +++ b/lib/redeemables/src/lib/SignedRedeem.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Ownable} from "solady/src/auth/Ownable.sol"; +import {SignatureCheckerLib} from "solady/src/utils/SignatureCheckerLib.sol"; +import {SignedRedeemErrorsAndEvents} from "./SignedRedeemErrorsAndEvents.sol"; + +contract SignedRedeem is Ownable, SignedRedeemErrorsAndEvents { + /// @dev Signer approval to redeem tokens (e.g. KYC), required when set. + address internal _redeemSigner; + + /// @dev The used digests, each digest can only be used once. + mapping(bytes32 => bool) internal _usedDigests; + + /// @notice Internal constants for EIP-712: Typed structured + /// data hashing and signing + bytes32 internal constant _SIGNED_REDEEM_TYPEHASH = + keccak256("SignedRedeem(address owner,uint256[] tokenIds,uint256 salt)"); + bytes32 internal constant _EIP_712_DOMAIN_TYPEHASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + bytes32 internal constant _NAME_HASH = keccak256("SignedRedeem"); + bytes32 internal constant _VERSION_HASH = keccak256("1.0"); + uint256 internal immutable _CHAIN_ID = block.chainid; + bytes32 internal immutable _DOMAIN_SEPARATOR; + + constructor() { + _initializeOwner(msg.sender); + _DOMAIN_SEPARATOR = _deriveDomainSeparator(); + } + + function updateSigner(address newSigner) public onlyOwner { + _redeemSigner = newSigner; + } + + function _verifySignatureAndRecordDigest( + address owner, + uint256[] calldata tokenIds, + uint256 salt, + bytes calldata signature + ) internal { + // Get the digest. + bytes32 digest = _getDigest(owner, tokenIds, salt); + + // Revert if signature does not recover to signer. + if (!SignatureCheckerLib.isValidSignatureNowCalldata(_redeemSigner, digest, signature)) revert InvalidSigner(); + + // Revert if the digest is already used. + if (_usedDigests[digest]) revert DigestAlreadyUsed(); + + // Record digest as used. + _usedDigests[digest] = true; + } + + /* + * @notice Verify an EIP-712 signature by recreating the data structure + * that we signed on the client side, and then using that to recover + * the address that signed the signature for this data. + */ + function _getDigest(address owner, uint256[] calldata tokenIds, uint256 salt) + internal + view + returns (bytes32 digest) + { + digest = keccak256( + bytes.concat( + bytes2(0x1901), + _domainSeparator(), + keccak256(abi.encode(_SIGNED_REDEEM_TYPEHASH, owner, tokenIds, salt)) + ) + ); + } + + /** + * @dev Internal view function to get the EIP-712 domain separator. If the + * chainId matches the chainId set on deployment, the cached domain + * separator will be returned; otherwise, it will be derived from + * scratch. + * + * @return The domain separator. + */ + function _domainSeparator() internal view returns (bytes32) { + return block.chainid == _CHAIN_ID ? _DOMAIN_SEPARATOR : _deriveDomainSeparator(); + } + + /** + * @dev Internal view function to derive the EIP-712 domain separator. + * + * @return The derived domain separator. + */ + function _deriveDomainSeparator() internal view returns (bytes32) { + return keccak256(abi.encode(_EIP_712_DOMAIN_TYPEHASH, _NAME_HASH, _VERSION_HASH, block.chainid, address(this))); + } +} diff --git a/lib/redeemables/src/lib/SignedRedeemContractOfferer.sol b/lib/redeemables/src/lib/SignedRedeemContractOfferer.sol new file mode 100644 index 0000000..53028e5 --- /dev/null +++ b/lib/redeemables/src/lib/SignedRedeemContractOfferer.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {SignatureCheckerLib} from "solady/src/utils/SignatureCheckerLib.sol"; +import {SignedRedeemErrorsAndEvents} from "./SignedRedeemErrorsAndEvents.sol"; + +contract SignedRedeemContractOfferer is SignedRedeemErrorsAndEvents { + /// @dev The used digests, each digest can only be used once. + mapping(bytes32 => bool) internal _usedDigests; + + /// @notice Internal constants for EIP-712: Typed structured + /// data hashing and signing + bytes32 internal constant _SIGNED_REDEEM_TYPEHASH = + keccak256("SignedRedeem(address owner,address token,uint256[] tokenIds,bytes32 redemptionHash,uint256 salt)"); + bytes32 internal constant _EIP_712_DOMAIN_TYPEHASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + bytes32 internal constant _NAME_HASH = keccak256("SignedRedeem"); + bytes32 internal constant _VERSION_HASH = keccak256("1.0"); + uint256 internal immutable _CHAIN_ID = block.chainid; + bytes32 internal immutable _DOMAIN_SEPARATOR; + + constructor() { + _DOMAIN_SEPARATOR = _deriveDomainSeparator(); + } + + function _verifySignature( + address signer, + address owner, + SpentItem[] memory maximumSpent, + bytes32 redemptionHash, + uint256 salt, + bytes memory signature, + bool recordDigest + ) internal { + // Get the digest. + bytes32 digest = _getDigest(owner, maximumSpent, redemptionHash, salt); + + // Revert if signature does not recover to signer. + if (!SignatureCheckerLib.isValidSignatureNow(signer, digest, signature)) revert InvalidSigner(); + + // Revert if the digest is already used. + if (_usedDigests[digest]) revert DigestAlreadyUsed(); + + // Record digest as used. + if (recordDigest) _usedDigests[digest] = true; + } + + /* + * @notice Verify an EIP-712 signature by recreating the data structure + * that we signed on the client side, and then using that to recover + * the address that signed the signature for this data. + */ + function _getDigest(address owner, SpentItem[] memory maximumSpent, bytes32 redemptionHash, uint256 salt) + internal + view + returns (bytes32 digest) + { + digest = keccak256( + bytes.concat( + bytes2(0x1901), + _domainSeparator(), + keccak256(abi.encode(_SIGNED_REDEEM_TYPEHASH, owner, maximumSpent, redemptionHash, salt)) + ) + ); + } + + /** + * @dev Internal view function to get the EIP-712 domain separator. If the + * chainId matches the chainId set on deployment, the cached domain + * separator will be returned; otherwise, it will be derived from + * scratch. + * + * @return The domain separator. + */ + function _domainSeparator() internal view returns (bytes32) { + return block.chainid == _CHAIN_ID ? _DOMAIN_SEPARATOR : _deriveDomainSeparator(); + } + + /** + * @dev Internal view function to derive the EIP-712 domain separator. + * + * @return The derived domain separator. + */ + function _deriveDomainSeparator() internal view returns (bytes32) { + return keccak256(abi.encode(_EIP_712_DOMAIN_TYPEHASH, _NAME_HASH, _VERSION_HASH, block.chainid, address(this))); + } +} diff --git a/lib/redeemables/src/lib/SignedRedeemErrorsAndEvents.sol b/lib/redeemables/src/lib/SignedRedeemErrorsAndEvents.sol new file mode 100644 index 0000000..b06e5be --- /dev/null +++ b/lib/redeemables/src/lib/SignedRedeemErrorsAndEvents.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface SignedRedeemErrorsAndEvents { + error InvalidSigner(); + error DigestAlreadyUsed(); +} diff --git a/lib/redeemables/test/RedeemViaSeaport-1155.t.sol b/lib/redeemables/test/RedeemViaSeaport-1155.t.sol new file mode 100644 index 0000000..5414012 --- /dev/null +++ b/lib/redeemables/test/RedeemViaSeaport-1155.t.sol @@ -0,0 +1,1233 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Solarray} from "solarray/Solarray.sol"; +import {BaseOrderTest} from "./utils/BaseOrderTest.sol"; +import {TestERC20} from "./utils/mocks/TestERC20.sol"; +import {TestERC721} from "./utils/mocks/TestERC721.sol"; +import {TestERC1155} from "./utils/mocks/TestERC1155.sol"; +import { + OfferItem, + ConsiderationItem, + SpentItem, + AdvancedOrder, + OrderParameters, + CriteriaResolver, + FulfillmentComponent +} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {ItemType, OrderType, Side} from "seaport-sol/src/SeaportEnums.sol"; +import {MockERC721DynamicTraits} from "./utils/mocks/MockERC721DynamicTraits.sol"; +import {OfferItemLib, ConsiderationItemLib, OrderParametersLib} from "seaport-sol/src/SeaportSol.sol"; +import {RedeemableContractOfferer} from "../src/RedeemableContractOfferer.sol"; +import {CampaignParams, TraitRedemption} from "../src/lib/RedeemableStructs.sol"; +import {RedeemableErrorsAndEvents} from "../src/lib/RedeemableErrorsAndEvents.sol"; +import {ERC1155RedemptionMintable} from "../src/lib/ERC1155RedemptionMintable.sol"; +import {ERC721RedemptionMintable} from "../src/lib/ERC721RedemptionMintable.sol"; +import {ERC721RedemptionMintableWithCounter} from "../src/lib/ERC721RedemptionMintableWithCounter.sol"; +import {Merkle} from "../lib/murky/src/Merkle.sol"; + +contract RedeemViaSeaport1155 is BaseOrderTest, RedeemableErrorsAndEvents { + using OrderParametersLib for OrderParameters; + + error InvalidContractOrder(bytes32 orderHash); + + RedeemableContractOfferer offerer; + TestERC1155 redeemableToken; + ERC1155RedemptionMintable redemptionToken; + CriteriaResolver[] criteriaResolvers; + Merkle merkle = new Merkle(); + + address constant _BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD; + + function setUp() public override { + super.setUp(); + offerer = new RedeemableContractOfferer( + address(conduit), + conduitKey, + address(seaport) + ); + redeemableToken = new TestERC1155(); + redemptionToken = new ERC1155RedemptionMintable( + address(offerer), + address(redeemableToken) + ); + vm.label(address(redeemableToken), "redeemableToken"); + vm.label(address(redemptionToken), "redemptionToken"); + } + + function testRedeemWithSeaport() public { + uint256 tokenId = 2; + uint256 amount = 10 ** 18; + redeemableToken.mint(address(this), tokenId, amount); + redeemableToken.setApprovalForAll(address(conduit), true); + + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC1155_WITH_CRITERIA, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: amount, + endAmount: amount + }); + + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC1155_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: 0, + startAmount: amount, + endAmount: amount, + recipient: payable(_BURN_ADDRESS) + }); + + { + CampaignParams memory params = CampaignParams({ + offer: offer, + consideration: consideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + offerer.createCampaign(params, ""); + } + + // uint256 campaignId = 1; + // bytes32 redemptionHash = bytes32(0); + + { + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC1155, + token: address(redemptionToken), + identifierOrCriteria: tokenId, + startAmount: amount, + endAmount: amount + }); + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC1155, + token: address(redeemableToken), + identifierOrCriteria: tokenId, + startAmount: amount, + endAmount: amount, + recipient: payable(_BURN_ADDRESS) + }); + + assertGt(uint256(consideration[0].itemType), uint256(considerationFromEvent[0].itemType)); + + bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash + consideration[0].identifierOrCriteria = tokenId; + + // TODO: validate OrderFulfilled event + OrderParameters memory parameters = OrderParametersLib.empty().withOfferer(address(offerer)).withOrderType( + OrderType.CONTRACT + ).withConsideration(considerationFromEvent).withOffer(offer).withConduitKey(conduitKey).withStartTime( + block.timestamp + ).withEndTime(block.timestamp + 1).withTotalOriginalConsiderationItems(consideration.length); + AdvancedOrder memory order = AdvancedOrder({ + parameters: parameters, + numerator: 1, + denominator: 1, + signature: "", + extraData: extraData + }); + + // { + // bytes + // vm.expectEmit(true, true, true, true); + // emit OrderFulfilled(); + // } + + seaport.fulfillAdvancedOrder({ + advancedOrder: order, + criteriaResolvers: criteriaResolvers, + fulfillerConduitKey: conduitKey, + recipient: address(0) + }); + + assertEq(redeemableToken.balanceOf(_BURN_ADDRESS, tokenId), amount); + assertEq(redemptionToken.balanceOf(address(this), tokenId), amount); + } + } + + // TODO: write test with ETH redemption consideration + // TODO: 1155 tests with same tokenId (amount > 1), different tokenIds + // TODO: update erc20 amount to use decimals + + function testRedeemAndSendErc20ToThirdAddressViaSeaport() public { + uint256 tokenId = 2; + uint256 amount = 10 ** 18; + redeemableToken.mint(address(this), tokenId, amount); + redeemableToken.setApprovalForAll(address(conduit), true); + + // Deploy the ERC20 + TestERC20 erc20 = new TestERC20(); + uint256 erc20Amount = 10; + + // Mint 100 tokens to the test contract + erc20.mint(address(this), 100); + + // Approve the conduit to spend tokens + erc20.approve(address(conduit), type(uint256).max); + + OfferItem[] memory campaignOffer = new OfferItem[](1); + campaignOffer[0] = OfferItem({ + itemType: ItemType.ERC1155_WITH_CRITERIA, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: amount, + endAmount: amount + }); + + ConsiderationItem[] memory campaignConsideration = new ConsiderationItem[](2); + campaignConsideration[0] = ConsiderationItem({ + itemType: ItemType.ERC1155_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: 0, + startAmount: amount, + endAmount: amount, + recipient: payable(_BURN_ADDRESS) + }); + campaignConsideration[1] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20), + identifierOrCriteria: 0, + startAmount: erc20Amount, + endAmount: erc20Amount, + recipient: payable(eve.addr) + }); + + { + CampaignParams memory params = CampaignParams({ + offer: campaignOffer, + consideration: campaignConsideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + offerer.createCampaign(params, ""); + } + + // uint256 campaignId = 1; + // bytes32 redemptionHash = bytes32(0); + + { + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC1155, + token: address(redemptionToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1 + }); + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC1155, + token: address(redeemableToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + assertGt(uint256(campaignConsideration[0].itemType), uint256(considerationFromEvent[0].itemType)); + + bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash + + campaignConsideration[0].itemType = ItemType.ERC1155; + campaignConsideration[0].identifierOrCriteria = tokenId; + + // TODO: validate OrderFulfilled event + OrderParameters memory parameters = OrderParametersLib.empty().withOfferer(address(offerer)).withOrderType( + OrderType.CONTRACT + ).withConsideration(campaignConsideration).withOffer(campaignOffer).withConduitKey(conduitKey).withStartTime( + block.timestamp + ).withEndTime(block.timestamp + 1).withTotalOriginalConsiderationItems(campaignConsideration.length); + AdvancedOrder memory order = AdvancedOrder({ + parameters: parameters, + numerator: 1, + denominator: 1, + signature: "", + extraData: extraData + }); + + uint256 erc20BalanceBefore = erc20.balanceOf(address(this)); + + seaport.fulfillAdvancedOrder({ + advancedOrder: order, + criteriaResolvers: criteriaResolvers, + fulfillerConduitKey: conduitKey, + recipient: address(0) + }); + + assertEq(redeemableToken.balanceOf(_BURN_ADDRESS, tokenId), amount); + assertEq(redemptionToken.balanceOf(address(this), tokenId), amount); + assertEq(erc20BalanceBefore - erc20.balanceOf(address(this)), erc20Amount); + assertEq(erc20.balanceOf(eve.addr), erc20Amount); + } + } + + // TODO: add resolved tokenId to extradata + // TODO: fix redemptionToken being minted with merkle root + function xtestRedeemWithCriteriaResolversViaSeaport() public { + uint256 tokenId = 2; + uint256 amount = 10 ** 18; + redeemableToken.mint(address(this), tokenId, amount); + redeemableToken.setApprovalForAll(address(conduit), true); + + CriteriaResolver[] memory resolvers = new CriteriaResolver[](1); + + // Create an array of hashed identifiers (0-4) + // Only tokenIds 0-4 can be redeemed + bytes32[] memory hashedIdentifiers = new bytes32[](5); + for (uint256 i = 0; i < hashedIdentifiers.length; i++) { + hashedIdentifiers[i] = keccak256(abi.encode(i)); + } + bytes32 root = merkle.getRoot(hashedIdentifiers); + + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC1155_WITH_CRITERIA, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + // Contract offerer will only consider tokenIds 0-4 + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC1155_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: uint256(root), + startAmount: amount, + endAmount: amount, + recipient: payable(_BURN_ADDRESS) + }); + + { + CampaignParams memory params = CampaignParams({ + offer: offer, + consideration: consideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + offerer.createCampaign(params, ""); + } + + { + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(redemptionToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1 + }); + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemableToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + assertGt(uint256(consideration[0].itemType), uint256(considerationFromEvent[0].itemType)); + + bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash + + OrderParameters memory parameters = OrderParametersLib.empty().withOfferer(address(offerer)).withOrderType( + OrderType.CONTRACT + ).withConsideration(consideration).withOffer(offer).withConduitKey(conduitKey).withStartTime( + block.timestamp + ).withEndTime(block.timestamp + 1).withTotalOriginalConsiderationItems(consideration.length); + AdvancedOrder memory order = AdvancedOrder({ + parameters: parameters, + numerator: 1, + denominator: 1, + signature: "", + extraData: extraData + }); + + resolvers[0] = CriteriaResolver({ + orderIndex: 0, + side: Side.CONSIDERATION, + index: 0, + identifier: tokenId, + criteriaProof: merkle.getProof(hashedIdentifiers, 2) + }); + + // TODO: validate OrderFulfilled event + // vm.expectEmit(true, true, true, true); + // emit OrderFulfilled(); + + seaport.fulfillAdvancedOrder({ + advancedOrder: order, + criteriaResolvers: resolvers, + fulfillerConduitKey: conduitKey, + recipient: address(0) + }); + + // TODO: failing because redemptionToken tokenId is merkle root + assertEq(redeemableToken.balanceOf(_BURN_ADDRESS, tokenId), amount); + // assertEq(redemptionToken.ownerOf(tokenId), address(this)); + } + } + + // // TODO: burn 1, send weth to third address, also redeem trait + // // TODO: mock erc20 to third address or burn + // // TODO: mock erc721 with tokenId counter + // // TODO: make MockErc20RedemptionMintable with mintRedemption + // // TODO: burn nft and send erc20 to third address, get nft and erc20 + // // TODO: mintRedemption should return tokenIds array + // // TODO: then add dynamic traits + // // TODO: by EOW, have dynamic traits demo + + // // notice: redemptionToken tokenId will be tokenId of first item in consideration + // function testBurn2Redeem1ViaSeaport() public { + // // Set the two tokenIds to be burned + // uint256 burnTokenId0 = 2; + // uint256 burnTokenId1 = 3; + + // // Mint two redeemableTokens of tokenId burnTokenId0 and burnTokenId1 to the test contract + // redeemableToken.mint(address(this), burnTokenId0); + // redeemableToken.mint(address(this), burnTokenId1); + + // // Approve the conduit to transfer the redeemableTokens on behalf of the test contract + // redeemableToken.setApprovalForAll(address(conduit), true); + + // // Create a single-item OfferItem array with the redemption token the caller will receive + // OfferItem[] memory offer = new OfferItem[](1); + // offer[0] = OfferItem({ + // itemType: ItemType.ERC721_WITH_CRITERIA, + // token: address(redemptionToken), + // identifierOrCriteria: 0, + // startAmount: 1, + // endAmount: 1 + // }); + + // // Create a single-item ConsiderationItem array and require the caller to burn two redeemableTokens (of any tokenId) + // ConsiderationItem[] memory consideration = new ConsiderationItem[](2); + // consideration[0] = ConsiderationItem({ + // itemType: ItemType.ERC721_WITH_CRITERIA, + // token: address(redeemableToken), + // identifierOrCriteria: 0, + // startAmount: 1, + // endAmount: 1, + // recipient: payable(_BURN_ADDRESS) + // }); + + // consideration[1] = ConsiderationItem({ + // itemType: ItemType.ERC721_WITH_CRITERIA, + // token: address(redeemableToken), + // identifierOrCriteria: 0, + // startAmount: 1, + // endAmount: 1, + // recipient: payable(_BURN_ADDRESS) + // }); + + // // Create the CampaignParams with the offer and consideration from above. + // { + // CampaignParams memory params = CampaignParams({ + // offer: offer, + // consideration: consideration, + // signer: address(0), + // startTime: uint32(block.timestamp), + // endTime: uint32(block.timestamp + 1000), + // maxCampaignRedemptions: 5, + // manager: address(this) + // }); + + // // Call createCampaign on the offerer and pass in the CampaignParams + // offerer.createCampaign(params, ""); + // } + + // // uint256 campaignId = 1; + // // bytes32 redemptionHash = bytes32(0); + + // { + // // Create the offer we expect to be emitted in the event + // OfferItem[] memory offerFromEvent = new OfferItem[](1); + // offerFromEvent[0] = OfferItem({ + // itemType: ItemType.ERC721, + // token: address(redemptionToken), + // identifierOrCriteria: burnTokenId0, + // startAmount: 1, + // endAmount: 1 + // }); + + // // Create the consideration we expect to be emitted in the event + // ConsiderationItem[] + // memory considerationFromEvent = new ConsiderationItem[](2); + // considerationFromEvent[0] = ConsiderationItem({ + // itemType: ItemType.ERC721, + // token: address(redeemableToken), + // identifierOrCriteria: burnTokenId0, + // startAmount: 1, + // endAmount: 1, + // recipient: payable(_BURN_ADDRESS) + // }); + + // considerationFromEvent[1] = ConsiderationItem({ + // itemType: ItemType.ERC721, + // token: address(redeemableToken), + // identifierOrCriteria: burnTokenId1, + // startAmount: 1, + // endAmount: 1, + // recipient: payable(_BURN_ADDRESS) + // }); + + // // Check that the consideration passed into createCampaign has itemType ERC721_WITH_CRITERIA + // assertEq(uint256(consideration[0].itemType), 4); + + // // Check that the consideration emitted in the event has itemType ERC721 + // assertEq(uint256(considerationFromEvent[0].itemType), 2); + // assertEq(uint256(considerationFromEvent[1].itemType), 2); + + // // Create the extraData to be passed into fulfillAdvancedOrder + // bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash + + // // TODO: validate OrderFulfilled event + + // // Create the OrderParameters to be passed into fulfillAdvancedOrder + // OrderParameters memory parameters = OrderParametersLib + // .empty() + // .withOfferer(address(offerer)) + // .withOrderType(OrderType.CONTRACT) + // .withConsideration(considerationFromEvent) + // .withOffer(offer) + // .withConduitKey(conduitKey) + // .withStartTime(block.timestamp) + // .withEndTime(block.timestamp + 1) + // .withTotalOriginalConsiderationItems( + // considerationFromEvent.length + // ); + + // // Create the AdvancedOrder to be passed into fulfillAdvancedOrder + // AdvancedOrder memory order = AdvancedOrder({ + // parameters: parameters, + // numerator: 1, + // denominator: 1, + // signature: "", + // extraData: extraData + // }); + + // // Call fulfillAdvancedOrder + // seaport.fulfillAdvancedOrder({ + // advancedOrder: order, + // criteriaResolvers: criteriaResolvers, + // fulfillerConduitKey: conduitKey, + // recipient: address(0) + // }); + + // // Check that the two redeemable tokens have been burned + // assertEq(redeemableToken.ownerOf(burnTokenId0), _BURN_ADDRESS); + // assertEq(redeemableToken.ownerOf(burnTokenId1), _BURN_ADDRESS); + + // // Check that the redemption token has been minted to the test contract + // assertEq(redemptionToken.ownerOf(burnTokenId0), address(this)); + // } + // } + + // function testBurn1Redeem2WithSeaport() public { + // // Set the two tokenIds to be redeemed + // uint256 redemptionTokenId0 = 0; + // uint256 redemptionTokenId1 = 1; + + // // Set the tokenId to be burned to the first tokenId to be redeemed + // uint256 redeemableTokenId0 = redemptionTokenId0; + + // ERC721RedemptionMintableWithCounter redemptionTokenWithCounter = new ERC721RedemptionMintableWithCounter( + // address(offerer), + // address(redeemableToken) + // ); + + // // Mint a redeemableToken of tokenId redeemableTokenId0 to the test contract + // redeemableToken.mint(address(this), redeemableTokenId0); + + // // Approve the conduit to transfer the redeemableTokens on behalf of the test contract + // redeemableToken.setApprovalForAll(address(conduit), true); + + // // Create a two-item OfferItem array with the 2 redemptionTokens the caller will receive + // OfferItem[] memory offer = new OfferItem[](2); + // offer[0] = OfferItem({ + // itemType: ItemType.ERC721_WITH_CRITERIA, + // token: address(redemptionTokenWithCounter), + // identifierOrCriteria: 0, + // startAmount: 1, + // endAmount: 1 + // }); + + // offer[1] = OfferItem({ + // itemType: ItemType.ERC721_WITH_CRITERIA, + // token: address(redemptionTokenWithCounter), + // identifierOrCriteria: 0, + // startAmount: 1, + // endAmount: 1 + // }); + + // // Create a single-item ConsiderationItem array with the redeemableToken the caller will burn + // ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + // consideration[0] = ConsiderationItem({ + // itemType: ItemType.ERC721_WITH_CRITERIA, + // token: address(redeemableToken), + // identifierOrCriteria: 0, + // startAmount: 1, + // endAmount: 1, + // recipient: payable(_BURN_ADDRESS) + // }); + + // // Create the CampaignParams with the offer and consideration from above. + // { + // CampaignParams memory params = CampaignParams({ + // offer: offer, + // consideration: consideration, + // signer: address(0), + // startTime: uint32(block.timestamp), + // endTime: uint32(block.timestamp + 1000), + // maxCampaignRedemptions: 5, + // manager: address(this) + // }); + + // // Call createCampaign on the offerer and pass in the CampaignParams + // offerer.createCampaign(params, ""); + // } + + // // uint256 campaignId = 1; + // // bytes32 redemptionHash = bytes32(0); + + // { + // // Create the offer we expect to be emitted in the event + // OfferItem[] memory offerFromEvent = new OfferItem[](2); + // offerFromEvent[0] = OfferItem({ + // itemType: ItemType.ERC721, + // token: address(redemptionTokenWithCounter), + // identifierOrCriteria: redemptionTokenId0, + // startAmount: 1, + // endAmount: 1 + // }); + + // offerFromEvent[1] = OfferItem({ + // itemType: ItemType.ERC721, + // token: address(redemptionTokenWithCounter), + // identifierOrCriteria: redemptionTokenId1, + // startAmount: 1, + // endAmount: 1 + // }); + + // // Create the consideration we expect to be emitted in the event + // ConsiderationItem[] + // memory considerationFromEvent = new ConsiderationItem[](1); + // considerationFromEvent[0] = ConsiderationItem({ + // itemType: ItemType.ERC721, + // token: address(redeemableToken), + // identifierOrCriteria: redeemableTokenId0, + // startAmount: 1, + // endAmount: 1, + // recipient: payable(_BURN_ADDRESS) + // }); + + // // Check that the consideration passed into createCampaign has itemType ERC721_WITH_CRITERIA + // assertEq(uint256(consideration[0].itemType), 4); + + // // Check that the consideration emitted in the event has itemType ERC721 + // assertEq(uint256(considerationFromEvent[0].itemType), 2); + + // // Create the extraData to be passed into fulfillAdvancedOrder + // bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash + + // // TODO: validate OrderFulfilled event + + // // Create the OrderParameters to be passed into fulfillAdvancedOrder + // OrderParameters memory parameters = OrderParametersLib + // .empty() + // .withOfferer(address(offerer)) + // .withOrderType(OrderType.CONTRACT) + // .withConsideration(considerationFromEvent) + // .withOffer(offer) + // .withConduitKey(conduitKey) + // .withStartTime(block.timestamp) + // .withEndTime(block.timestamp + 1) + // .withTotalOriginalConsiderationItems( + // considerationFromEvent.length + // ); + + // // Create the AdvancedOrder to be passed into fulfillAdvancedOrder + // AdvancedOrder memory order = AdvancedOrder({ + // parameters: parameters, + // numerator: 1, + // denominator: 1, + // signature: "", + // extraData: extraData + // }); + + // // Call fulfillAdvancedOrder + // seaport.fulfillAdvancedOrder({ + // advancedOrder: order, + // criteriaResolvers: criteriaResolvers, + // fulfillerConduitKey: conduitKey, + // recipient: address(0) + // }); + + // // Check that the redeemableToken has been burned + // assertEq( + // redeemableToken.ownerOf(redeemableTokenId0), + // _BURN_ADDRESS + // ); + + // // Check that the two redemptionTokens has been minted to the test contract + // assertEq( + // redemptionTokenWithCounter.ownerOf(redemptionTokenId0), + // address(this) + // ); + // assertEq( + // redemptionTokenWithCounter.ownerOf(redemptionTokenId1), + // address(this) + // ); + // } + // } + + // function testBurn2SeparateRedeemableTokensRedeem1ViaSeaport() public { + // // Set the tokenId to be burned + // uint256 burnTokenId0 = 2; + + // // Create the second redeemableToken to be burned + // TestERC721 redeemableTokenTwo = new TestERC721(); + + // // Mint one redeemableToken ane one redeemableTokenTwo of tokenId burnTokenId0 to the test contract + // redeemableToken.mint(address(this), burnTokenId0); + // redeemableTokenTwo.mint(address(this), burnTokenId0); + + // // Approve the conduit to transfer the redeemableTokens on behalf of the test contract + // redeemableToken.setApprovalForAll(address(conduit), true); + // redeemableTokenTwo.setApprovalForAll(address(conduit), true); + + // // Create a single-item OfferItem array with the redemption token the caller will receive + // OfferItem[] memory offer = new OfferItem[](1); + // offer[0] = OfferItem({ + // itemType: ItemType.ERC721_WITH_CRITERIA, + // token: address(redemptionToken), + // identifierOrCriteria: 0, + // startAmount: 1, + // endAmount: 1 + // }); + + // // Create a two-item ConsiderationItem array and require the caller to burn one redeemableToken and one redeemableTokenTwo (of any tokenId) + // ConsiderationItem[] memory consideration = new ConsiderationItem[](2); + // consideration[0] = ConsiderationItem({ + // itemType: ItemType.ERC721_WITH_CRITERIA, + // token: address(redeemableToken), + // identifierOrCriteria: 0, + // startAmount: 1, + // endAmount: 1, + // recipient: payable(_BURN_ADDRESS) + // }); + + // consideration[1] = ConsiderationItem({ + // itemType: ItemType.ERC721_WITH_CRITERIA, + // token: address(redeemableTokenTwo), + // identifierOrCriteria: 0, + // startAmount: 1, + // endAmount: 1, + // recipient: payable(_BURN_ADDRESS) + // }); + + // // Create the CampaignParams with the offer and consideration from above. + // { + // CampaignParams memory params = CampaignParams({ + // offer: offer, + // consideration: consideration, + // signer: address(0), + // startTime: uint32(block.timestamp), + // endTime: uint32(block.timestamp + 1000), + // maxCampaignRedemptions: 5, + // manager: address(this) + // }); + + // // Call createCampaign on the offerer and pass in the CampaignParams + // offerer.createCampaign(params, ""); + // } + + // // uint256 campaignId = 1; + // // bytes32 redemptionHash = bytes32(0); + + // { + // // Create the offer we expect to be emitted in the event + // OfferItem[] memory offerFromEvent = new OfferItem[](1); + // offerFromEvent[0] = OfferItem({ + // itemType: ItemType.ERC721, + // token: address(redemptionToken), + // identifierOrCriteria: burnTokenId0, + // startAmount: 1, + // endAmount: 1 + // }); + + // // Create the consideration we expect to be emitted in the event + // ConsiderationItem[] + // memory considerationFromEvent = new ConsiderationItem[](2); + // considerationFromEvent[0] = ConsiderationItem({ + // itemType: ItemType.ERC721, + // token: address(redeemableToken), + // identifierOrCriteria: burnTokenId0, + // startAmount: 1, + // endAmount: 1, + // recipient: payable(_BURN_ADDRESS) + // }); + + // considerationFromEvent[1] = ConsiderationItem({ + // itemType: ItemType.ERC721, + // token: address(redeemableTokenTwo), + // identifierOrCriteria: burnTokenId0, + // startAmount: 1, + // endAmount: 1, + // recipient: payable(_BURN_ADDRESS) + // }); + + // // Check that the consideration passed into createCampaign has itemType ERC721_WITH_CRITERIA + // assertEq(uint256(consideration[0].itemType), 4); + + // // Check that the consideration emitted in the event has itemType ERC721 + // assertEq(uint256(considerationFromEvent[0].itemType), 2); + // assertEq(uint256(considerationFromEvent[1].itemType), 2); + + // // Create the extraData to be passed into fulfillAdvancedOrder + // bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash + + // // TODO: validate OrderFulfilled event + + // // Create the OrderParameters to be passed into fulfillAdvancedOrder + // OrderParameters memory parameters = OrderParametersLib + // .empty() + // .withOfferer(address(offerer)) + // .withOrderType(OrderType.CONTRACT) + // .withConsideration(considerationFromEvent) + // .withOffer(offer) + // .withConduitKey(conduitKey) + // .withStartTime(block.timestamp) + // .withEndTime(block.timestamp + 1) + // .withTotalOriginalConsiderationItems( + // considerationFromEvent.length + // ); + + // // Create the AdvancedOrder to be passed into fulfillAdvancedOrder + // AdvancedOrder memory order = AdvancedOrder({ + // parameters: parameters, + // numerator: 1, + // denominator: 1, + // signature: "", + // extraData: extraData + // }); + + // // Call fulfillAdvancedOrder + // seaport.fulfillAdvancedOrder({ + // advancedOrder: order, + // criteriaResolvers: criteriaResolvers, + // fulfillerConduitKey: conduitKey, + // recipient: address(0) + // }); + + // // Check that one redeemableToken and one redeemableTokenTwo have been burned + // assertEq(redeemableToken.ownerOf(burnTokenId0), _BURN_ADDRESS); + // assertEq(redeemableTokenTwo.ownerOf(burnTokenId0), _BURN_ADDRESS); + + // // Check that the redemption token has been minted to the test contract + // assertEq(redemptionToken.ownerOf(burnTokenId0), address(this)); + // } + // } + + // // TODO: add multi-redeem file + + // function testBurn1Redeem2SeparateRedemptionTokensWithSeaport() public { + // // Set the tokenId to be redeemed + // uint256 redemptionTokenId = 2; + + // // Set the tokenId to be burned to the first tokenId to be redeemed + // uint256 redeemableTokenId = redemptionTokenId; + + // // Create a new ERC721RedemptionMintable redemptionTokenTwo + // ERC721RedemptionMintable redemptionTokenTwo = new ERC721RedemptionMintable( + // address(offerer), + // address(redeemableToken) + // ); + + // // Mint a redeemableToken of tokenId redeemableTokenId to the test contract + // redeemableToken.mint(address(this), redeemableTokenId); + + // // Approve the conduit to transfer the redeemableTokens on behalf of the test contract + // redeemableToken.setApprovalForAll(address(conduit), true); + + // // Create a two-item OfferItem array with the one redemptionToken and one redemptionTokenTwo the caller will receive + // OfferItem[] memory offer = new OfferItem[](2); + // offer[0] = OfferItem({ + // itemType: ItemType.ERC721_WITH_CRITERIA, + // token: address(redemptionToken), + // identifierOrCriteria: 0, + // startAmount: 1, + // endAmount: 1 + // }); + + // offer[1] = OfferItem({ + // itemType: ItemType.ERC721_WITH_CRITERIA, + // token: address(redemptionTokenTwo), + // identifierOrCriteria: 0, + // startAmount: 1, + // endAmount: 1 + // }); + + // // Create a single-item ConsiderationItem array with the redeemableToken the caller will burn + // ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + // consideration[0] = ConsiderationItem({ + // itemType: ItemType.ERC721_WITH_CRITERIA, + // token: address(redeemableToken), + // identifierOrCriteria: 0, + // startAmount: 1, + // endAmount: 1, + // recipient: payable(_BURN_ADDRESS) + // }); + + // // Create the CampaignParams with the offer and consideration from above. + // { + // CampaignParams memory params = CampaignParams({ + // offer: offer, + // consideration: consideration, + // signer: address(0), + // startTime: uint32(block.timestamp), + // endTime: uint32(block.timestamp + 1000), + // maxCampaignRedemptions: 5, + // manager: address(this) + // }); + + // // Call createCampaign on the offerer and pass in the CampaignParams + // offerer.createCampaign(params, ""); + // } + + // // uint256 campaignId = 1; + // // bytes32 redemptionHash = bytes32(0); + + // { + // // Create the offer we expect to be emitted in the event + // OfferItem[] memory offerFromEvent = new OfferItem[](2); + // offerFromEvent[0] = OfferItem({ + // itemType: ItemType.ERC721, + // token: address(redemptionToken), + // identifierOrCriteria: redemptionTokenId, + // startAmount: 1, + // endAmount: 1 + // }); + + // offerFromEvent[1] = OfferItem({ + // itemType: ItemType.ERC721, + // token: address(redemptionToken), + // identifierOrCriteria: redemptionTokenId, + // startAmount: 1, + // endAmount: 1 + // }); + + // // Create the consideration we expect to be emitted in the event + // ConsiderationItem[] + // memory considerationFromEvent = new ConsiderationItem[](1); + // considerationFromEvent[0] = ConsiderationItem({ + // itemType: ItemType.ERC721, + // token: address(redeemableToken), + // identifierOrCriteria: redeemableTokenId, + // startAmount: 1, + // endAmount: 1, + // recipient: payable(_BURN_ADDRESS) + // }); + + // // Check that the consideration passed into createCampaign has itemType ERC721_WITH_CRITERIA + // assertEq(uint256(consideration[0].itemType), 4); + + // // Check that the consideration emitted in the event has itemType ERC721 + // assertEq(uint256(considerationFromEvent[0].itemType), 2); + + // // Create the extraData to be passed into fulfillAdvancedOrder + // bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash + + // // TODO: validate OrderFulfilled event + + // // Create the OrderParameters to be passed into fulfillAdvancedOrder + // OrderParameters memory parameters = OrderParametersLib + // .empty() + // .withOfferer(address(offerer)) + // .withOrderType(OrderType.CONTRACT) + // .withConsideration(considerationFromEvent) + // .withOffer(offer) + // .withConduitKey(conduitKey) + // .withStartTime(block.timestamp) + // .withEndTime(block.timestamp + 1) + // .withTotalOriginalConsiderationItems( + // considerationFromEvent.length + // ); + + // // Create the AdvancedOrder to be passed into fulfillAdvancedOrder + // AdvancedOrder memory order = AdvancedOrder({ + // parameters: parameters, + // numerator: 1, + // denominator: 1, + // signature: "", + // extraData: extraData + // }); + + // // Call fulfillAdvancedOrder + // seaport.fulfillAdvancedOrder({ + // advancedOrder: order, + // criteriaResolvers: criteriaResolvers, + // fulfillerConduitKey: conduitKey, + // recipient: address(0) + // }); + + // // Check that the redeemableToken has been burned + // assertEq(redeemableToken.ownerOf(redeemableTokenId), _BURN_ADDRESS); + + // // Check that the two redemptionTokens has been minted to the test contract + // assertEq(redemptionToken.ownerOf(redemptionTokenId), address(this)); + // assertEq( + // redemptionTokenTwo.ownerOf(redemptionTokenId), + // address(this) + // ); + // } + // } + + // function xtestDynamicTraitRedemptionViaSeaport() public { + // // Set the tokenId to be redeemed + // uint256 redemptionTokenId0 = 2; + + // // Set the tokenId to be burned to the tokenId to be redeemed + // uint256 redeemableTokenId0 = redemptionTokenId0; + + // // Deploy the mock ERC721 with dynamic traits + // // Allow the contract offerer to set traits + // MockERC721DynamicTraits dynamicTraitsToken = new MockERC721DynamicTraits( + // address(offerer) + // ); + + // // Mint a dynamicTraitsToken of tokenId redeemableTokenId0 to the test contract + // dynamicTraitsToken.mint(address(this), redeemableTokenId0); + + // // Approve the conduit to transfer the redeemableTokens on behalf of the test contract + // dynamicTraitsToken.setApprovalForAll(address(conduit), true); + + // // Create a single-item offer array with the redemptionToken the caller will receive + // OfferItem[] memory offer = new OfferItem[](1); + // offer[0] = OfferItem({ + // itemType: ItemType.ERC721_WITH_CRITERIA, + // token: address(redemptionToken), + // identifierOrCriteria: 0, + // startAmount: 1, + // endAmount: 1 + // }); + + // // Create an empty consideration array since the redeemable is a trait redemption + // ConsiderationItem[] memory consideration = new ConsiderationItem[](0); + + // // Create the CampaignParams with the offer and consideration from above. + // { + // CampaignParams memory params = CampaignParams({ + // offer: offer, + // consideration: consideration, + // signer: address(0), + // startTime: uint32(block.timestamp), + // endTime: uint32(block.timestamp + 1000), + // maxCampaignRedemptions: 5, + // manager: address(this) + // }); + + // // Call createCampaign on the offerer and pass in the CampaignParams + // offerer.createCampaign(params, ""); + // } + + // // uint256 campaignId = 1; + // // bytes32 redemptionHash = bytes32(0); + + // { + // // Create the offer we expect to be emitted in the event + // OfferItem[] memory offerFromEvent = new OfferItem[](2); + // offerFromEvent[0] = OfferItem({ + // itemType: ItemType.ERC721, + // token: address(redemptionToken), + // identifierOrCriteria: redemptionTokenId0, + // startAmount: 1, + // endAmount: 1 + // }); + + // // Check that the consideration passed into createCampaign has itemType ERC721_WITH_CRITERIA + // assertEq(uint256(consideration[0].itemType), 4); + + // TraitRedemption memory traitRedemption = TraitRedemption({ + // substandard: 0, // set value to traitValue + // token: address(dynamicTraitsToken), + // identifier: redeemableTokenId0, + // traitKey: "isRedeemed", + // traitValue: bytes32(abi.encode(1)), + // substandardValue: bytes32(abi.encode(0)) + // }); + + // // Create the extraData to be passed into fulfillAdvancedOrder + // bytes memory extraData = abi.encode(1, bytes32(0), traitRedemption); // campaignId, redemptionHash + + // // TODO: validate OrderFulfilled event + + // // Create the OrderParameters to be passed into fulfillAdvancedOrder + // OrderParameters memory parameters = OrderParametersLib + // .empty() + // .withOfferer(address(offerer)) + // .withOrderType(OrderType.CONTRACT) + // .withConsideration(consideration) + // .withOffer(offer) + // .withConduitKey(conduitKey) + // .withStartTime(block.timestamp) + // .withEndTime(block.timestamp + 1) + // .withTotalOriginalConsiderationItems(consideration.length); + + // // Create the AdvancedOrder to be passed into fulfillAdvancedOrder + // AdvancedOrder memory order = AdvancedOrder({ + // parameters: parameters, + // numerator: 1, + // denominator: 1, + // signature: "", + // extraData: extraData + // }); + + // // Call fulfillAdvancedOrder + // seaport.fulfillAdvancedOrder({ + // advancedOrder: order, + // criteriaResolvers: criteriaResolvers, + // fulfillerConduitKey: conduitKey, + // recipient: address(0) + // }); + + // // Check that the redeemableToken has been burned + // assertEq( + // dynamicTraitsToken.ownerOf(redeemableTokenId0), + // address(this) + // ); + + // // Check that the two redemptionTokens has been minted to the test contract + // assertEq( + // redemptionToken.ownerOf(redemptionTokenId0), + // address(this) + // ); + // } + // } + + // function xtestRedeemMultipleWithSeaport() public { + // uint256 tokenId; + // redeemableToken.setApprovalForAll(address(conduit), true); + + // AdvancedOrder[] memory orders = new AdvancedOrder[](5); + // OfferItem[] memory offer = new OfferItem[](1); + // ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + + // uint256 campaignId = 1; + // bytes32 redemptionHash = bytes32(0); + + // offer[0] = OfferItem({ + // itemType: ItemType.ERC721_WITH_CRITERIA, + // token: address(redemptionToken), + // identifierOrCriteria: 0, + // startAmount: 1, + // endAmount: 1 + // }); + + // consideration[0] = ConsiderationItem({ + // itemType: ItemType.ERC721_WITH_CRITERIA, + // token: address(redeemableToken), + // identifierOrCriteria: 0, + // startAmount: 1, + // endAmount: 1, + // recipient: payable(_BURN_ADDRESS) + // }); + + // OrderParameters memory parameters = OrderParametersLib + // .empty() + // .withOfferer(address(offerer)) + // .withOrderType(OrderType.CONTRACT) + // .withConsideration(consideration) + // .withOffer(offer) + // .withStartTime(block.timestamp) + // .withEndTime(block.timestamp + 1) + // .withTotalOriginalConsiderationItems(1); + + // for (uint256 i; i < 5; i++) { + // tokenId = i; + // redeemableToken.mint(address(this), tokenId); + + // bytes memory extraData = abi.encode(campaignId, redemptionHash); + // AdvancedOrder memory order = AdvancedOrder({ + // parameters: parameters, + // numerator: 1, + // denominator: 1, + // signature: "", + // extraData: extraData + // }); + + // orders[i] = order; + // } + + // CampaignParams memory params = CampaignParams({ + // offer: offer, + // consideration: consideration, + // signer: address(0), + // startTime: uint32(block.timestamp), + // endTime: uint32(block.timestamp + 1000), + // maxCampaignRedemptions: 5, + // manager: address(this) + // }); + + // offerer.createCampaign(params, ""); + + // OfferItem[] memory offerFromEvent = new OfferItem[](1); + // offerFromEvent[0] = OfferItem({ + // itemType: ItemType.ERC721_WITH_CRITERIA, + // token: address(redemptionToken), + // identifierOrCriteria: 0, + // startAmount: 1, + // endAmount: 1 + // }); + + // ConsiderationItem[] + // memory considerationFromEvent = new ConsiderationItem[](1); + // considerationFromEvent[0] = ConsiderationItem({ + // itemType: ItemType.ERC721_WITH_CRITERIA, + // token: address(redeemableToken), + // identifierOrCriteria: 0, + // startAmount: 1, + // endAmount: 1, + // recipient: payable(_BURN_ADDRESS) + // }); + + // ( + // FulfillmentComponent[][] memory offerFulfillmentComponents, + // FulfillmentComponent[][] memory considerationFulfillmentComponents + // ) = fulfill.getNaiveFulfillmentComponents(orders); + + // seaport.fulfillAvailableAdvancedOrders({ + // advancedOrders: orders, + // criteriaResolvers: criteriaResolvers, + // offerFulfillments: offerFulfillmentComponents, + // considerationFulfillments: considerationFulfillmentComponents, + // fulfillerConduitKey: conduitKey, + // recipient: address(0), + // maximumFulfilled: 10 + // }); + + // for (uint256 i; i < 5; i++) { + // tokenId = i; + // assertEq(redeemableToken.ownerOf(tokenId), _BURN_ADDRESS); + // assertEq(redemptionToken.ownerOf(tokenId), address(this)); + // } + // } +} diff --git a/lib/redeemables/test/RedeemViaSeaport-721.t.sol b/lib/redeemables/test/RedeemViaSeaport-721.t.sol new file mode 100644 index 0000000..43636e9 --- /dev/null +++ b/lib/redeemables/test/RedeemViaSeaport-721.t.sol @@ -0,0 +1,1173 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Solarray} from "solarray/Solarray.sol"; +import {BaseOrderTest} from "./utils/BaseOrderTest.sol"; +import {TestERC20} from "./utils/mocks/TestERC20.sol"; +import {TestERC721} from "./utils/mocks/TestERC721.sol"; +import { + OfferItem, + ConsiderationItem, + SpentItem, + AdvancedOrder, + OrderParameters, + CriteriaResolver, + FulfillmentComponent +} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {ItemType, OrderType, Side} from "seaport-sol/src/SeaportEnums.sol"; +import {MockERC721DynamicTraits} from "./utils/mocks/MockERC721DynamicTraits.sol"; +import {OfferItemLib, ConsiderationItemLib, OrderParametersLib} from "seaport-sol/src/SeaportSol.sol"; +import {RedeemableContractOfferer} from "../src/RedeemableContractOfferer.sol"; +import {CampaignParams, TraitRedemption} from "../src/lib/RedeemableStructs.sol"; +import {RedeemableErrorsAndEvents} from "../src/lib/RedeemableErrorsAndEvents.sol"; +import {ERC721RedemptionMintable} from "../src/lib/ERC721RedemptionMintable.sol"; +import {ERC721RedemptionMintableWithCounter} from "../src/lib/ERC721RedemptionMintableWithCounter.sol"; +import {Merkle} from "../lib/murky/src/Merkle.sol"; + +contract RedeemViaSeaport721 is BaseOrderTest, RedeemableErrorsAndEvents { + using OrderParametersLib for OrderParameters; + + error InvalidContractOrder(bytes32 orderHash); + + RedeemableContractOfferer offerer; + TestERC721 redeemableToken; + ERC721RedemptionMintable redemptionToken; + CriteriaResolver[] criteriaResolvers; + Merkle merkle = new Merkle(); + + address constant _BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD; + + function setUp() public override { + super.setUp(); + offerer = new RedeemableContractOfferer( + address(conduit), + conduitKey, + address(seaport) + ); + redeemableToken = new TestERC721(); + redemptionToken = new ERC721RedemptionMintable( + address(offerer), + address(redeemableToken) + ); + vm.label(address(redeemableToken), "redeemableToken"); + vm.label(address(redemptionToken), "redemptionToken"); + } + + function testRedeemWithSeaport() public { + uint256 tokenId = 2; + redeemableToken.mint(address(this), tokenId); + redeemableToken.setApprovalForAll(address(conduit), true); + + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + { + CampaignParams memory params = CampaignParams({ + offer: offer, + consideration: consideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + offerer.createCampaign(params, ""); + } + + // uint256 campaignId = 1; + // bytes32 redemptionHash = bytes32(0); + + { + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(redemptionToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1 + }); + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemableToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + assertGt(uint256(consideration[0].itemType), uint256(considerationFromEvent[0].itemType)); + + bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash + consideration[0].identifierOrCriteria = tokenId; + + // TODO: validate OrderFulfilled event + OrderParameters memory parameters = OrderParametersLib.empty().withOfferer(address(offerer)).withOrderType( + OrderType.CONTRACT + ).withConsideration(considerationFromEvent).withOffer(offer).withConduitKey(conduitKey).withStartTime( + block.timestamp + ).withEndTime(block.timestamp + 1).withTotalOriginalConsiderationItems(consideration.length); + AdvancedOrder memory order = AdvancedOrder({ + parameters: parameters, + numerator: 1, + denominator: 1, + signature: "", + extraData: extraData + }); + + // { + // bytes + // vm.expectEmit(true, true, true, true); + // emit OrderFulfilled(); + // } + + seaport.fulfillAdvancedOrder({ + advancedOrder: order, + criteriaResolvers: criteriaResolvers, + fulfillerConduitKey: conduitKey, + recipient: address(0) + }); + + assertEq(redeemableToken.ownerOf(tokenId), _BURN_ADDRESS); + assertEq(redemptionToken.ownerOf(tokenId), address(this)); + } + } + + // TODO: write test with ETH redemption consideration + // TODO: 1155 tests with same tokenId (amount > 1), different tokenIds + // TODO: update erc20 amount to use decimals + + function testRedeemAndSendErc20ToThirdAddressViaSeaport() public { + uint256 tokenId = 2; + redeemableToken.mint(address(this), tokenId); + redeemableToken.setApprovalForAll(address(conduit), true); + + // Deploy the ERC20 + TestERC20 erc20 = new TestERC20(); + uint256 erc20Amount = 10; + + // Mint 100 tokens to the test contract + erc20.mint(address(this), 100); + + // Approve the conduit to spend tokens + erc20.approve(address(conduit), type(uint256).max); + + OfferItem[] memory campaignOffer = new OfferItem[](1); + campaignOffer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + ConsiderationItem[] memory campaignConsideration = new ConsiderationItem[](2); + campaignConsideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + campaignConsideration[1] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20), + identifierOrCriteria: 0, + startAmount: erc20Amount, + endAmount: erc20Amount, + recipient: payable(eve.addr) + }); + + { + CampaignParams memory params = CampaignParams({ + offer: campaignOffer, + consideration: campaignConsideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + offerer.createCampaign(params, ""); + } + + // uint256 campaignId = 1; + // bytes32 redemptionHash = bytes32(0); + + { + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(redemptionToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1 + }); + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemableToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + assertGt(uint256(campaignConsideration[0].itemType), uint256(considerationFromEvent[0].itemType)); + + bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash + + campaignConsideration[0].itemType = ItemType.ERC721; + campaignConsideration[0].identifierOrCriteria = tokenId; + + // TODO: validate OrderFulfilled event + OrderParameters memory parameters = OrderParametersLib.empty().withOfferer(address(offerer)).withOrderType( + OrderType.CONTRACT + ).withConsideration(campaignConsideration).withOffer(campaignOffer).withConduitKey(conduitKey).withStartTime( + block.timestamp + ).withEndTime(block.timestamp + 1).withTotalOriginalConsiderationItems(campaignConsideration.length); + AdvancedOrder memory order = AdvancedOrder({ + parameters: parameters, + numerator: 1, + denominator: 1, + signature: "", + extraData: extraData + }); + + uint256 erc20BalanceBefore = erc20.balanceOf(address(this)); + + seaport.fulfillAdvancedOrder({ + advancedOrder: order, + criteriaResolvers: criteriaResolvers, + fulfillerConduitKey: conduitKey, + recipient: address(0) + }); + + assertEq(redeemableToken.ownerOf(tokenId), _BURN_ADDRESS); + assertEq(redemptionToken.ownerOf(tokenId), address(this)); + assertEq(erc20BalanceBefore - erc20.balanceOf(address(this)), erc20Amount); + assertEq(erc20.balanceOf(eve.addr), erc20Amount); + } + } + + // TODO: add resolved tokenId to extradata + // TODO: fix redemptionToken being minted with merkle root + function testRedeemWithCriteriaResolversViaSeaport() public { + uint256 tokenId = 2; + redeemableToken.mint(address(this), tokenId); + redeemableToken.setApprovalForAll(address(conduit), true); + + ERC721RedemptionMintableWithCounter redemptionTokenWithCounter = new ERC721RedemptionMintableWithCounter( + address(offerer), + address(redeemableToken) + ); + + CriteriaResolver[] memory resolvers = new CriteriaResolver[](1); + + // Create an array of hashed identifiers (0-4) + // Only tokenIds 0-4 can be redeemed + bytes32[] memory hashedIdentifiers = new bytes32[](5); + for (uint256 i = 0; i < hashedIdentifiers.length; i++) { + hashedIdentifiers[i] = keccak256(abi.encode(i)); + } + bytes32 root = merkle.getRoot(hashedIdentifiers); + + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionTokenWithCounter), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + // Contract offerer will only consider tokenIds 0-4 + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: uint256(root), + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + { + CampaignParams memory params = CampaignParams({ + offer: offer, + consideration: consideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + offerer.createCampaign(params, ""); + } + + { + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(redemptionTokenWithCounter), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemableToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + assertGt(uint256(consideration[0].itemType), uint256(considerationFromEvent[0].itemType)); + + bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash + + OrderParameters memory parameters = OrderParametersLib.empty().withOfferer(address(offerer)).withOrderType( + OrderType.CONTRACT + ).withConsideration(consideration).withOffer(offer).withConduitKey(conduitKey).withStartTime( + block.timestamp + ).withEndTime(block.timestamp + 1).withTotalOriginalConsiderationItems(consideration.length); + AdvancedOrder memory order = AdvancedOrder({ + parameters: parameters, + numerator: 1, + denominator: 1, + signature: "", + extraData: extraData + }); + + resolvers[0] = CriteriaResolver({ + orderIndex: 0, + side: Side.CONSIDERATION, + index: 0, + identifier: tokenId, + criteriaProof: merkle.getProof(hashedIdentifiers, 2) + }); + + // TODO: validate OrderFulfilled event + // vm.expectEmit(true, true, true, true); + // emit OrderFulfilled(); + + seaport.fulfillAdvancedOrder({ + advancedOrder: order, + criteriaResolvers: resolvers, + fulfillerConduitKey: conduitKey, + recipient: address(0) + }); + + // TODO: failing because redemptionToken tokenId is merkle root + assertEq(redeemableToken.ownerOf(tokenId), _BURN_ADDRESS); + assertEq(redemptionTokenWithCounter.ownerOf(0), address(this)); + } + } + + // TODO: burn 1, send weth to third address, also redeem trait + // TODO: mock erc20 to third address or burn + // TODO: mock erc721 with tokenId counter + // TODO: make MockErc20RedemptionMintable with mintRedemption + // TODO: burn nft and send erc20 to third address, get nft and erc20 + // TODO: mintRedemption should return tokenIds array + // TODO: then add dynamic traits + // TODO: by EOW, have dynamic traits demo + + // notice: redemptionToken tokenId will be tokenId of first item in consideration + function testBurn2Redeem1ViaSeaport() public { + // Set the two tokenIds to be burned + uint256 burnTokenId0 = 2; + uint256 burnTokenId1 = 3; + + // Mint two redeemableTokens of tokenId burnTokenId0 and burnTokenId1 to the test contract + redeemableToken.mint(address(this), burnTokenId0); + redeemableToken.mint(address(this), burnTokenId1); + + // Approve the conduit to transfer the redeemableTokens on behalf of the test contract + redeemableToken.setApprovalForAll(address(conduit), true); + + // Create a single-item OfferItem array with the redemption token the caller will receive + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + // Create a single-item ConsiderationItem array and require the caller to burn two redeemableTokens (of any tokenId) + ConsiderationItem[] memory consideration = new ConsiderationItem[](2); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + consideration[1] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + // Create the CampaignParams with the offer and consideration from above. + { + CampaignParams memory params = CampaignParams({ + offer: offer, + consideration: consideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + // Call createCampaign on the offerer and pass in the CampaignParams + offerer.createCampaign(params, ""); + } + + // uint256 campaignId = 1; + // bytes32 redemptionHash = bytes32(0); + + { + // Create the offer we expect to be emitted in the event + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(redemptionToken), + identifierOrCriteria: burnTokenId0, + startAmount: 1, + endAmount: 1 + }); + + // Create the consideration we expect to be emitted in the event + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](2); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemableToken), + identifierOrCriteria: burnTokenId0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + considerationFromEvent[1] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemableToken), + identifierOrCriteria: burnTokenId1, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + // Check that the consideration passed into createCampaign has itemType ERC721_WITH_CRITERIA + assertEq(uint256(consideration[0].itemType), 4); + + // Check that the consideration emitted in the event has itemType ERC721 + assertEq(uint256(considerationFromEvent[0].itemType), 2); + assertEq(uint256(considerationFromEvent[1].itemType), 2); + + // Create the extraData to be passed into fulfillAdvancedOrder + bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash + + // TODO: validate OrderFulfilled event + + // Create the OrderParameters to be passed into fulfillAdvancedOrder + OrderParameters memory parameters = OrderParametersLib.empty().withOfferer(address(offerer)).withOrderType( + OrderType.CONTRACT + ).withConsideration(considerationFromEvent).withOffer(offer).withConduitKey(conduitKey).withStartTime( + block.timestamp + ).withEndTime(block.timestamp + 1).withTotalOriginalConsiderationItems(considerationFromEvent.length); + + // Create the AdvancedOrder to be passed into fulfillAdvancedOrder + AdvancedOrder memory order = AdvancedOrder({ + parameters: parameters, + numerator: 1, + denominator: 1, + signature: "", + extraData: extraData + }); + + // Call fulfillAdvancedOrder + seaport.fulfillAdvancedOrder({ + advancedOrder: order, + criteriaResolvers: criteriaResolvers, + fulfillerConduitKey: conduitKey, + recipient: address(0) + }); + + // Check that the two redeemable tokens have been burned + assertEq(redeemableToken.ownerOf(burnTokenId0), _BURN_ADDRESS); + assertEq(redeemableToken.ownerOf(burnTokenId1), _BURN_ADDRESS); + + // Check that the redemption token has been minted to the test contract + assertEq(redemptionToken.ownerOf(burnTokenId0), address(this)); + } + } + + function testBurn1Redeem2WithSeaport() public { + // Set the two tokenIds to be redeemed + uint256 redemptionTokenId0 = 0; + uint256 redemptionTokenId1 = 1; + + // Set the tokenId to be burned to the first tokenId to be redeemed + uint256 redeemableTokenId0 = redemptionTokenId0; + + ERC721RedemptionMintableWithCounter redemptionTokenWithCounter = new ERC721RedemptionMintableWithCounter( + address(offerer), + address(redeemableToken) + ); + + // Mint a redeemableToken of tokenId redeemableTokenId0 to the test contract + redeemableToken.mint(address(this), redeemableTokenId0); + + // Approve the conduit to transfer the redeemableTokens on behalf of the test contract + redeemableToken.setApprovalForAll(address(conduit), true); + + // Create a two-item OfferItem array with the 2 redemptionTokens the caller will receive + OfferItem[] memory offer = new OfferItem[](2); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionTokenWithCounter), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + offer[1] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionTokenWithCounter), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + // Create a single-item ConsiderationItem array with the redeemableToken the caller will burn + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + // Create the CampaignParams with the offer and consideration from above. + { + CampaignParams memory params = CampaignParams({ + offer: offer, + consideration: consideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + // Call createCampaign on the offerer and pass in the CampaignParams + offerer.createCampaign(params, ""); + } + + // uint256 campaignId = 1; + // bytes32 redemptionHash = bytes32(0); + + { + // Create the offer we expect to be emitted in the event + OfferItem[] memory offerFromEvent = new OfferItem[](2); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(redemptionTokenWithCounter), + identifierOrCriteria: redemptionTokenId0, + startAmount: 1, + endAmount: 1 + }); + + offerFromEvent[1] = OfferItem({ + itemType: ItemType.ERC721, + token: address(redemptionTokenWithCounter), + identifierOrCriteria: redemptionTokenId1, + startAmount: 1, + endAmount: 1 + }); + + // Create the consideration we expect to be emitted in the event + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemableToken), + identifierOrCriteria: redeemableTokenId0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + // Check that the consideration passed into createCampaign has itemType ERC721_WITH_CRITERIA + assertEq(uint256(consideration[0].itemType), 4); + + // Check that the consideration emitted in the event has itemType ERC721 + assertEq(uint256(considerationFromEvent[0].itemType), 2); + + // Create the extraData to be passed into fulfillAdvancedOrder + bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash + + // TODO: validate OrderFulfilled event + + // Create the OrderParameters to be passed into fulfillAdvancedOrder + OrderParameters memory parameters = OrderParametersLib.empty().withOfferer(address(offerer)).withOrderType( + OrderType.CONTRACT + ).withConsideration(considerationFromEvent).withOffer(offer).withConduitKey(conduitKey).withStartTime( + block.timestamp + ).withEndTime(block.timestamp + 1).withTotalOriginalConsiderationItems(considerationFromEvent.length); + + // Create the AdvancedOrder to be passed into fulfillAdvancedOrder + AdvancedOrder memory order = AdvancedOrder({ + parameters: parameters, + numerator: 1, + denominator: 1, + signature: "", + extraData: extraData + }); + + // Call fulfillAdvancedOrder + seaport.fulfillAdvancedOrder({ + advancedOrder: order, + criteriaResolvers: criteriaResolvers, + fulfillerConduitKey: conduitKey, + recipient: address(0) + }); + + // Check that the redeemableToken has been burned + assertEq(redeemableToken.ownerOf(redeemableTokenId0), _BURN_ADDRESS); + + // Check that the two redemptionTokens has been minted to the test contract + assertEq(redemptionTokenWithCounter.ownerOf(redemptionTokenId0), address(this)); + assertEq(redemptionTokenWithCounter.ownerOf(redemptionTokenId1), address(this)); + } + } + + function testBurn2SeparateRedeemableTokensRedeem1ViaSeaport() public { + // Set the tokenId to be burned + uint256 burnTokenId0 = 2; + + // Create the second redeemableToken to be burned + TestERC721 redeemableTokenTwo = new TestERC721(); + + // Mint one redeemableToken ane one redeemableTokenTwo of tokenId burnTokenId0 to the test contract + redeemableToken.mint(address(this), burnTokenId0); + redeemableTokenTwo.mint(address(this), burnTokenId0); + + // Approve the conduit to transfer the redeemableTokens on behalf of the test contract + redeemableToken.setApprovalForAll(address(conduit), true); + redeemableTokenTwo.setApprovalForAll(address(conduit), true); + + // Create a single-item OfferItem array with the redemption token the caller will receive + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + // Create a two-item ConsiderationItem array and require the caller to burn one redeemableToken and one redeemableTokenTwo (of any tokenId) + ConsiderationItem[] memory consideration = new ConsiderationItem[](2); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + consideration[1] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemableTokenTwo), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + // Create the CampaignParams with the offer and consideration from above. + { + CampaignParams memory params = CampaignParams({ + offer: offer, + consideration: consideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + // Call createCampaign on the offerer and pass in the CampaignParams + offerer.createCampaign(params, ""); + } + + // uint256 campaignId = 1; + // bytes32 redemptionHash = bytes32(0); + + { + // Create the offer we expect to be emitted in the event + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(redemptionToken), + identifierOrCriteria: burnTokenId0, + startAmount: 1, + endAmount: 1 + }); + + // Create the consideration we expect to be emitted in the event + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](2); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemableToken), + identifierOrCriteria: burnTokenId0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + considerationFromEvent[1] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemableTokenTwo), + identifierOrCriteria: burnTokenId0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + // Check that the consideration passed into createCampaign has itemType ERC721_WITH_CRITERIA + assertEq(uint256(consideration[0].itemType), 4); + + // Check that the consideration emitted in the event has itemType ERC721 + assertEq(uint256(considerationFromEvent[0].itemType), 2); + assertEq(uint256(considerationFromEvent[1].itemType), 2); + + // Create the extraData to be passed into fulfillAdvancedOrder + bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash + + // TODO: validate OrderFulfilled event + + // Create the OrderParameters to be passed into fulfillAdvancedOrder + OrderParameters memory parameters = OrderParametersLib.empty().withOfferer(address(offerer)).withOrderType( + OrderType.CONTRACT + ).withConsideration(considerationFromEvent).withOffer(offer).withConduitKey(conduitKey).withStartTime( + block.timestamp + ).withEndTime(block.timestamp + 1).withTotalOriginalConsiderationItems(considerationFromEvent.length); + + // Create the AdvancedOrder to be passed into fulfillAdvancedOrder + AdvancedOrder memory order = AdvancedOrder({ + parameters: parameters, + numerator: 1, + denominator: 1, + signature: "", + extraData: extraData + }); + + // Call fulfillAdvancedOrder + seaport.fulfillAdvancedOrder({ + advancedOrder: order, + criteriaResolvers: criteriaResolvers, + fulfillerConduitKey: conduitKey, + recipient: address(0) + }); + + // Check that one redeemableToken and one redeemableTokenTwo have been burned + assertEq(redeemableToken.ownerOf(burnTokenId0), _BURN_ADDRESS); + assertEq(redeemableTokenTwo.ownerOf(burnTokenId0), _BURN_ADDRESS); + + // Check that the redemption token has been minted to the test contract + assertEq(redemptionToken.ownerOf(burnTokenId0), address(this)); + } + } + + // TODO: add multi-redeem file + + function testBurn1Redeem2SeparateRedemptionTokensWithSeaport() public { + // Set the tokenId to be redeemed + uint256 redemptionTokenId = 2; + + // Set the tokenId to be burned to the first tokenId to be redeemed + uint256 redeemableTokenId = redemptionTokenId; + + // Create a new ERC721RedemptionMintable redemptionTokenTwo + ERC721RedemptionMintable redemptionTokenTwo = new ERC721RedemptionMintable( + address(offerer), + address(redeemableToken) + ); + + // Mint a redeemableToken of tokenId redeemableTokenId to the test contract + redeemableToken.mint(address(this), redeemableTokenId); + + // Approve the conduit to transfer the redeemableTokens on behalf of the test contract + redeemableToken.setApprovalForAll(address(conduit), true); + + // Create a two-item OfferItem array with the one redemptionToken and one redemptionTokenTwo the caller will receive + OfferItem[] memory offer = new OfferItem[](2); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + offer[1] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionTokenTwo), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + // Create a single-item ConsiderationItem array with the redeemableToken the caller will burn + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + // Create the CampaignParams with the offer and consideration from above. + { + CampaignParams memory params = CampaignParams({ + offer: offer, + consideration: consideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + // Call createCampaign on the offerer and pass in the CampaignParams + offerer.createCampaign(params, ""); + } + + // uint256 campaignId = 1; + // bytes32 redemptionHash = bytes32(0); + + { + // Create the offer we expect to be emitted in the event + OfferItem[] memory offerFromEvent = new OfferItem[](2); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(redemptionToken), + identifierOrCriteria: redemptionTokenId, + startAmount: 1, + endAmount: 1 + }); + + offerFromEvent[1] = OfferItem({ + itemType: ItemType.ERC721, + token: address(redemptionToken), + identifierOrCriteria: redemptionTokenId, + startAmount: 1, + endAmount: 1 + }); + + // Create the consideration we expect to be emitted in the event + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemableToken), + identifierOrCriteria: redeemableTokenId, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + // Check that the consideration passed into createCampaign has itemType ERC721_WITH_CRITERIA + assertEq(uint256(consideration[0].itemType), 4); + + // Check that the consideration emitted in the event has itemType ERC721 + assertEq(uint256(considerationFromEvent[0].itemType), 2); + + // Create the extraData to be passed into fulfillAdvancedOrder + bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash + + // TODO: validate OrderFulfilled event + + // Create the OrderParameters to be passed into fulfillAdvancedOrder + OrderParameters memory parameters = OrderParametersLib.empty().withOfferer(address(offerer)).withOrderType( + OrderType.CONTRACT + ).withConsideration(considerationFromEvent).withOffer(offer).withConduitKey(conduitKey).withStartTime( + block.timestamp + ).withEndTime(block.timestamp + 1).withTotalOriginalConsiderationItems(considerationFromEvent.length); + + // Create the AdvancedOrder to be passed into fulfillAdvancedOrder + AdvancedOrder memory order = AdvancedOrder({ + parameters: parameters, + numerator: 1, + denominator: 1, + signature: "", + extraData: extraData + }); + + // Call fulfillAdvancedOrder + seaport.fulfillAdvancedOrder({ + advancedOrder: order, + criteriaResolvers: criteriaResolvers, + fulfillerConduitKey: conduitKey, + recipient: address(0) + }); + + // Check that the redeemableToken has been burned + assertEq(redeemableToken.ownerOf(redeemableTokenId), _BURN_ADDRESS); + + // Check that the two redemptionTokens has been minted to the test contract + assertEq(redemptionToken.ownerOf(redemptionTokenId), address(this)); + assertEq(redemptionTokenTwo.ownerOf(redemptionTokenId), address(this)); + } + } + + function xtestDynamicTraitRedemptionViaSeaport() public { + // Set the tokenId to be redeemed + uint256 redemptionTokenId0 = 2; + + // Set the tokenId to be burned to the tokenId to be redeemed + uint256 redeemableTokenId0 = redemptionTokenId0; + + // Deploy the mock ERC721 with dynamic traits + // Allow the contract offerer to set traits + MockERC721DynamicTraits dynamicTraitsToken = new MockERC721DynamicTraits( + address(offerer) + ); + + // Mint a dynamicTraitsToken of tokenId redeemableTokenId0 to the test contract + dynamicTraitsToken.mint(address(this), redeemableTokenId0); + + // Approve the conduit to transfer the redeemableTokens on behalf of the test contract + dynamicTraitsToken.setApprovalForAll(address(conduit), true); + + // Create a single-item offer array with the redemptionToken the caller will receive + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + // Create an empty consideration array since the redeemable is a trait redemption + ConsiderationItem[] memory consideration = new ConsiderationItem[](0); + + // Create the CampaignParams with the offer and consideration from above. + { + CampaignParams memory params = CampaignParams({ + offer: offer, + consideration: consideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + // Call createCampaign on the offerer and pass in the CampaignParams + offerer.createCampaign(params, ""); + } + + // uint256 campaignId = 1; + // bytes32 redemptionHash = bytes32(0); + + { + // Create the offer we expect to be emitted in the event + OfferItem[] memory offerFromEvent = new OfferItem[](2); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(redemptionToken), + identifierOrCriteria: redemptionTokenId0, + startAmount: 1, + endAmount: 1 + }); + + // Check that the consideration passed into createCampaign has itemType ERC721_WITH_CRITERIA + assertEq(uint256(consideration[0].itemType), 4); + + TraitRedemption memory traitRedemption = TraitRedemption({ + substandard: 0, // set value to traitValue + token: address(dynamicTraitsToken), + identifier: redeemableTokenId0, + traitKey: "isRedeemed", + traitValue: bytes32(abi.encode(1)), + substandardValue: bytes32(abi.encode(0)) + }); + + // Create the extraData to be passed into fulfillAdvancedOrder + bytes memory extraData = abi.encode(1, bytes32(0), traitRedemption); // campaignId, redemptionHash + + // TODO: validate OrderFulfilled event + + // Create the OrderParameters to be passed into fulfillAdvancedOrder + OrderParameters memory parameters = OrderParametersLib.empty().withOfferer(address(offerer)).withOrderType( + OrderType.CONTRACT + ).withConsideration(consideration).withOffer(offer).withConduitKey(conduitKey).withStartTime( + block.timestamp + ).withEndTime(block.timestamp + 1).withTotalOriginalConsiderationItems(consideration.length); + + // Create the AdvancedOrder to be passed into fulfillAdvancedOrder + AdvancedOrder memory order = AdvancedOrder({ + parameters: parameters, + numerator: 1, + denominator: 1, + signature: "", + extraData: extraData + }); + + // Call fulfillAdvancedOrder + seaport.fulfillAdvancedOrder({ + advancedOrder: order, + criteriaResolvers: criteriaResolvers, + fulfillerConduitKey: conduitKey, + recipient: address(0) + }); + + // Check that the redeemableToken has been burned + assertEq(dynamicTraitsToken.ownerOf(redeemableTokenId0), address(this)); + + // Check that the two redemptionTokens has been minted to the test contract + assertEq(redemptionToken.ownerOf(redemptionTokenId0), address(this)); + } + } + + function xtestRedeemMultipleWithSeaport() public { + uint256 tokenId; + redeemableToken.setApprovalForAll(address(conduit), true); + + AdvancedOrder[] memory orders = new AdvancedOrder[](5); + OfferItem[] memory offer = new OfferItem[](1); + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + + uint256 campaignId = 1; + bytes32 redemptionHash = bytes32(0); + + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + OrderParameters memory parameters = OrderParametersLib.empty().withOfferer(address(offerer)).withOrderType( + OrderType.CONTRACT + ).withConsideration(consideration).withOffer(offer).withStartTime(block.timestamp).withEndTime( + block.timestamp + 1 + ).withTotalOriginalConsiderationItems(1); + + for (uint256 i; i < 5; i++) { + tokenId = i; + redeemableToken.mint(address(this), tokenId); + + bytes memory extraData = abi.encode(campaignId, redemptionHash); + AdvancedOrder memory order = AdvancedOrder({ + parameters: parameters, + numerator: 1, + denominator: 1, + signature: "", + extraData: extraData + }); + + orders[i] = order; + } + + CampaignParams memory params = CampaignParams({ + offer: offer, + consideration: consideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + offerer.createCampaign(params, ""); + + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + ( + FulfillmentComponent[][] memory offerFulfillmentComponents, + FulfillmentComponent[][] memory considerationFulfillmentComponents + ) = fulfill.getNaiveFulfillmentComponents(orders); + + seaport.fulfillAvailableAdvancedOrders({ + advancedOrders: orders, + criteriaResolvers: criteriaResolvers, + offerFulfillments: offerFulfillmentComponents, + considerationFulfillments: considerationFulfillmentComponents, + fulfillerConduitKey: conduitKey, + recipient: address(0), + maximumFulfilled: 10 + }); + + for (uint256 i; i < 5; i++) { + tokenId = i; + assertEq(redeemableToken.ownerOf(tokenId), _BURN_ADDRESS); + assertEq(redemptionToken.ownerOf(tokenId), address(this)); + } + } +} diff --git a/lib/redeemables/test/RedeemViaTransfer.t.sol b/lib/redeemables/test/RedeemViaTransfer.t.sol new file mode 100644 index 0000000..9155f66 --- /dev/null +++ b/lib/redeemables/test/RedeemViaTransfer.t.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Solarray} from "solarray/Solarray.sol"; +import {BaseOrderTest} from "./utils/BaseOrderTest.sol"; +import {TestERC20} from "./utils/mocks/TestERC20.sol"; +import {TestERC721} from "./utils/mocks/TestERC721.sol"; +import { + OfferItem, + ConsiderationItem, + SpentItem, + AdvancedOrder, + OrderParameters, + CriteriaResolver, + FulfillmentComponent +} from "seaport-types/src/lib/ConsiderationStructs.sol"; +// import {CriteriaResolutionErrors} from "seaport-types/src/interfaces/CriteriaResolutionErrors.sol"; +import {ItemType, OrderType, Side} from "seaport-sol/src/SeaportEnums.sol"; +import {OfferItemLib, ConsiderationItemLib, OrderParametersLib} from "seaport-sol/src/SeaportSol.sol"; +import {RedeemableContractOfferer} from "../src/RedeemableContractOfferer.sol"; +import {CampaignParams} from "../src/lib/RedeemableStructs.sol"; +import {RedeemableErrorsAndEvents} from "../src/lib/RedeemableErrorsAndEvents.sol"; +import {ERC721RedemptionMintable} from "../src/lib/ERC721RedemptionMintable.sol"; +import {Merkle} from "../lib/murky/src/Merkle.sol"; + +contract TestRedeemableContractOfferer is BaseOrderTest, RedeemableErrorsAndEvents { + using OrderParametersLib for OrderParameters; + + error InvalidContractOrder(bytes32 orderHash); + + RedeemableContractOfferer offerer; + TestERC721 redeemableToken; + ERC721RedemptionMintable redemptionToken; + CriteriaResolver[] criteriaResolvers; + Merkle merkle = new Merkle(); + + address constant _BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD; + + function setUp() public override { + super.setUp(); + offerer = new RedeemableContractOfferer( + address(conduit), + conduitKey, + address(seaport) + ); + redeemableToken = new TestERC721(); + redemptionToken = new ERC721RedemptionMintable( + address(offerer), + address(redeemableToken) + ); + vm.label(address(redeemableToken), "redeemableToken"); + vm.label(address(redemptionToken), "redemptionToken"); + } + + function testRedeemWith721SafeTransferFrom() public { + uint256 tokenId = 2; + redeemableToken.mint(address(this), tokenId); + + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + CampaignParams memory params = CampaignParams({ + offer: offer, + consideration: consideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + offerer.createCampaign(params, ""); + + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(redemptionToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1 + }); + + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemableToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + uint256 campaignId = 1; + bytes32 redemptionHash = bytes32(0); + bytes memory extraData = abi.encode(campaignId, redemptionHash); + + // TODO: validate OrderFulfilled event + bytes memory data = abi.encode(campaignId, redemptionHash); + redeemableToken.safeTransferFrom(address(this), address(offerer), tokenId, extraData); + + assertEq(redeemableToken.ownerOf(tokenId), _BURN_ADDRESS); + assertEq(redemptionToken.ownerOf(tokenId), address(this)); + } +} diff --git a/lib/redeemables/test/Revert.t.sol b/lib/redeemables/test/Revert.t.sol new file mode 100644 index 0000000..60a736a --- /dev/null +++ b/lib/redeemables/test/Revert.t.sol @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Solarray} from "solarray/Solarray.sol"; +import {BaseOrderTest} from "./utils/BaseOrderTest.sol"; +import {TestERC20} from "./utils/mocks/TestERC20.sol"; +import {TestERC721} from "./utils/mocks/TestERC721.sol"; +import { + OfferItem, + ConsiderationItem, + SpentItem, + AdvancedOrder, + OrderParameters, + CriteriaResolver, + FulfillmentComponent +} from "seaport-types/src/lib/ConsiderationStructs.sol"; +// import {CriteriaResolutionErrors} from "seaport-types/src/interfaces/CriteriaResolutionErrors.sol"; +import {ItemType, OrderType, Side} from "seaport-sol/src/SeaportEnums.sol"; +import {OfferItemLib, ConsiderationItemLib, OrderParametersLib} from "seaport-sol/src/SeaportSol.sol"; +import {RedeemableContractOfferer} from "../src/RedeemableContractOfferer.sol"; +import {CampaignParams} from "../src/lib/RedeemableStructs.sol"; +import {RedeemableErrorsAndEvents} from "../src/lib/RedeemableErrorsAndEvents.sol"; +import {ERC721RedemptionMintable} from "../src/lib/ERC721RedemptionMintable.sol"; +import {Merkle} from "../lib/murky/src/Merkle.sol"; + +contract TestRedeemableContractOfferer is BaseOrderTest, RedeemableErrorsAndEvents { + using OrderParametersLib for OrderParameters; + + error InvalidContractOrder(bytes32 orderHash); + + RedeemableContractOfferer offerer; + TestERC721 redeemableToken; + ERC721RedemptionMintable redemptionToken; + CriteriaResolver[] criteriaResolvers; + Merkle merkle = new Merkle(); + + address constant _BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD; + + function setUp() public override { + super.setUp(); + offerer = new RedeemableContractOfferer( + address(conduit), + conduitKey, + address(seaport) + ); + redeemableToken = new TestERC721(); + redemptionToken = new ERC721RedemptionMintable( + address(offerer), + address(redeemableToken) + ); + vm.label(address(redeemableToken), "redeemableToken"); + vm.label(address(redemptionToken), "redemptionToken"); + } + + function testRevertRedeemWithCriteriaResolversViaSeaport() public { + uint256 tokenId = 7; + redeemableToken.mint(address(this), tokenId); + redeemableToken.setApprovalForAll(address(conduit), true); + + CriteriaResolver[] memory resolvers = new CriteriaResolver[](1); + + // Create an array of hashed identifiers (0-4) + // Get the merkle root of the hashed identifiers to pass into updateCampaign + // Only tokenIds 0-4 can be redeemed + bytes32[] memory hashedIdentifiers = new bytes32[](5); + for (uint256 i = 0; i < hashedIdentifiers.length; i++) { + hashedIdentifiers[i] = keccak256(abi.encode(i)); + } + bytes32 root = merkle.getRoot(hashedIdentifiers); + + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + // Contract offerer will only consider tokenIds 0-4 + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: uint256(root), + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + { + CampaignParams memory params = CampaignParams({ + offer: offer, + consideration: consideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + offerer.createCampaign(params, ""); + } + + { + // Hash identifiers 5 - 9 and create invalid merkle root + // to pass into consideration + for (uint256 i = 0; i < hashedIdentifiers.length; i++) { + hashedIdentifiers[i] = keccak256(abi.encode(i + 5)); + } + root = merkle.getRoot(hashedIdentifiers); + consideration[0].identifierOrCriteria = uint256(root); + + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(redemptionToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1 + }); + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemableToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + assertGt(uint256(consideration[0].itemType), uint256(considerationFromEvent[0].itemType)); + + bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash + + OrderParameters memory parameters = OrderParametersLib.empty().withOfferer(address(offerer)).withOrderType( + OrderType.CONTRACT + ).withConsideration(consideration).withOffer(offer).withConduitKey(conduitKey).withStartTime( + block.timestamp + ).withEndTime(block.timestamp + 1).withTotalOriginalConsiderationItems(consideration.length); + AdvancedOrder memory order = AdvancedOrder({ + parameters: parameters, + numerator: 1, + denominator: 1, + signature: "", + extraData: extraData + }); + + resolvers[0] = CriteriaResolver({ + orderIndex: 0, + side: Side.CONSIDERATION, + index: 0, + identifier: tokenId, + criteriaProof: merkle.getProof(hashedIdentifiers, 2) + }); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidContractOrder.selector, + (uint256(uint160(address(offerer))) << 96) + seaport.getContractOffererNonce(address(offerer)) + ) + ); + seaport.fulfillAdvancedOrder({ + advancedOrder: order, + criteriaResolvers: resolvers, + fulfillerConduitKey: conduitKey, + recipient: address(0) + }); + } + } + + function testRevertmaxCampaignRedemptionsReached() public { + redeemableToken.mint(address(this), 0); + redeemableToken.mint(address(this), 1); + redeemableToken.mint(address(this), 2); + redeemableToken.setApprovalForAll(address(conduit), true); + + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + { + CampaignParams memory params = CampaignParams({ + offer: offer, + consideration: consideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 2, + manager: address(this) + }); + + offerer.createCampaign(params, ""); + } + + { + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemableToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(redemptionToken), + identifierOrCriteria: 1, + startAmount: 1, + endAmount: 1 + }); + + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemableToken), + identifierOrCriteria: 1, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + assertGt(uint256(consideration[0].itemType), uint256(considerationFromEvent[0].itemType)); + + bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash + + considerationFromEvent[0].identifierOrCriteria = 0; + + OrderParameters memory parameters = OrderParametersLib.empty().withOfferer(address(offerer)).withOrderType( + OrderType.CONTRACT + ).withConsideration(considerationFromEvent).withOffer(offer).withConduitKey(conduitKey).withStartTime( + block.timestamp + ).withEndTime(block.timestamp + 1).withTotalOriginalConsiderationItems(consideration.length); + AdvancedOrder memory order = AdvancedOrder({ + parameters: parameters, + numerator: 1, + denominator: 1, + signature: "", + extraData: extraData + }); + + seaport.fulfillAdvancedOrder({ + advancedOrder: order, + criteriaResolvers: criteriaResolvers, + fulfillerConduitKey: conduitKey, + recipient: address(0) + }); + + considerationFromEvent[0].identifierOrCriteria = 1; + + // vm.expectEmit(true, true, true, true); + // emit Or( + // address(this), + // campaignId, + // ConsiderationItemLib.toSpentItemArray(considerationFromEvent), + // OfferItemLib.toSpentItemArray(offerFromEvent), + // redemptionHash + // ); + + seaport.fulfillAdvancedOrder({ + advancedOrder: order, + criteriaResolvers: criteriaResolvers, + fulfillerConduitKey: conduitKey, + recipient: address(0) + }); + + considerationFromEvent[0].identifierOrCriteria = 2; + + // Should revert on the third redemption + // The call to Seaport should revert with maxCampaignRedemptionsReached(3, 2) + // vm.expectRevert( + // abi.encodeWithSelector( + // maxCampaignRedemptionsReached.selector, + // 3, + // 2 + // ) + // ); + vm.expectRevert( + abi.encodeWithSelector( + InvalidContractOrder.selector, + (uint256(uint160(address(offerer))) << 96) + seaport.getContractOffererNonce(address(offerer)) + ) + ); + seaport.fulfillAdvancedOrder({ + advancedOrder: order, + criteriaResolvers: criteriaResolvers, + fulfillerConduitKey: conduitKey, + recipient: address(0) + }); + + assertEq(redeemableToken.ownerOf(0), _BURN_ADDRESS); + assertEq(redeemableToken.ownerOf(1), _BURN_ADDRESS); + assertEq(redemptionToken.ownerOf(0), address(this)); + assertEq(redemptionToken.ownerOf(1), address(this)); + } + } + + function testRevertConsiderationItemRecipientCannotBeZeroAddress() public { + uint256 tokenId = 2; + redeemableToken.mint(address(this), tokenId); + redeemableToken.setApprovalForAll(address(conduit), true); + + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(address(0)) + }); + + { + CampaignParams memory params = CampaignParams({ + offer: offer, + consideration: consideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + vm.expectRevert(abi.encodeWithSelector(ConsiderationItemRecipientCannotBeZeroAddress.selector)); + offerer.createCampaign(params, ""); + } + } +} diff --git a/lib/redeemables/test/utils/ArithmeticUtil.sol b/lib/redeemables/test/utils/ArithmeticUtil.sol new file mode 100644 index 0000000..c8d4372 --- /dev/null +++ b/lib/redeemables/test/utils/ArithmeticUtil.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +library ArithmeticUtil { + ///@dev utility function to avoid overflows when multiplying fuzzed uints with widths <256 + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + return a * b; + } + + ///@dev utility function to avoid overflows when adding fuzzed uints with widths <256 + function add(uint256 a, uint256 b) internal pure returns (uint256) { + return a + b; + } + + ///@dev utility function to avoid overflows when subtracting fuzzed uints with widths <256 + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return a - b; + } + + ///@dev utility function to avoid overflows when dividing fuzzed uints with widths <256 + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return a / b; + } +} diff --git a/lib/redeemables/test/utils/BaseOrderTest.sol b/lib/redeemables/test/utils/BaseOrderTest.sol new file mode 100644 index 0000000..f038da8 --- /dev/null +++ b/lib/redeemables/test/utils/BaseOrderTest.sol @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {LibString} from "solady/src/utils/LibString.sol"; +import {FulfillAvailableHelper} from "seaport-sol/src/fulfillments/available/FulfillAvailableHelper.sol"; +import {MatchFulfillmentHelper} from "seaport-sol/src/fulfillments/match/MatchFulfillmentHelper.sol"; +import { + AdvancedOrderLib, + ConsiderationItemLib, + FulfillmentComponentLib, + FulfillmentLib, + OfferItemLib, + OrderComponentsLib, + OrderLib, + OrderParametersLib, + SeaportArrays +} from "seaport-sol/src/SeaportSol.sol"; +import { + AdvancedOrder, + ConsiderationItem, + Fulfillment, + FulfillmentComponent, + OfferItem, + Order, + OrderComponents, + OrderParameters +} from "seaport-sol/src/SeaportStructs.sol"; +import {ItemType, OrderType} from "seaport-sol/src/SeaportEnums.sol"; +import {SeaportInterface} from "seaport-sol/src/SeaportInterface.sol"; +import {AmountDeriver} from "seaport-core/src/lib/AmountDeriver.sol"; +import {BaseSeaportTest} from "./BaseSeaportTest.sol"; +import {ArithmeticUtil} from "./ArithmeticUtil.sol"; +import {ERC1155Recipient} from "./mocks/ERC1155Recipient.sol"; +import {ERC721Recipient} from "./mocks/ERC721Recipient.sol"; +import {TestERC20} from "./mocks/TestERC20.sol"; +import {TestERC721} from "./mocks/TestERC721.sol"; +import {TestERC1155} from "./mocks/TestERC1155.sol"; + +/** + * @dev This is a base test class for cases that depend on pre-deployed token + * contracts. Note that it is different from the BaseOrderTest in the + * legacy test suite. + */ +contract BaseOrderTest is BaseSeaportTest, AmountDeriver, ERC721Recipient, ERC1155Recipient { + using ArithmeticUtil for *; + + using AdvancedOrderLib for AdvancedOrder; + using AdvancedOrderLib for AdvancedOrder[]; + using ConsiderationItemLib for ConsiderationItem; + using ConsiderationItemLib for ConsiderationItem[]; + using FulfillmentComponentLib for FulfillmentComponent; + using FulfillmentComponentLib for FulfillmentComponent[]; + using FulfillmentLib for Fulfillment; + using FulfillmentLib for Fulfillment[]; + using OfferItemLib for OfferItem; + using OfferItemLib for OfferItem[]; + using OrderComponentsLib for OrderComponents; + using OrderLib for Order; + using OrderLib for Order[]; + using OrderParametersLib for OrderParameters; + + event Transfer(address indexed from, address indexed to, uint256 value); + event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); + + struct Context { + SeaportInterface seaport; + } + + // SeaportValidatorHelper validatorHelper; + // SeaportValidator validator; + FulfillAvailableHelper fulfill; + MatchFulfillmentHelper matcher; + + Account offerer1; + Account offerer2; + + Account dillon; + Account eve; + Account frank; + + TestERC20[] erc20s; + TestERC721[] erc721s; + TestERC1155[] erc1155s; + + // ExpectedBalances public balanceChecker; + + address[] preapprovals; + + string constant SINGLE_ERC721 = "single erc721"; + string constant STANDARD = "standard"; + string constant STANDARD_CONDUIT = "standard conduit"; + string constant FULL = "full"; + string constant FIRST_FIRST = "first first"; + string constant FIRST_SECOND = "first second"; + string constant SECOND_FIRST = "second first"; + string constant SECOND_SECOND = "second second"; + string constant FF_SF = "ff to sf"; + string constant SF_FF = "sf to ff"; + + function setUp() public virtual override { + super.setUp(); + + // balanceChecker = new ExpectedBalances(); + + // TODO: push to 24 if performance allows + // criteriaResolverHelper = new CriteriaResolverHelper(6); + + preapprovals = [address(seaport), address(conduit)]; + + _deployTestTokenContracts(); + + offerer1 = makeAndAllocateAccount("alice"); + offerer2 = makeAndAllocateAccount("bob"); + + dillon = makeAndAllocateAccount("dillon"); + eve = makeAndAllocateAccount("eve"); + frank = makeAndAllocateAccount("frank"); + + // allocate funds and tokens to test addresses + allocateTokensAndApprovals(address(this), type(uint128).max); + + _configureStructDefaults(); + + fulfill = new FulfillAvailableHelper(); + matcher = new MatchFulfillmentHelper(); + } + + /** + * @dev Creates a set of globally available default structs for use in + * tests. + */ + function _configureStructDefaults() internal { + OfferItemLib.empty().withItemType(ItemType.ERC721).withStartAmount(1).withEndAmount(1).saveDefault( + SINGLE_ERC721 + ); + ConsiderationItemLib.empty().withItemType(ItemType.ERC721).withStartAmount(1).withEndAmount(1).saveDefault( + SINGLE_ERC721 + ); + + OrderComponentsLib.empty().withOrderType(OrderType.FULL_OPEN).withStartTime(block.timestamp).withEndTime( + block.timestamp + 100 + ).saveDefault(STANDARD); + + OrderComponentsLib.fromDefault(STANDARD).withConduitKey(conduitKey).saveDefault(STANDARD_CONDUIT); + + AdvancedOrderLib.empty().withNumerator(1).withDenominator(1).saveDefault(FULL); + + FulfillmentComponentLib.empty().withOrderIndex(0).withItemIndex(0).saveDefault(FIRST_FIRST); + FulfillmentComponentLib.empty().withOrderIndex(0).withItemIndex(1).saveDefault(FIRST_SECOND); + FulfillmentComponentLib.empty().withOrderIndex(1).withItemIndex(0).saveDefault(SECOND_FIRST); + FulfillmentComponentLib.empty().withOrderIndex(1).withItemIndex(1).saveDefault(SECOND_SECOND); + + SeaportArrays.FulfillmentComponents(FulfillmentComponentLib.fromDefault(FIRST_FIRST)).saveDefaultMany( + FIRST_FIRST + ); + SeaportArrays.FulfillmentComponents(FulfillmentComponentLib.fromDefault(FIRST_SECOND)).saveDefaultMany( + FIRST_SECOND + ); + SeaportArrays.FulfillmentComponents(FulfillmentComponentLib.fromDefault(SECOND_FIRST)).saveDefaultMany( + SECOND_FIRST + ); + SeaportArrays.FulfillmentComponents(FulfillmentComponentLib.fromDefault(SECOND_SECOND)).saveDefaultMany( + SECOND_SECOND + ); + + FulfillmentLib.empty().withOfferComponents(FulfillmentComponentLib.fromDefaultMany(SECOND_FIRST)) + .withConsiderationComponents(FulfillmentComponentLib.fromDefaultMany(FIRST_FIRST)).saveDefault(SF_FF); + FulfillmentLib.empty().withOfferComponents(FulfillmentComponentLib.fromDefaultMany(FIRST_FIRST)) + .withConsiderationComponents(FulfillmentComponentLib.fromDefaultMany(SECOND_FIRST)).saveDefault(FF_SF); + } + + function test(function(Context memory) external fn, Context memory context) internal { + try fn(context) { + fail("Differential test should have reverted with failure status"); + } catch (bytes memory reason) { + assertPass(reason); + } + } + + /** + * @dev Wrapper for forge-std's makeAccount that has public visibility + * instead of internal visibility, so that we can access it in + * libraries. + */ + function makeAccountWrapper(string memory name) public returns (Account memory) { + return makeAccount(name); + } + + /** + * @dev Convenience wrapper for makeAddrAndKey that also allocates tokens, + * ether, and approvals. + */ + function makeAndAllocateAccount(string memory name) internal returns (Account memory) { + Account memory account = makeAccountWrapper(name); + allocateTokensAndApprovals(account.addr, type(uint128).max); + return account; + } + + /** + * @dev Sets up a new address and sets up token approvals for it. + */ + function makeAddrWithAllocationsAndApprovals(string memory label) internal returns (address) { + address addr = makeAddr(label); + allocateTokensAndApprovals(addr, type(uint128).max); + return addr; + } + + /** + * @dev Deploy test token contracts. + */ + function _deployTestTokenContracts() internal { + for (uint256 i; i < 3; i++) { + createErc20Token(); + createErc721Token(); + createErc1155Token(); + } + // preapproved721 = new PreapprovedERC721(preapprovals); + } + + /** + * @dev Creates a new ERC20 token contract and stores it in the erc20s + * array. + */ + function createErc20Token() internal returns (uint256 i) { + i = erc20s.length; + TestERC20 token = new TestERC20(); + erc20s.push(token); + vm.label(address(token), string.concat("ERC20", LibString.toString(i))); + } + + /** + * @dev Creates a new ERC721 token contract and stores it in the erc721s + * array. + */ + function createErc721Token() internal returns (uint256 i) { + i = erc721s.length; + TestERC721 token = new TestERC721(); + erc721s.push(token); + vm.label(address(token), string.concat("ERC721", LibString.toString(i))); + } + + /** + * @dev Creates a new ERC1155 token contract and stores it in the erc1155s + * array. + */ + function createErc1155Token() internal returns (uint256 i) { + i = erc1155s.length; + TestERC1155 token = new TestERC1155(); + erc1155s.push(token); + vm.label(address(token), string.concat("ERC1155", LibString.toString(i))); + } + + /** + * @dev Allocate amount of ether and each erc20 token; set approvals for all + * tokens. + */ + function allocateTokensAndApprovals(address _to, uint128 _amount) public { + vm.deal(_to, _amount); + for (uint256 i = 0; i < erc20s.length; ++i) { + erc20s[i].mint(_to, _amount); + } + _setApprovals(_to); + } + + /** + * @dev Set approvals for all tokens. + * + * @param _owner The address to set approvals for. + */ + function _setApprovals(address _owner) internal virtual { + vm.startPrank(_owner); + for (uint256 i = 0; i < erc20s.length; ++i) { + erc20s[i].approve(address(seaport), type(uint256).max); + erc20s[i].approve(address(conduit), type(uint256).max); + } + for (uint256 i = 0; i < erc721s.length; ++i) { + erc721s[i].setApprovalForAll(address(seaport), true); + erc721s[i].setApprovalForAll(address(conduit), true); + } + for (uint256 i = 0; i < erc1155s.length; ++i) { + erc1155s[i].setApprovalForAll(address(seaport), true); + erc1155s[i].setApprovalForAll(address(conduit), true); + } + + vm.stopPrank(); + } + + receive() external payable virtual {} +} diff --git a/lib/redeemables/test/utils/BaseSeaportTest.sol b/lib/redeemables/test/utils/BaseSeaportTest.sol new file mode 100644 index 0000000..5fe8313 --- /dev/null +++ b/lib/redeemables/test/utils/BaseSeaportTest.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {stdStorage, StdStorage} from "forge-std/Test.sol"; +import {DifferentialTest} from "./DifferentialTest.sol"; +import {ConduitControllerInterface} from "seaport-sol/src/ConduitControllerInterface.sol"; +import {ConduitController} from "seaport-core/src/conduit/ConduitController.sol"; +import {ConsiderationInterface} from "seaport-types/src/interfaces/ConsiderationInterface.sol"; +import {Consideration} from "seaport-core/src/lib/Consideration.sol"; +import {Conduit} from "seaport-core/src/conduit/Conduit.sol"; + +/// @dev Base test case that deploys Consideration and its dependencies. +contract BaseSeaportTest is DifferentialTest { + using stdStorage for StdStorage; + + bool coverage_or_debug; + bytes32 conduitKey; + + Conduit conduit; + Conduit referenceConduit; + ConduitControllerInterface conduitController; + ConsiderationInterface seaport; + + function stringEq(string memory a, string memory b) internal pure returns (bool) { + return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b)); + } + + function debugEnabled() internal returns (bool) { + return vm.envOr("SEAPORT_COVERAGE", false) || debugProfileEnabled(); + } + + function debugProfileEnabled() internal returns (bool) { + string memory env = vm.envOr("FOUNDRY_PROFILE", string("")); + return stringEq(env, "debug") || stringEq(env, "moat_debug"); + } + + function setUp() public virtual { + // Conditionally deploy contracts normally or from precompiled source + // deploys normally when SEAPORT_COVERAGE is true for coverage analysis + // or when FOUNDRY_PROFILE is "debug" for debugging with source maps + // deploys from precompiled source when both are false. + coverage_or_debug = debugEnabled(); + + conduitKey = bytes32(uint256(uint160(address(this))) << 96); + _deployAndConfigurePrecompiledOptimizedConsideration(); + + vm.label(address(conduitController), "conduitController"); + vm.label(address(seaport), "seaport"); + vm.label(address(conduit), "conduit"); + vm.label(address(this), "testContract"); + } + + /** + * @dev Get the configured preferred Seaport + */ + function getSeaport() internal view returns (ConsiderationInterface seaport_) { + seaport_ = seaport; + } + + /** + * @dev Get the configured preferred ConduitController + */ + function getConduitController() internal view returns (ConduitControllerInterface conduitController_) { + conduitController_ = conduitController; + } + + ///@dev deploy optimized consideration contracts from pre-compiled source + // (solc-0.8.19, IR pipeline enabled, unless running coverage or debug) + function _deployAndConfigurePrecompiledOptimizedConsideration() public { + conduitController = new ConduitController(); + seaport = new Consideration(address(conduitController)); + + //create conduit, update channel + conduit = Conduit(conduitController.createConduit(conduitKey, address(this))); + conduitController.updateChannel(address(conduit), address(seaport), true); + } + + function signOrder(ConsiderationInterface _consideration, uint256 _pkOfSigner, bytes32 _orderHash) + internal + view + returns (bytes memory) + { + (bytes32 r, bytes32 s, uint8 v) = getSignatureComponents(_consideration, _pkOfSigner, _orderHash); + return abi.encodePacked(r, s, v); + } + + function getSignatureComponents(ConsiderationInterface _consideration, uint256 _pkOfSigner, bytes32 _orderHash) + internal + view + returns (bytes32, bytes32, uint8) + { + (, bytes32 domainSeparator,) = _consideration.information(); + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(_pkOfSigner, keccak256(abi.encodePacked(bytes2(0x1901), domainSeparator, _orderHash))); + return (r, s, v); + } +} diff --git a/lib/redeemables/test/utils/DifferentialTest.sol b/lib/redeemables/test/utils/DifferentialTest.sol new file mode 100644 index 0000000..8de88cf --- /dev/null +++ b/lib/redeemables/test/utils/DifferentialTest.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Test} from "forge-std/Test.sol"; + +contract DifferentialTest is Test { + ///@dev error to supply + error RevertWithFailureStatus(bool status); + error DifferentialTestAssertionFailed(); + + // slot where HEVM stores a bool representing whether or not an assertion has failed + bytes32 HEVM_FAILED_SLOT = bytes32("failed"); + + // hash of the bytes surfaced by `revert RevertWithFailureStatus(false)` + bytes32 PASSING_HASH = keccak256(abi.encodeWithSelector(RevertWithFailureStatus.selector, false)); + + ///@dev reverts after function body with HEVM failure status, which clears all state changes + /// but still surfaces assertion failure status. + modifier stateless() { + _; + revert RevertWithFailureStatus(readHevmFailureSlot()); + } + + ///@dev revert if the supplied bytes do not match the expected "passing" revert bytes + function assertPass(bytes memory reason) internal view { + // hash the reason and compare to the hash of the passing revert bytes + if (keccak256(reason) != PASSING_HASH) { + revert DifferentialTestAssertionFailed(); + } + } + + ///@dev read the failure slot of the HEVM using the vm.load cheatcode + /// Returns true if there was an assertion failure. recorded. + function readHevmFailureSlot() internal view returns (bool) { + return vm.load(address(vm), HEVM_FAILED_SLOT) == bytes32(uint256(1)); + } +} diff --git a/lib/redeemables/test/utils/mocks/ERC1155Recipient.sol b/lib/redeemables/test/utils/mocks/ERC1155Recipient.sol new file mode 100644 index 0000000..593927a --- /dev/null +++ b/lib/redeemables/test/utils/mocks/ERC1155Recipient.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +import {IERC1155Receiver} from "../../../src/interfaces/IERC1155Receiver.sol"; + +contract ERC1155Recipient is IERC1155Receiver { + function onERC1155Received(address, address, uint256, uint256, bytes calldata) + public + virtual + override + returns (bytes4) + { + return IERC1155Receiver.onERC1155Received.selector; + } + + function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) + external + virtual + override + returns (bytes4) + { + return IERC1155Receiver.onERC1155BatchReceived.selector; + } +} diff --git a/lib/redeemables/test/utils/mocks/ERC721Recipient.sol b/lib/redeemables/test/utils/mocks/ERC721Recipient.sol new file mode 100644 index 0000000..363e61f --- /dev/null +++ b/lib/redeemables/test/utils/mocks/ERC721Recipient.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +import {IERC721Receiver} from "seaport-types/src/interfaces/IERC721Receiver.sol"; + +contract ERC721Recipient is IERC721Receiver { + function onERC721Received(address, address, uint256, bytes calldata) public virtual override returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } +} diff --git a/lib/redeemables/test/utils/mocks/MockERC1271Wallet.sol b/lib/redeemables/test/utils/mocks/MockERC1271Wallet.sol new file mode 100644 index 0000000..044b909 --- /dev/null +++ b/lib/redeemables/test/utils/mocks/MockERC1271Wallet.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "solady/src/utils/ECDSA.sol"; + +contract MockERC1271Wallet { + address signer; + + constructor(address signer_) { + signer = signer_; + } + + function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { + return ECDSA.recover(hash, signature) == signer ? bytes4(0x1626ba7e) : bytes4(0); + } +} diff --git a/lib/redeemables/test/utils/mocks/MockERC721DynamicTraits.sol b/lib/redeemables/test/utils/mocks/MockERC721DynamicTraits.sol new file mode 100644 index 0000000..ca96ebe --- /dev/null +++ b/lib/redeemables/test/utils/mocks/MockERC721DynamicTraits.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.19; + +import {ERC721} from "solady/src/tokens/ERC721.sol"; +import {IERC7XXX} from "../../../src/interfaces/IERC7XXX.sol"; + +contract MockERC721DynamicTraits is ERC721, IERC7XXX { + error InvalidCaller(); + + // The manager account that can set traits + address public _manager; + + // The dynamic traits + mapping(bytes32 traitKey => mapping(uint256 tokenId => bytes32 value)) public traits; + + // Set the manager at construction + constructor(address manager) { + _manager = manager; + } + + function mint(address to, uint256 tokenId) public { + _mint(to, tokenId); + } + + function tokenURI(uint256) public pure override returns (string memory) { + return "tokenURI"; + } + + function name() public view virtual override returns (string memory) { + return "TestERC721"; + } + + function symbol() public view virtual override returns (string memory) { + return "TST721"; + } + + function getTrait(bytes32 traitKey, uint256 tokenId) public view returns (bytes32) { + return traits[traitKey][tokenId]; + } + + function getTraits(bytes32 traitKey, uint256[] calldata tokenIds) public view returns (bytes32[] memory) { + bytes32[] memory values = new bytes32[](tokenIds.length); + for (uint256 i = 0; i < tokenIds.length; i++) { + values[i] = traits[traitKey][tokenIds[i]]; + } + return values; + } + + function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) public { + if (msg.sender != _manager) { + revert InvalidCaller(); + } + traits[traitKey][tokenId] = value; + } + + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, IERC7XXX) returns (bool) { + return interfaceId == type(IERC7XXX).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/lib/redeemables/test/utils/mocks/TestERC1155.sol b/lib/redeemables/test/utils/mocks/TestERC1155.sol new file mode 100644 index 0000000..7eb4e86 --- /dev/null +++ b/lib/redeemables/test/utils/mocks/TestERC1155.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.19; + +import {ERC1155} from "solady/src/tokens/ERC1155.sol"; + +// Used for minting test ERC1155s in our tests +contract TestERC1155 is ERC1155 { + function mint(address to, uint256 tokenId, uint256 amount) public returns (bool) { + _mint(to, tokenId, amount, ""); + return true; + } + + function uri(uint256) public pure override returns (string memory) { + return "uri"; + } +} diff --git a/lib/redeemables/test/utils/mocks/TestERC20.sol b/lib/redeemables/test/utils/mocks/TestERC20.sol new file mode 100644 index 0000000..bb00468 --- /dev/null +++ b/lib/redeemables/test/utils/mocks/TestERC20.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.19; + +import {ERC20} from "solady/src/tokens/ERC20.sol"; + +// Used for minting test ERC20s in our tests +contract TestERC20 is ERC20 { + bool public blocked; + + bool public noReturnData; + + constructor() { + blocked = false; + noReturnData = false; + } + + function blockTransfer(bool blocking) external { + blocked = blocking; + } + + function setNoReturnData(bool noReturn) external { + noReturnData = noReturn; + } + + function mint(address to, uint256 amount) external returns (bool) { + _mint(to, amount); + return true; + } + + function transferFrom(address from, address to, uint256 amount) public override returns (bool ok) { + if (blocked) { + return false; + } + + uint256 allowed = allowance(from, msg.sender); + + if (amount > allowed) { + revert("NOT_AUTHORIZED"); + } + + super.transferFrom(from, to, amount); + + if (noReturnData) { + assembly { + return(0, 0) + } + } + + ok = true; + } + + function name() public view virtual override returns (string memory) { + return "TestERC20"; + } + + function symbol() public view virtual override returns (string memory) { + return "TST20"; + } +} diff --git a/lib/redeemables/test/utils/mocks/TestERC721.sol b/lib/redeemables/test/utils/mocks/TestERC721.sol new file mode 100644 index 0000000..50c5865 --- /dev/null +++ b/lib/redeemables/test/utils/mocks/TestERC721.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.19; + +import {ERC721} from "solady/src/tokens/ERC721.sol"; + +// Used for minting test ERC721s in our tests +contract TestERC721 is ERC721 { + function mint(address to, uint256 tokenId) public { + _mint(to, tokenId); + } + + function tokenURI(uint256) public pure override returns (string memory) { + return "tokenURI"; + } + + function name() public view virtual override returns (string memory) { + return "TestERC721"; + } + + function symbol() public view virtual override returns (string memory) { + return "TST721"; + } +} From 9e50978a1065044ed7692b75811076c22b8d2084 Mon Sep 17 00:00:00 2001 From: Stephan Min Date: Tue, 19 Sep 2023 11:08:44 -0400 Subject: [PATCH 02/10] progress on trait redemptions in redeem --- src/dynamic-traits/ERC7498DynamicTraits.sol | 234 ++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 src/dynamic-traits/ERC7498DynamicTraits.sol diff --git a/src/dynamic-traits/ERC7498DynamicTraits.sol b/src/dynamic-traits/ERC7498DynamicTraits.sol new file mode 100644 index 0000000..00f98db --- /dev/null +++ b/src/dynamic-traits/ERC7498DynamicTraits.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ERC721DynamicTraits} from "./ERC721DynamicTraits.sol"; +import {ERC20} from "solady/src/tokens/ERC20.sol"; +import {IERC7498} from "../interfaces/IERC7498.sol"; +import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; +import {IERC721RedemptionMintable} from "../interfaces/IERC721RedemptionMintable.sol"; +import {CampaignParams} from "./RedeemableStructs.sol"; +import {RedeemableErrorsAndEvents} from "./RedeemableErrorsAndEvents.sol"; + +contract ERC7498NFTRedeemables is ERC721DynamicTraits, IERC7498, RedeemableErrorsAndEvents { + /// @dev Counter for next campaign id. + uint256 private _nextCampaignId = 1; + + /// @dev The campaign parameters by campaign id. + mapping(uint256 campaignId => CampaignParams params) private _campaignParams; + + /// @dev The campaign URIs by campaign id. + mapping(uint256 campaignId => string campaignURI) private _campaignURIs; + + /// @dev The total current redemptions by campaign id. + mapping(uint256 campaignId => uint256 count) private _totalRedemptions; + + constructor() ERC721 {} + + function name() public pure override returns (string memory) { + return "ERC7498 NFT Redeemables"; + } + + function symbol() public pure override returns (string memory) { + return "NFTR"; + } + + function tokenURI(uint256 tokenId) public view override returns (string memory) { + return "https://example.com/"; + } + + function mint(address to, uint256 tokenId) public { + _mint(to, tokenId); + } + + function redeem(uint256[] calldata tokenIds, address recipient, bytes calldata extraData) public virtual { + // Get the campaign. + uint256 campaignId = uint256(bytes32(extraData[0:32])); + CampaignParams storage params = _campaignParams[campaignId]; + ConsiderationItem[] memory consideration = params.consideration; + + // Revert if campaign is inactive. + if (_isInactive(params.startTime, params.endTime)) { + revert NotActive(block.timestamp, params.startTime, params.endTime); + } + + // Revert if max total redemptions would be exceeded. + if (_totalRedemptions[campaignId] + tokenIds.length > params.maxCampaignRedemptions) { + revert MaxCampaignRedemptionsReached(_totalRedemptions[campaignId] + 1, params.maxCampaignRedemptions); + } + + address considerationRecipient; + + for (uint256 i; i < consideration.length;) { + if (consideration[i].token == address(this)) { + considerationRecipient = consideration[i].recipient; + } + + unchecked { + ++i; + } + } + + TraitRedemption[] memory traitRedemptions; + + // Iterate over the trait redemptions and set traits on the tokens. + for (uint256 i; i < traitRedemptions.length;) { + // Get the trait redemption token address and place on the stack. + address token = traitRedemptions[i].token; + + uint256 identifier = traitRedemptions[i].identifier; + + // Revert if the trait redemption token is not this token contract. + if (token != address(this)) { + revert InvalidToken(token); + } + + // Revert if the trait redemption identifier is not owned by the caller. + if (ERC721(token).ownerOf(identifier) != msg.sender) { + revert InvalidCaller(token); + } + + // Get the substandard and place on the stack. + uint8 substandard = traitRedemptions[i].substandard; + + // Get the substandard value and place on the stack. + bytes32 substandardValue = traitRedemptions[i].substandardValue; + + // Get the trait key and place on the stack. + bytes32 traitKey = traitRedemptions[i].traitKey; + + // Get the current trait value and place on the stack. + bytes32 currentTraitValue = getTraitValue(traitKey, identifier); + + // If substandard is 1, set trait to traitValue. + if (substandard == 1) { + // Revert if the current trait value does not match the substandard value. + if (currentTraitValue != substandardValue) { + revert InvalidRequiredValue(existingTraitValue, substandardValue); + } + + // Set the trait to the trait value. + _setTrait(traitRedemptions[i].traitKey, traitRedemptions[i].identifier, traitRedemptions[i].traitValue); + // If substandard is 2, increment trait by traitValue. + } else if (substandard == 2) { + // Revert if the current trait value is greater than the substandard value. + if (currentTraitValue > substandardValue) { + revert InvalidRequiredValue(currentTraitValue, substandardValue); + } + + // Increment the trait by the trait value. + _setTrait( + traitRedemptions[i].traitKey, + traitRedemptions[i].identifier, + currentTraitValue + traitRedemptions[i].traitValue + ); + } else if (substandard == 3) { + if (currentTraitValue < substandardValue) { + revert InvalidRequiredValue(currentTraitValue, substandardValue); + } + + // Decrement the trait by the trait value. + _setTrait( + traitRedemptions[i].traitKey, + traitRedemptions[i].identifier, + currentTraitValue - traitRedemptions[i].traitValue + ); + } + } + + // Iterate over the token IDs and check if caller is the owner or approved operator. + // Redeem the token if the caller is valid. + for (uint256 i; i < tokenIds.length;) { + // Get the identifier. + uint256 identifier = tokenIds[i]; + + // Get the token owner. + address owner = ownerOf(identifier); + + // Check the caller is either the owner or approved operator. + if (msg.sender != owner && !isApprovedForAll(owner, msg.sender)) { + revert InvalidCaller(msg.sender); + } + + // Transfer the token to the consideration recipient. + ERC721(consideration[i].token).safeTransferFrom(owner, considerationRecipient, identifier); + + // Mint the redemption token. + IERC721RedemptionMintable(params.offer[0].token).mintRedemption(recipient, identifier); + + unchecked { + ++i; + } + } + } + + function getCampaign(uint256 campaignId) + external + view + override + returns (CampaignParams memory params, string memory uri, uint256 totalRedemptions) + {} + + function createCampaign(CampaignParams calldata params, string calldata uri) + external + override + returns (uint256 campaignId) + { + // Revert if there are no consideration items, since the redemption should require at least something. + if (params.consideration.length == 0) revert NoConsiderationItems(); + + // Revert if startTime is past endTime. + if (params.startTime > params.endTime) revert InvalidTime(); + + // Revert if any of the consideration item recipients is the zero address. The 0xdead address should be used instead. + for (uint256 i = 0; i < params.consideration.length;) { + if (params.consideration[i].recipient == address(0)) { + revert ConsiderationItemRecipientCannotBeZeroAddress(); + } + unchecked { + ++i; + } + } + + // Set the campaign params for the next campaignId. + _campaignParams[_nextCampaignId] = params; + + // Set the campaign URI for the next campaignId. + _campaignURIs[_nextCampaignId] = uri; + + // Set the correct current campaignId to return before incrementing + // the next campaignId. + campaignId = _nextCampaignId; + + // Increment the next campaignId. + _nextCampaignId++; + + emit CampaignUpdated(campaignId, params, _campaignURIs[campaignId]); + } + + function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata uri) + external + override + {} + + /** + * @dev Internal pure function to cast a `bool` value to a `uint256` value. + * + * @param b The `bool` value to cast. + * + * @return u The `uint256` value. + */ + function _cast(bool b) internal pure returns (uint256 u) { + assembly { + u := b + } + } + + function _isInactive(uint256 startTime, uint256 endTime) internal view returns (bool inactive) { + // Using the same check for time boundary from Seaport. + // startTime <= block.timestamp < endTime + assembly { + inactive := or(iszero(gt(endTime, timestamp())), gt(startTime, timestamp())) + } + } +} From d0379b6198f639b024d7f1b009da8d4c918d3ac9 Mon Sep 17 00:00:00 2001 From: Stephan Min Date: Tue, 19 Sep 2023 11:12:16 -0400 Subject: [PATCH 03/10] add todo --- src/dynamic-traits/ERC7498DynamicTraits.sol | 95 +++++++++++---------- 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/src/dynamic-traits/ERC7498DynamicTraits.sol b/src/dynamic-traits/ERC7498DynamicTraits.sol index 00f98db..d9df176 100644 --- a/src/dynamic-traits/ERC7498DynamicTraits.sol +++ b/src/dynamic-traits/ERC7498DynamicTraits.sol @@ -69,6 +69,7 @@ contract ERC7498NFTRedeemables is ERC721DynamicTraits, IERC7498, RedeemableError } } + // TODO: get traitRedemptions from extraData. TraitRedemption[] memory traitRedemptions; // Iterate over the trait redemptions and set traits on the tokens. @@ -88,51 +89,57 @@ contract ERC7498NFTRedeemables is ERC721DynamicTraits, IERC7498, RedeemableError revert InvalidCaller(token); } - // Get the substandard and place on the stack. - uint8 substandard = traitRedemptions[i].substandard; - - // Get the substandard value and place on the stack. - bytes32 substandardValue = traitRedemptions[i].substandardValue; - - // Get the trait key and place on the stack. - bytes32 traitKey = traitRedemptions[i].traitKey; - - // Get the current trait value and place on the stack. - bytes32 currentTraitValue = getTraitValue(traitKey, identifier); - - // If substandard is 1, set trait to traitValue. - if (substandard == 1) { - // Revert if the current trait value does not match the substandard value. - if (currentTraitValue != substandardValue) { - revert InvalidRequiredValue(existingTraitValue, substandardValue); + // Declare a new block to manage stack depth. + { + // Get the substandard and place on the stack. + uint8 substandard = traitRedemptions[i].substandard; + + // Get the substandard value and place on the stack. + bytes32 substandardValue = traitRedemptions[i].substandardValue; + + // Get the trait key and place on the stack. + bytes32 traitKey = traitRedemptions[i].traitKey; + + // Get the current trait value and place on the stack. + bytes32 currentTraitValue = getTraitValue(traitKey, identifier); + + // If substandard is 1, set trait to traitValue. + if (substandard == 1) { + // Revert if the current trait value does not match the substandard value. + if (currentTraitValue != substandardValue) { + revert InvalidRequiredValue(existingTraitValue, substandardValue); + } + + // Set the trait to the trait value. + _setTrait( + traitRedemptions[i].traitKey, traitRedemptions[i].identifier, traitRedemptions[i].traitValue + ); + // If substandard is 2, increment trait by traitValue. + } else if (substandard == 2) { + // Revert if the current trait value is greater than the substandard value. + if (currentTraitValue > substandardValue) { + revert InvalidRequiredValue(currentTraitValue, substandardValue); + } + + // Increment the trait by the trait value. + _setTrait( + traitRedemptions[i].traitKey, + traitRedemptions[i].identifier, + currentTraitValue + traitRedemptions[i].traitValue + ); + } else if (substandard == 3) { + // Revert if the current trait value is less than the substandard value. + if (currentTraitValue < substandardValue) { + revert InvalidRequiredValue(currentTraitValue, substandardValue); + } + + // Decrement the trait by the trait value. + _setTrait( + traitRedemptions[i].traitKey, + traitRedemptions[i].identifier, + currentTraitValue - traitRedemptions[i].traitValue + ); } - - // Set the trait to the trait value. - _setTrait(traitRedemptions[i].traitKey, traitRedemptions[i].identifier, traitRedemptions[i].traitValue); - // If substandard is 2, increment trait by traitValue. - } else if (substandard == 2) { - // Revert if the current trait value is greater than the substandard value. - if (currentTraitValue > substandardValue) { - revert InvalidRequiredValue(currentTraitValue, substandardValue); - } - - // Increment the trait by the trait value. - _setTrait( - traitRedemptions[i].traitKey, - traitRedemptions[i].identifier, - currentTraitValue + traitRedemptions[i].traitValue - ); - } else if (substandard == 3) { - if (currentTraitValue < substandardValue) { - revert InvalidRequiredValue(currentTraitValue, substandardValue); - } - - // Decrement the trait by the trait value. - _setTrait( - traitRedemptions[i].traitKey, - traitRedemptions[i].identifier, - currentTraitValue - traitRedemptions[i].traitValue - ); } } From 9e41a7a33d9009b9fc2f19b2218deb69b421c362 Mon Sep 17 00:00:00 2001 From: Stephan Min Date: Tue, 19 Sep 2023 16:18:14 -0400 Subject: [PATCH 04/10] add seadrop and redeemables files --- .gitmodules | 9 + foundry.toml | 4 + lib/ERC721A | 1 + lib/openzeppelin-contracts-upgradeable | 1 + lib/redeemables/src/lib/RedeemableStructs.sol | 2 +- lib/utility-contracts | 1 + ...icTraits.sol => AbstractDynamicTraits.sol} | 9 +- .../ERC7498DynamicTraits.sol | 84 +- .../interfaces/IERC721RedemptionMintable.sol | 18 + src/redeemables/interfaces/IERC7498.sol | 87 ++ .../lib/RedeemablesErrorsAndEvents.sol | 40 + src/redeemables/lib/RedeemablesStructs.sol | 23 + src/seadrop/src/ERC721ContractMetadata.sol | 284 ++++ src/seadrop/src/ERC721PartnerSeaDrop.sol | 309 ++++ src/seadrop/src/ERC721SeaDrop.sol | 597 ++++++++ src/seadrop/src/SeaDrop.sol | 1154 +++++++++++++++ src/seadrop/src/clones/ERC721ACloneable.sol | 1237 +++++++++++++++++ .../ERC721ContractMetadataCloneable.sol | 285 ++++ .../src/clones/ERC721SeaDropCloneFactory.sol | 29 + .../src/clones/ERC721SeaDropCloneable.sol | 586 ++++++++ .../ERC721PartnerSeaDropBurnable.sol | 31 + .../ERC721PartnerSeaDropRandomOffset.sol | 99 ++ .../src/extensions/ERC721SeaDropBurnable.sol | 34 + .../extensions/ERC721SeaDropRandomOffset.sol | 100 ++ .../extensions/ERC721SeaDropRedeemable.sol | 131 ++ .../ERC721SeaDropRedemptionMintable.sol | 119 ++ .../interfaces/INonFungibleSeaDropToken.sol | 160 +++ src/seadrop/src/interfaces/ISeaDrop.sol | 312 +++++ .../ISeaDropTokenContractMetadata.sol | 141 ++ .../ERC721SeaDropStructsErrorsAndEvents.sol | 56 + .../src/lib/SeaDropErrorsAndEvents.sol | 288 ++++ src/seadrop/src/lib/SeaDropStructs.sol | 144 ++ src/seadrop/src/shim/Shim.sol | 10 + src/seadrop/src/test/MaliciousRecipient.sol | 42 + src/seadrop/src/test/TestERC721.sol | 16 + test/dynamic-traits/ERC721DynamicTraits.t.sol | 11 +- 36 files changed, 6404 insertions(+), 50 deletions(-) create mode 160000 lib/ERC721A create mode 160000 lib/openzeppelin-contracts-upgradeable create mode 160000 lib/utility-contracts rename src/dynamic-traits/{ERC721DynamicTraits.sol => AbstractDynamicTraits.sol} (65%) rename src/{dynamic-traits => redeemables}/ERC7498DynamicTraits.sol (76%) create mode 100644 src/redeemables/interfaces/IERC721RedemptionMintable.sol create mode 100644 src/redeemables/interfaces/IERC7498.sol create mode 100644 src/redeemables/lib/RedeemablesErrorsAndEvents.sol create mode 100644 src/redeemables/lib/RedeemablesStructs.sol create mode 100644 src/seadrop/src/ERC721ContractMetadata.sol create mode 100644 src/seadrop/src/ERC721PartnerSeaDrop.sol create mode 100644 src/seadrop/src/ERC721SeaDrop.sol create mode 100644 src/seadrop/src/SeaDrop.sol create mode 100644 src/seadrop/src/clones/ERC721ACloneable.sol create mode 100644 src/seadrop/src/clones/ERC721ContractMetadataCloneable.sol create mode 100644 src/seadrop/src/clones/ERC721SeaDropCloneFactory.sol create mode 100644 src/seadrop/src/clones/ERC721SeaDropCloneable.sol create mode 100644 src/seadrop/src/extensions/ERC721PartnerSeaDropBurnable.sol create mode 100644 src/seadrop/src/extensions/ERC721PartnerSeaDropRandomOffset.sol create mode 100644 src/seadrop/src/extensions/ERC721SeaDropBurnable.sol create mode 100644 src/seadrop/src/extensions/ERC721SeaDropRandomOffset.sol create mode 100644 src/seadrop/src/extensions/ERC721SeaDropRedeemable.sol create mode 100644 src/seadrop/src/extensions/ERC721SeaDropRedemptionMintable.sol create mode 100644 src/seadrop/src/interfaces/INonFungibleSeaDropToken.sol create mode 100644 src/seadrop/src/interfaces/ISeaDrop.sol create mode 100644 src/seadrop/src/interfaces/ISeaDropTokenContractMetadata.sol create mode 100644 src/seadrop/src/lib/ERC721SeaDropStructsErrorsAndEvents.sol create mode 100644 src/seadrop/src/lib/SeaDropErrorsAndEvents.sol create mode 100644 src/seadrop/src/lib/SeaDropStructs.sol create mode 100644 src/seadrop/src/shim/Shim.sol create mode 100644 src/seadrop/src/test/MaliciousRecipient.sol create mode 100644 src/seadrop/src/test/TestERC721.sol diff --git a/.gitmodules b/.gitmodules index f59df4c..483a33b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,12 @@ [submodule "redeemables"] path = redeemables url = https://github.com/ProjectOpenSea/redeemables.git +[submodule "lib/ERC721A"] + path = lib/ERC721A + url = https://github.com/chiru-labs/ERC721A +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/openzeppelin/openzeppelin-contracts-upgradeable +[submodule "lib/utility-contracts"] + path = lib/utility-contracts + url = https://github.com/jameswenzel/utility-contracts diff --git a/foundry.toml b/foundry.toml index 9afd017..5173517 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,9 +1,11 @@ [profile.default] +solc = "0.8.21" src = "src" out = "out" libs = ["lib"] remappings = [ 'forge-std/=lib/forge-std/src', + 'ERC721A/=lib/ERC721A/contracts', 'solady/=lib/solady/src/', 'solady-test/=lib/solady/test/', 'openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/', @@ -12,6 +14,8 @@ remappings = [ 'solarray/=lib/solarray/src/', 'dynamic-traits/=lib/dynamic-traits/src/', 'openzeppelin-contracts/contracts/=lib/openzeppelin-contracts/contracts/', + 'openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/', + 'utility-contracts/=lib/utility-contracts/src/', ] # bytecode_hash = 'none' ignored_error_codes = ['license', 'code-size', 'init-code-size', 2519] diff --git a/lib/ERC721A b/lib/ERC721A new file mode 160000 index 0000000..9be81f0 --- /dev/null +++ b/lib/ERC721A @@ -0,0 +1 @@ +Subproject commit 9be81f029a0883e0c7c6d5a410ee49c48c69205d diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..3d4c0d5 --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 3d4c0d5741b131c231e558d7a6213392ab3672a5 diff --git a/lib/redeemables/src/lib/RedeemableStructs.sol b/lib/redeemables/src/lib/RedeemableStructs.sol index dd6d2b4..db35636 100644 --- a/lib/redeemables/src/lib/RedeemableStructs.sol +++ b/lib/redeemables/src/lib/RedeemableStructs.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/lib/ConsiderationStructs.sol"; struct CampaignParams { uint32 startTime; diff --git a/lib/utility-contracts b/lib/utility-contracts new file mode 160000 index 0000000..6543a1d --- /dev/null +++ b/lib/utility-contracts @@ -0,0 +1 @@ +Subproject commit 6543a1dd10160936849de2f2fbcc18503906fb1e diff --git a/src/dynamic-traits/ERC721DynamicTraits.sol b/src/dynamic-traits/AbstractDynamicTraits.sol similarity index 65% rename from src/dynamic-traits/ERC721DynamicTraits.sol rename to src/dynamic-traits/AbstractDynamicTraits.sol index bd8988b..e777880 100644 --- a/src/dynamic-traits/ERC721DynamicTraits.sol +++ b/src/dynamic-traits/AbstractDynamicTraits.sol @@ -1,12 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; import {Ownable} from "openzeppelin-contracts/access/Ownable.sol"; import {DynamicTraits} from "./DynamicTraits.sol"; -contract ERC721DynamicTraits is DynamicTraits, Ownable, ERC721 { - constructor() Ownable(msg.sender) ERC721("ERC721DynamicTraits", "ERC721DT") { +abstract contract AbstractDynamicTraits is DynamicTraits, Ownable { + constructor() Ownable(msg.sender) { _traitLabelsURI = "https://example.com"; } @@ -22,7 +21,7 @@ contract ERC721DynamicTraits is DynamicTraits, Ownable, ERC721 { _setTraitLabelsURI(uri); } - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, DynamicTraits) returns (bool) { - return ERC721.supportsInterface(interfaceId) || DynamicTraits.supportsInterface(interfaceId); + function supportsInterface(bytes4 interfaceId) public view virtual override(DynamicTraits) returns (bool) { + return DynamicTraits.supportsInterface(interfaceId); } } diff --git a/src/dynamic-traits/ERC7498DynamicTraits.sol b/src/redeemables/ERC7498DynamicTraits.sol similarity index 76% rename from src/dynamic-traits/ERC7498DynamicTraits.sol rename to src/redeemables/ERC7498DynamicTraits.sol index d9df176..85bf184 100644 --- a/src/dynamic-traits/ERC7498DynamicTraits.sol +++ b/src/redeemables/ERC7498DynamicTraits.sol @@ -1,16 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {ERC721DynamicTraits} from "./ERC721DynamicTraits.sol"; -import {ERC20} from "solady/src/tokens/ERC20.sol"; -import {IERC7498} from "../interfaces/IERC7498.sol"; -import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; -import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; -import {IERC721RedemptionMintable} from "../interfaces/IERC721RedemptionMintable.sol"; -import {CampaignParams} from "./RedeemableStructs.sol"; -import {RedeemableErrorsAndEvents} from "./RedeemableErrorsAndEvents.sol"; - -contract ERC7498NFTRedeemables is ERC721DynamicTraits, IERC7498, RedeemableErrorsAndEvents { +import {AbstractDynamicTraits} from "../dynamic-traits/AbstractDynamicTraits.sol"; +import {ERC20} from "solady/tokens/ERC20.sol"; +import {ERC721} from "solady/tokens/ERC721.sol"; +import {ERC721SeaDrop} from "../seadrop/src/ERC721SeaDrop.sol"; +import {IERC7498} from "./interfaces/IERC7498.sol"; +import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/lib/ConsiderationStructs.sol"; +import {ItemType} from "seaport-types/lib/ConsiderationEnums.sol"; +import {IERC721RedemptionMintable} from "./interfaces/IERC721RedemptionMintable.sol"; +import {CampaignParams, TraitRedemption} from "./lib/RedeemablesStructs.sol"; +import {RedeemablesErrorsAndEvents} from "./lib/RedeemablesErrorsAndEvents.sol"; + +contract ERC7498NFTRedeemables is AbstractDynamicTraits, ERC721SeaDrop, IERC7498, RedeemablesErrorsAndEvents { /// @dev Counter for next campaign id. uint256 private _nextCampaignId = 1; @@ -23,7 +25,7 @@ contract ERC7498NFTRedeemables is ERC721DynamicTraits, IERC7498, RedeemableError /// @dev The total current redemptions by campaign id. mapping(uint256 campaignId => uint256 count) private _totalRedemptions; - constructor() ERC721 {} + constructor() AbstractDynamicTraits() ERC721SeaDrop() {} function name() public pure override returns (string memory) { return "ERC7498 NFT Redeemables"; @@ -41,7 +43,10 @@ contract ERC7498NFTRedeemables is ERC721DynamicTraits, IERC7498, RedeemableError _mint(to, tokenId); } - function redeem(uint256[] calldata tokenIds, address recipient, bytes calldata extraData) public virtual { + // at 64 will be pointer to array + // mload pointer, pointer points to length + // next word is start of array + function redeem(uint256[] calldata tokenIds, address recipient, bytes calldata extraData) public virtual override { // Get the campaign. uint256 campaignId = uint256(bytes32(extraData[0:32])); CampaignParams storage params = _campaignParams[campaignId]; @@ -69,8 +74,19 @@ contract ERC7498NFTRedeemables is ERC721DynamicTraits, IERC7498, RedeemableError } } - // TODO: get traitRedemptions from extraData. - TraitRedemption[] memory traitRedemptions; + TraitRedemption[] calldata traitRedemptions; + + // calldata array is two vars on stack (length, ptr to start of array) + assembly { + // Get the pointer to the length of the trait redemptions array by adding 0x40 to the extraData offset. + let traitRedemptionsLengthPtr := calldataload(add(0x40, extraData.offset)) + + // Set the length of the trait redeptions array to the value at the array length pointer. + traitRedemptions.length := calldataload(traitRedemptionsLengthPtr) + + // Set the pointer to the start of the trait redemptions array to the word after the length. + traitRedemptions.offset := add(0x20, traitRedemptionsLengthPtr) + } // Iterate over the trait redemptions and set traits on the tokens. for (uint256 i; i < traitRedemptions.length;) { @@ -100,6 +116,8 @@ contract ERC7498NFTRedeemables is ERC721DynamicTraits, IERC7498, RedeemableError // Get the trait key and place on the stack. bytes32 traitKey = traitRedemptions[i].traitKey; + bytes32 traitValue = traitRedemptions[i].traitValue; + // Get the current trait value and place on the stack. bytes32 currentTraitValue = getTraitValue(traitKey, identifier); @@ -107,13 +125,11 @@ contract ERC7498NFTRedeemables is ERC721DynamicTraits, IERC7498, RedeemableError if (substandard == 1) { // Revert if the current trait value does not match the substandard value. if (currentTraitValue != substandardValue) { - revert InvalidRequiredValue(existingTraitValue, substandardValue); + revert InvalidRequiredValue(currentTraitValue, substandardValue); } // Set the trait to the trait value. - _setTrait( - traitRedemptions[i].traitKey, traitRedemptions[i].identifier, traitRedemptions[i].traitValue - ); + _setTrait(traitRedemptions[i].traitKey, identifier, traitValue); // If substandard is 2, increment trait by traitValue. } else if (substandard == 2) { // Revert if the current trait value is greater than the substandard value. @@ -122,25 +138,24 @@ contract ERC7498NFTRedeemables is ERC721DynamicTraits, IERC7498, RedeemableError } // Increment the trait by the trait value. - _setTrait( - traitRedemptions[i].traitKey, - traitRedemptions[i].identifier, - currentTraitValue + traitRedemptions[i].traitValue - ); + uint256 newTraitValue = uint256(currentTraitValue) + uint256(traitValue); + + _setTrait(traitRedemptions[i].traitKey, identifier, bytes32(newTraitValue)); } else if (substandard == 3) { // Revert if the current trait value is less than the substandard value. if (currentTraitValue < substandardValue) { revert InvalidRequiredValue(currentTraitValue, substandardValue); } + uint256 newTraitValue = uint256(currentTraitValue) - uint256(traitValue); + // Decrement the trait by the trait value. - _setTrait( - traitRedemptions[i].traitKey, - traitRedemptions[i].identifier, - currentTraitValue - traitRedemptions[i].traitValue - ); + _setTrait(traitRedemptions[i].traitKey, traitRedemptions[i].identifier, bytes32(newTraitValue)); } } + unchecked { + ++i; + } } // Iterate over the token IDs and check if caller is the owner or approved operator. @@ -218,19 +233,6 @@ contract ERC7498NFTRedeemables is ERC721DynamicTraits, IERC7498, RedeemableError override {} - /** - * @dev Internal pure function to cast a `bool` value to a `uint256` value. - * - * @param b The `bool` value to cast. - * - * @return u The `uint256` value. - */ - function _cast(bool b) internal pure returns (uint256 u) { - assembly { - u := b - } - } - function _isInactive(uint256 startTime, uint256 endTime) internal view returns (bool inactive) { // Using the same check for time boundary from Seaport. // startTime <= block.timestamp < endTime diff --git a/src/redeemables/interfaces/IERC721RedemptionMintable.sol b/src/redeemables/interfaces/IERC721RedemptionMintable.sol new file mode 100644 index 0000000..3e855c9 --- /dev/null +++ b/src/redeemables/interfaces/IERC721RedemptionMintable.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {SpentItem} from "seaport-types/lib/ConsiderationStructs.sol"; + +interface IERC721RedemptionMintable { + function mintRedemption(address to, uint256 tokenId) external returns (uint256); +} + +// import {ConsiderationItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; + +// interface IERC721RedemptionMintable { +// function mintRedemption( +// uint256 campaignId, +// address recipient, +// ConsiderationItem[] memory consideration +// ) external returns (uint256[] memory tokenIds); +// } diff --git a/src/redeemables/interfaces/IERC7498.sol b/src/redeemables/interfaces/IERC7498.sol new file mode 100644 index 0000000..18346ed --- /dev/null +++ b/src/redeemables/interfaces/IERC7498.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/lib/ConsiderationStructs.sol"; +import {CampaignParams, TraitRedemption} from "../lib/RedeemablesStructs.sol"; + +interface IERC7498 { + /* Events */ + // event CampaignUpdated( + // uint256 indexed campaignId, + // CampaignParams params, + // string URI + // ); + // event Redemption( + // uint256 indexed campaignId, + // bytes32 redemptionHash, + // uint256[] tokenIds, + // address redeemedBy + // ); + + /* Structs */ + // struct CampaignParams { + // uint32 startTime; + // uint32 endTime; + // uint32 maxCampaignRedemptions; + // address manager; // the address that can modify the campaign + // address signer; // null address means no EIP-712 signature required + // OfferItem[] offer; // items to be minted, can be empty for offchain redeemable + // ConsiderationItem[] consideration; // the items you are transferring to recipient + // } + + // struct TraitRedemption { + // uint8 substandard; + // address token; + // uint256 identifier; + // bytes32 traitKey; + // bytes32 traitValue; + // bytes32 substandardValue; + // } + + /* Getters */ + function getCampaign(uint256 campaignId) + external + view + returns (CampaignParams memory params, string memory uri, uint256 totalRedemptions); + + /* Setters */ + function createCampaign(CampaignParams calldata params, string calldata uri) + external + returns (uint256 campaignId); + + function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata uri) external; + + function redeem(uint256[] calldata tokenIds, address recipient, bytes calldata extraData) external; +} + +/* Seaport structs, for reference, used in offer/consideration above */ +// enum ItemType { +// NATIVE, +// ERC20, +// ERC721, +// ERC1155 +// } + +// struct OfferItem { +// ItemType itemType; +// address token; +// uint256 identifierOrCriteria; +// uint256 startAmount; +// uint256 endAmount; +// } + +// struct ConsiderationItem { +// ItemType itemType; +// address token; +// uint256 identifierOrCriteria; +// uint256 startAmount; +// uint256 endAmount; +// address payable recipient; +// } + +// struct SpentItem { +// ItemType itemType; +// address token; +// uint256 identifier; +// uint256 amount; +// } diff --git a/src/redeemables/lib/RedeemablesErrorsAndEvents.sol b/src/redeemables/lib/RedeemablesErrorsAndEvents.sol new file mode 100644 index 0000000..d413a19 --- /dev/null +++ b/src/redeemables/lib/RedeemablesErrorsAndEvents.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {SpentItem} from "seaport-types/lib/ConsiderationStructs.sol"; +import {CampaignParams} from "./RedeemablesStructs.sol"; + +interface RedeemablesErrorsAndEvents { + /// Configuration errors + error NotManager(); + error InvalidTime(); + error NoConsiderationItems(); + error ConsiderationItemRecipientCannotBeZeroAddress(); + + /// Redemption errors + error InvalidCampaignId(); + error CampaignAlreadyExists(); + error InvalidCaller(address caller); + error NotActive(uint256 currentTimestamp, uint256 startTime, uint256 endTime); + error MaxRedemptionsReached(uint256 total, uint256 max); + error MaxCampaignRedemptionsReached(uint256 total, uint256 max); + error NativeTransferFailed(); + error RedeemMismatchedLengths(); + // error TraitValueUnchanged(); + error InvalidConsiderationLength(uint256 got, uint256 want); + error InvalidConsiderationItem(address got, address want); + error InvalidOfferLength(uint256 got, uint256 want); + error InvalidNativeOfferItem(); + error InvalidOwner(); + error InvalidRequiredValue(bytes32 got, bytes32 want); + error InvalidSubstandard(uint256 substandard); + error InvalidToken(address token); + error InvalidTraitRedemption(); + error InvalidTraitRedemptionToken(address token); + error ConsiderationRecipientNotFound(address token); + error RedemptionValuesAreImmutable(); + + /// Events + event CampaignUpdated(uint256 indexed campaignId, CampaignParams params, string uri); + event Redemption(uint256 indexed campaignId, bytes32 redemptionHash); +} diff --git a/src/redeemables/lib/RedeemablesStructs.sol b/src/redeemables/lib/RedeemablesStructs.sol new file mode 100644 index 0000000..dab79c8 --- /dev/null +++ b/src/redeemables/lib/RedeemablesStructs.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/lib/ConsiderationStructs.sol"; + +struct CampaignParams { + uint32 startTime; + uint32 endTime; + uint32 maxCampaignRedemptions; + address manager; + address signer; + OfferItem[] offer; + ConsiderationItem[] consideration; +} + +struct TraitRedemption { + uint8 substandard; + address token; + uint256 identifier; + bytes32 traitKey; + bytes32 traitValue; + bytes32 substandardValue; +} diff --git a/src/seadrop/src/ERC721ContractMetadata.sol b/src/seadrop/src/ERC721ContractMetadata.sol new file mode 100644 index 0000000..e6e6857 --- /dev/null +++ b/src/seadrop/src/ERC721ContractMetadata.sol @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {ISeaDropTokenContractMetadata} from "./interfaces/ISeaDropTokenContractMetadata.sol"; + +import {ERC721A} from "ERC721A/ERC721A.sol"; + +import {Ownable} from "solady/auth/Ownable.sol"; + +import {IERC2981} from "openzeppelin-contracts/interfaces/IERC2981.sol"; + +import {IERC165} from "openzeppelin-contracts/utils/introspection/IERC165.sol"; + +/** + * @title ERC721ContractMetadata + * @author James Wenzel (emo.eth) + * @author Ryan Ghods (ralxz.eth) + * @author Stephan Min (stephanm.eth) + * @notice ERC721ContractMetadata is a token contract that extends ERC721A + * with additional metadata and ownership capabilities. + */ +contract ERC721ContractMetadata is ERC721A, Ownable, ISeaDropTokenContractMetadata { + /// @notice Track the max supply. + uint256 _maxSupply; + + /// @notice Track the base URI for token metadata. + string _tokenBaseURI; + + /// @notice Track the contract URI for contract metadata. + string _contractURI; + + /// @notice Track the provenance hash for guaranteeing metadata order + /// for random reveals. + bytes32 _provenanceHash; + + /// @notice Track the royalty info: address to receive royalties, and + /// royalty basis points. + RoyaltyInfo _royaltyInfo; + + /** + * @dev Reverts if the sender is not the owner or the contract itself. + * This function is inlined instead of being a modifier + * to save contract space from being inlined N times. + */ + function _onlyOwnerOrSelf() internal view { + if (_cast(msg.sender == owner()) | _cast(msg.sender == address(this)) == 0) { + revert Unauthorized(); + } + } + + /** + * @notice Deploy the token contract with its name and symbol. + */ + constructor(string memory name, string memory symbol) ERC721A(name, symbol) {} + + /** + * @notice Sets the base URI for the token metadata and emits an event. + * + * @param newBaseURI The new base URI to set. + */ + function setBaseURI(string calldata newBaseURI) external override { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Set the new base URI. + _tokenBaseURI = newBaseURI; + + // Emit an event with the update. + if (totalSupply() != 0) { + emit BatchMetadataUpdate(1, _nextTokenId() - 1); + } + } + + /** + * @notice Sets the contract URI for contract metadata. + * + * @param newContractURI The new contract URI. + */ + function setContractURI(string calldata newContractURI) external override { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Set the new contract URI. + _contractURI = newContractURI; + + // Emit an event with the update. + emit ContractURIUpdated(newContractURI); + } + + /** + * @notice Emit an event notifying metadata updates for + * a range of token ids, according to EIP-4906. + * + * @param fromTokenId The start token id. + * @param toTokenId The end token id. + */ + function emitBatchMetadataUpdate(uint256 fromTokenId, uint256 toTokenId) external { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Emit an event with the update. + emit BatchMetadataUpdate(fromTokenId, toTokenId); + } + + /** + * @notice Sets the max token supply and emits an event. + * + * @param newMaxSupply The new max supply to set. + */ + function setMaxSupply(uint256 newMaxSupply) external { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the max supply does not exceed the maximum value of uint64. + if (newMaxSupply > 2 ** 64 - 1) { + revert CannotExceedMaxSupplyOfUint64(newMaxSupply); + } + + // Set the new max supply. + _maxSupply = newMaxSupply; + + // Emit an event with the update. + emit MaxSupplyUpdated(newMaxSupply); + } + + /** + * @notice Sets the provenance hash and emits an event. + * + * The provenance hash is used for random reveals, which + * is a hash of the ordered metadata to show it has not been + * modified after mint started. + * + * This function will revert after the first item has been minted. + * + * @param newProvenanceHash The new provenance hash to set. + */ + function setProvenanceHash(bytes32 newProvenanceHash) external { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Revert if any items have been minted. + if (_totalMinted() > 0) { + revert ProvenanceHashCannotBeSetAfterMintStarted(); + } + + // Keep track of the old provenance hash for emitting with the event. + bytes32 oldProvenanceHash = _provenanceHash; + + // Set the new provenance hash. + _provenanceHash = newProvenanceHash; + + // Emit an event with the update. + emit ProvenanceHashUpdated(oldProvenanceHash, newProvenanceHash); + } + + /** + * @notice Sets the address and basis points for royalties. + * + * @param newInfo The struct to configure royalties. + */ + function setRoyaltyInfo(RoyaltyInfo calldata newInfo) external { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Revert if the new royalty address is the zero address. + if (newInfo.royaltyAddress == address(0)) { + revert RoyaltyAddressCannotBeZeroAddress(); + } + + // Revert if the new basis points is greater than 10_000. + if (newInfo.royaltyBps > 10_000) { + revert InvalidRoyaltyBasisPoints(newInfo.royaltyBps); + } + + // Set the new royalty info. + _royaltyInfo = newInfo; + + // Emit an event with the updated params. + emit RoyaltyInfoUpdated(newInfo.royaltyAddress, newInfo.royaltyBps); + } + + /** + * @notice Returns the base URI for token metadata. + */ + function baseURI() external view override returns (string memory) { + return _baseURI(); + } + + /** + * @notice Returns the base URI for the contract, which ERC721A uses + * to return tokenURI. + */ + function _baseURI() internal view virtual override returns (string memory) { + return _tokenBaseURI; + } + + /** + * @notice Returns the contract URI for contract metadata. + */ + function contractURI() external view override returns (string memory) { + return _contractURI; + } + + /** + * @notice Returns the max token supply. + */ + function maxSupply() public view returns (uint256) { + return _maxSupply; + } + + /** + * @notice Returns the provenance hash. + * The provenance hash is used for random reveals, which + * is a hash of the ordered metadata to show it is unmodified + * after mint has started. + */ + function provenanceHash() external view override returns (bytes32) { + return _provenanceHash; + } + + /** + * @notice Returns the address that receives royalties. + */ + function royaltyAddress() external view returns (address) { + return _royaltyInfo.royaltyAddress; + } + + /** + * @notice Returns the royalty basis points out of 10_000. + */ + function royaltyBasisPoints() external view returns (uint256) { + return _royaltyInfo.royaltyBps; + } + + /** + * @notice Called with the sale price to determine how much royalty + * is owed and to whom. + * + * @ param _tokenId The NFT asset queried for royalty information. + * @param _salePrice The sale price of the NFT asset specified by + * _tokenId. + * + * @return receiver Address of who should be sent the royalty payment. + * @return royaltyAmount The royalty payment amount for _salePrice. + */ + function royaltyInfo(uint256, /* _tokenId */ uint256 _salePrice) + external + view + returns (address receiver, uint256 royaltyAmount) + { + // Put the royalty info on the stack for more efficient access. + RoyaltyInfo storage info = _royaltyInfo; + + // Set the royalty amount to the sale price times the royalty basis + // points divided by 10_000. + royaltyAmount = (_salePrice * info.royaltyBps) / 10_000; + + // Set the receiver of the royalty. + receiver = info.royaltyAddress; + } + + /** + * @notice Returns whether the interface is supported. + * + * @param interfaceId The interface id to check against. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721A) returns (bool) { + return interfaceId == type(IERC2981).interfaceId || interfaceId == 0x49064906 // ERC-4906 + || super.supportsInterface(interfaceId); + } + + /** + * @dev Internal pure function to cast a `bool` value to a `uint256` value. + * + * @param b The `bool` value to cast. + * + * @return u The `uint256` value. + */ + function _cast(bool b) internal pure returns (uint256 u) { + assembly { + u := b + } + } +} diff --git a/src/seadrop/src/ERC721PartnerSeaDrop.sol b/src/seadrop/src/ERC721PartnerSeaDrop.sol new file mode 100644 index 0000000..74e5f89 --- /dev/null +++ b/src/seadrop/src/ERC721PartnerSeaDrop.sol @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {ERC721SeaDrop} from "./ERC721SeaDrop.sol"; + +import {ISeaDrop} from "./interfaces/ISeaDrop.sol"; + +import {AllowListData, PublicDrop, TokenGatedDropStage, SignedMintValidationParams} from "./lib/SeaDropStructs.sol"; + +import {TwoStepAdministered} from "utility-contracts/TwoStepAdministered.sol"; + +/** + * @title ERC721PartnerSeaDrop + * @author James Wenzel (emo.eth) + * @author Ryan Ghods (ralxz.eth) + * @author Stephan Min (stephanm.eth) + * @notice ERC721PartnerSeaDrop is a token contract that contains methods + * to properly interact with SeaDrop, with additional administrative + * functionality tailored for business requirements around partnered + * mints with off-chain agreements in place between two parties. + * + * The "Owner" should control mint specifics such as price and start. + * The "Administrator" should control fee parameters. + * + * Otherwise, for ease of administration, either Owner or Administrator + * should be able to configure mint parameters. They have the ability + * to override each other's actions in many circumstances, which is + * why the establishment of off-chain trust is important. + * + * Note: An Administrator is not required to interface with SeaDrop. + */ +contract ERC721PartnerSeaDrop is ERC721SeaDrop, TwoStepAdministered { + /// @notice To prevent Owner from overriding fees, Administrator must + /// first initialize with fee. + error AdministratorMustInitializeWithFee(); + + /** + * @notice Deploy the token contract with its name, symbol, + * administrator, and allowed SeaDrop addresses. + */ + constructor(string memory name, string memory symbol, address administrator, address[] memory allowedSeaDrop) + ERC721SeaDrop(name, symbol, allowedSeaDrop) + TwoStepAdministered(administrator) + {} + + /** + * @notice Mint tokens, restricted to the SeaDrop contract. + * + * @param minter The address to mint to. + * @param quantity The number of tokens to mint. + */ + function mintSeaDrop(address minter, uint256 quantity) external virtual override { + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(msg.sender); + + // Extra safety check to ensure the max supply is not exceeded. + if (_totalMinted() + quantity > maxSupply()) { + revert MintQuantityExceedsMaxSupply(_totalMinted() + quantity, maxSupply()); + } + + // Mint the quantity of tokens to the minter. + _mint(minter, quantity); + } + + /** + * @notice Update the allowed SeaDrop contracts. + * Only the owner or administrator can use this function. + * + * @param allowedSeaDrop The allowed SeaDrop addresses. + */ + function updateAllowedSeaDrop(address[] calldata allowedSeaDrop) external override onlyOwnerOrAdministrator { + _updateAllowedSeaDrop(allowedSeaDrop); + } + + /** + * @notice Update the public drop data for this nft contract on SeaDrop. + * Only the owner or administrator can use this function. + * + * The administrator can only update `feeBps`. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param publicDrop The public drop data. + */ + function updatePublicDrop(address seaDropImpl, PublicDrop calldata publicDrop) + external + virtual + override + onlyOwnerOrAdministrator + { + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Track the previous public drop data. + PublicDrop memory retrieved = ISeaDrop(seaDropImpl).getPublicDrop(address(this)); + + // Track the newly supplied drop data. + PublicDrop memory supplied = publicDrop; + + // Only the administrator (OpenSea) can set feeBps. + if (msg.sender != administrator) { + // Administrator must first set fee. + if (retrieved.maxTotalMintableByWallet == 0) { + revert AdministratorMustInitializeWithFee(); + } + supplied.feeBps = retrieved.feeBps; + supplied.restrictFeeRecipients = true; + } else { + // Administrator can only initialize + // (maxTotalMintableByWallet > 0) and set + // feeBps/restrictFeeRecipients. + uint16 maxTotalMintableByWallet = retrieved.maxTotalMintableByWallet; + retrieved.maxTotalMintableByWallet = maxTotalMintableByWallet > 0 ? maxTotalMintableByWallet : 1; + retrieved.feeBps = supplied.feeBps; + retrieved.restrictFeeRecipients = true; + supplied = retrieved; + } + + // Update the public drop data on SeaDrop. + ISeaDrop(seaDropImpl).updatePublicDrop(supplied); + } + + /** + * @notice Update the allow list data for this nft contract on SeaDrop. + * Only the owner or administrator can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param allowListData The allow list data. + */ + function updateAllowList(address seaDropImpl, AllowListData calldata allowListData) + external + virtual + override + onlyOwnerOrAdministrator + { + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the allow list on SeaDrop. + ISeaDrop(seaDropImpl).updateAllowList(allowListData); + } + + /** + * @notice Update the token gated drop stage data for this nft contract + * on SeaDrop. + * Only the owner or administrator can use this function. + * + * The administrator must first set `feeBps`. + * + * Note: If two INonFungibleSeaDropToken tokens are doing + * simultaneous token gated drop promotions for each other, + * they can be minted by the same actor until + * `maxTokenSupplyForStage` is reached. Please ensure the + * `allowedNftToken` is not running an active drop during the + * `dropStage` time period. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param allowedNftToken The allowed nft token. + * @param dropStage The token gated drop stage data. + */ + function updateTokenGatedDrop(address seaDropImpl, address allowedNftToken, TokenGatedDropStage calldata dropStage) + external + virtual + override + onlyOwnerOrAdministrator + { + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Track the previous drop stage data. + TokenGatedDropStage memory retrieved = ISeaDrop(seaDropImpl).getTokenGatedDrop(address(this), allowedNftToken); + + // Track the newly supplied drop data. + TokenGatedDropStage memory supplied = dropStage; + + // Only the administrator (OpenSea) can set feeBps on Partner + // contracts. + if (msg.sender != administrator) { + // Administrator must first set fee. + if (retrieved.maxTotalMintableByWallet == 0) { + revert AdministratorMustInitializeWithFee(); + } + supplied.feeBps = retrieved.feeBps; + supplied.restrictFeeRecipients = true; + } else { + // Administrator can only initialize + // (maxTotalMintableByWallet > 0) and set + // feeBps/restrictFeeRecipients. + uint16 maxTotalMintableByWallet = retrieved.maxTotalMintableByWallet; + retrieved.maxTotalMintableByWallet = maxTotalMintableByWallet > 0 ? maxTotalMintableByWallet : 1; + retrieved.feeBps = supplied.feeBps; + retrieved.restrictFeeRecipients = true; + supplied = retrieved; + } + + // Update the token gated drop stage. + ISeaDrop(seaDropImpl).updateTokenGatedDrop(allowedNftToken, supplied); + } + + /** + * @notice Update the drop URI for this nft contract on SeaDrop. + * Only the owner or administrator can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param dropURI The new drop URI. + */ + function updateDropURI(address seaDropImpl, string calldata dropURI) + external + virtual + override + onlyOwnerOrAdministrator + { + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the drop URI. + ISeaDrop(seaDropImpl).updateDropURI(dropURI); + } + + /** + * @notice Update the allowed fee recipient for this nft contract + * on SeaDrop. + * Only the administrator can set the allowed fee recipient. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param feeRecipient The new fee recipient. + * @param allowed If the fee recipient is allowed. + */ + function updateAllowedFeeRecipient(address seaDropImpl, address feeRecipient, bool allowed) + external + override + onlyAdministrator + { + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the allowed fee recipient. + ISeaDrop(seaDropImpl).updateAllowedFeeRecipient(feeRecipient, allowed); + } + + /** + * @notice Update the server-side signers for this nft contract + * on SeaDrop. + * Only the owner or administrator can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param signer The signer to update. + * @param signedMintValidationParams Minimum and maximum parameters to + * enforce for signed mints. + */ + function updateSignedMintValidationParams( + address seaDropImpl, + address signer, + SignedMintValidationParams memory signedMintValidationParams + ) external virtual override onlyOwnerOrAdministrator { + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Track the previous signed mint validation params. + SignedMintValidationParams memory retrieved = + ISeaDrop(seaDropImpl).getSignedMintValidationParams(address(this), signer); + + // Track the newly supplied params. + SignedMintValidationParams memory supplied = signedMintValidationParams; + + // Only the administrator (OpenSea) can set feeBps on Partner + // contracts. + if (msg.sender != administrator) { + // Administrator must first set fee. + if (retrieved.maxMaxTotalMintableByWallet == 0) { + revert AdministratorMustInitializeWithFee(); + } + supplied.minFeeBps = retrieved.minFeeBps; + supplied.maxFeeBps = retrieved.maxFeeBps; + } else { + // Administrator can only initialize + // (maxTotalMintableByWallet > 0) and set + // feeBps/restrictFeeRecipients. + uint24 maxMaxTotalMintableByWallet = retrieved.maxMaxTotalMintableByWallet; + retrieved.maxMaxTotalMintableByWallet = maxMaxTotalMintableByWallet > 0 ? maxMaxTotalMintableByWallet : 1; + retrieved.minFeeBps = supplied.minFeeBps; + retrieved.maxFeeBps = supplied.maxFeeBps; + supplied = retrieved; + } + + // Update the signed mint validation params. + ISeaDrop(seaDropImpl).updateSignedMintValidationParams(signer, supplied); + } + + /** + * @notice Update the allowed payers for this nft contract on SeaDrop. + * Only the owner or administrator can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param payer The payer to update. + * @param allowed Whether the payer is allowed. + */ + function updatePayer(address seaDropImpl, address payer, bool allowed) + external + virtual + override + onlyOwnerOrAdministrator + { + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the payer. + ISeaDrop(seaDropImpl).updatePayer(payer, allowed); + } +} diff --git a/src/seadrop/src/ERC721SeaDrop.sol b/src/seadrop/src/ERC721SeaDrop.sol new file mode 100644 index 0000000..9d62e32 --- /dev/null +++ b/src/seadrop/src/ERC721SeaDrop.sol @@ -0,0 +1,597 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {ERC721ContractMetadata, ISeaDropTokenContractMetadata} from "./ERC721ContractMetadata.sol"; + +import {INonFungibleSeaDropToken} from "./interfaces/INonFungibleSeaDropToken.sol"; + +import {ISeaDrop} from "./interfaces/ISeaDrop.sol"; + +import {AllowListData, PublicDrop, TokenGatedDropStage, SignedMintValidationParams} from "./lib/SeaDropStructs.sol"; + +import {ERC721SeaDropStructsErrorsAndEvents} from "./lib/ERC721SeaDropStructsErrorsAndEvents.sol"; + +import {ERC721A} from "ERC721A/ERC721A.sol"; + +import {ReentrancyGuard} from "openzeppelin-contracts/security/ReentrancyGuard.sol"; + +import {IERC165} from "openzeppelin-contracts/utils/introspection/IERC165.sol"; + +/** + * @title ERC721SeaDrop + * @author James Wenzel (emo.eth) + * @author Ryan Ghods (ralxz.eth) + * @author Stephan Min (stephanm.eth) + * @author Michael Cohen (notmichael.eth) + * @notice ERC721SeaDrop is a token contract that contains methods + * to properly interact with SeaDrop. + */ +contract ERC721SeaDrop is + ERC721ContractMetadata, + INonFungibleSeaDropToken, + ERC721SeaDropStructsErrorsAndEvents, + ReentrancyGuard +{ + /// @notice Track the allowed SeaDrop addresses. + mapping(address => bool) internal _allowedSeaDrop; + + /// @notice Track the enumerated allowed SeaDrop addresses. + address[] internal _enumeratedAllowedSeaDrop; + + /** + * @dev Reverts if not an allowed SeaDrop contract. + * This function is inlined instead of being a modifier + * to save contract space from being inlined N times. + * + * @param seaDrop The SeaDrop address to check if allowed. + */ + function _onlyAllowedSeaDrop(address seaDrop) internal view { + if (_allowedSeaDrop[seaDrop] != true) { + revert OnlyAllowedSeaDrop(); + } + } + + /** + * @notice Deploy the token contract with its name, symbol, + * and allowed SeaDrop addresses. + */ + constructor(string memory name, string memory symbol, address[] memory allowedSeaDrop) + ERC721ContractMetadata(name, symbol) + { + // Put the length on the stack for more efficient access. + uint256 allowedSeaDropLength = allowedSeaDrop.length; + + // Set the mapping for allowed SeaDrop contracts. + for (uint256 i = 0; i < allowedSeaDropLength;) { + _allowedSeaDrop[allowedSeaDrop[i]] = true; + unchecked { + ++i; + } + } + + // Set the enumeration. + _enumeratedAllowedSeaDrop = allowedSeaDrop; + + // Emit an event noting the contract deployment. + emit SeaDropTokenDeployed(); + } + + /** + * @notice Update the allowed SeaDrop contracts. + * Only the owner or administrator can use this function. + * + * @param allowedSeaDrop The allowed SeaDrop addresses. + */ + function updateAllowedSeaDrop(address[] calldata allowedSeaDrop) external virtual override onlyOwner { + _updateAllowedSeaDrop(allowedSeaDrop); + } + + /** + * @notice Internal function to update the allowed SeaDrop contracts. + * + * @param allowedSeaDrop The allowed SeaDrop addresses. + */ + function _updateAllowedSeaDrop(address[] calldata allowedSeaDrop) internal { + // Put the length on the stack for more efficient access. + uint256 enumeratedAllowedSeaDropLength = _enumeratedAllowedSeaDrop.length; + uint256 allowedSeaDropLength = allowedSeaDrop.length; + + // Reset the old mapping. + for (uint256 i = 0; i < enumeratedAllowedSeaDropLength;) { + _allowedSeaDrop[_enumeratedAllowedSeaDrop[i]] = false; + unchecked { + ++i; + } + } + + // Set the new mapping for allowed SeaDrop contracts. + for (uint256 i = 0; i < allowedSeaDropLength;) { + _allowedSeaDrop[allowedSeaDrop[i]] = true; + unchecked { + ++i; + } + } + + // Set the enumeration. + _enumeratedAllowedSeaDrop = allowedSeaDrop; + + // Emit an event for the update. + emit AllowedSeaDropUpdated(allowedSeaDrop); + } + + /** + * @dev Overrides the `_startTokenId` function from ERC721A + * to start at token id `1`. + * + * This is to avoid future possible problems since `0` is usually + * used to signal values that have not been set or have been removed. + */ + function _startTokenId() internal view virtual override returns (uint256) { + return 1; + } + + /** + * @dev Overrides the `tokenURI()` function from ERC721A + * to return just the base URI if it is implied to not be a directory. + * + * This is to help with ERC721 contracts in which the same token URI + * is desired for each token, such as when the tokenURI is 'unrevealed'. + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + + string memory baseURI = _baseURI(); + + // Exit early if the baseURI is empty. + if (bytes(baseURI).length == 0) { + return ""; + } + + // Check if the last character in baseURI is a slash. + if (bytes(baseURI)[bytes(baseURI).length - 1] != bytes("/")[0]) { + return baseURI; + } + + return string(abi.encodePacked(baseURI, _toString(tokenId))); + } + + /** + * @notice Mint tokens, restricted to the SeaDrop contract. + * + * @dev NOTE: If a token registers itself with multiple SeaDrop + * contracts, the implementation of this function should guard + * against reentrancy. If the implementing token uses + * _safeMint(), or a feeRecipient with a malicious receive() hook + * is specified, the token or fee recipients may be able to execute + * another mint in the same transaction via a separate SeaDrop + * contract. + * This is dangerous if an implementing token does not correctly + * update the minterNumMinted and currentTotalSupply values before + * transferring minted tokens, as SeaDrop references these values + * to enforce token limits on a per-wallet and per-stage basis. + * + * ERC721A tracks these values automatically, but this note and + * nonReentrant modifier are left here to encourage best-practices + * when referencing this contract. + * + * @param minter The address to mint to. + * @param quantity The number of tokens to mint. + */ + function mintSeaDrop(address minter, uint256 quantity) external virtual override nonReentrant { + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(msg.sender); + + // Extra safety check to ensure the max supply is not exceeded. + if (_totalMinted() + quantity > maxSupply()) { + revert MintQuantityExceedsMaxSupply(_totalMinted() + quantity, maxSupply()); + } + + // Mint the quantity of tokens to the minter. + _safeMint(minter, quantity); + } + + /** + * @notice Update the public drop data for this nft contract on SeaDrop. + * Only the owner can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param publicDrop The public drop data. + */ + function updatePublicDrop(address seaDropImpl, PublicDrop calldata publicDrop) external virtual override { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the public drop data on SeaDrop. + ISeaDrop(seaDropImpl).updatePublicDrop(publicDrop); + } + + /** + * @notice Update the allow list data for this nft contract on SeaDrop. + * Only the owner can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param allowListData The allow list data. + */ + function updateAllowList(address seaDropImpl, AllowListData calldata allowListData) external virtual override { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the allow list on SeaDrop. + ISeaDrop(seaDropImpl).updateAllowList(allowListData); + } + + /** + * @notice Update the token gated drop stage data for this nft contract + * on SeaDrop. + * Only the owner can use this function. + * + * Note: If two INonFungibleSeaDropToken tokens are doing + * simultaneous token gated drop promotions for each other, + * they can be minted by the same actor until + * `maxTokenSupplyForStage` is reached. Please ensure the + * `allowedNftToken` is not running an active drop during the + * `dropStage` time period. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param allowedNftToken The allowed nft token. + * @param dropStage The token gated drop stage data. + */ + function updateTokenGatedDrop(address seaDropImpl, address allowedNftToken, TokenGatedDropStage calldata dropStage) + external + virtual + override + { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the token gated drop stage. + ISeaDrop(seaDropImpl).updateTokenGatedDrop(allowedNftToken, dropStage); + } + + /** + * @notice Update the drop URI for this nft contract on SeaDrop. + * Only the owner can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param dropURI The new drop URI. + */ + function updateDropURI(address seaDropImpl, string calldata dropURI) external virtual override { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the drop URI. + ISeaDrop(seaDropImpl).updateDropURI(dropURI); + } + + /** + * @notice Update the creator payout address for this nft contract on + * SeaDrop. + * Only the owner can set the creator payout address. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param payoutAddress The new payout address. + */ + function updateCreatorPayoutAddress(address seaDropImpl, address payoutAddress) external { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the creator payout address. + ISeaDrop(seaDropImpl).updateCreatorPayoutAddress(payoutAddress); + } + + /** + * @notice Update the allowed fee recipient for this nft contract + * on SeaDrop. + * Only the owner can set the allowed fee recipient. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param feeRecipient The new fee recipient. + * @param allowed If the fee recipient is allowed. + */ + function updateAllowedFeeRecipient(address seaDropImpl, address feeRecipient, bool allowed) external virtual { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the allowed fee recipient. + ISeaDrop(seaDropImpl).updateAllowedFeeRecipient(feeRecipient, allowed); + } + + /** + * @notice Update the server-side signers for this nft contract + * on SeaDrop. + * Only the owner can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param signer The signer to update. + * @param signedMintValidationParams Minimum and maximum parameters to + * enforce for signed mints. + */ + function updateSignedMintValidationParams( + address seaDropImpl, + address signer, + SignedMintValidationParams memory signedMintValidationParams + ) external virtual override { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the signer. + ISeaDrop(seaDropImpl).updateSignedMintValidationParams(signer, signedMintValidationParams); + } + + /** + * @notice Update the allowed payers for this nft contract on SeaDrop. + * Only the owner can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param payer The payer to update. + * @param allowed Whether the payer is allowed. + */ + function updatePayer(address seaDropImpl, address payer, bool allowed) external virtual override { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the payer. + ISeaDrop(seaDropImpl).updatePayer(payer, allowed); + } + + /** + * @notice Returns a set of mint stats for the address. + * This assists SeaDrop in enforcing maxSupply, + * maxTotalMintableByWallet, and maxTokenSupplyForStage checks. + * + * @dev NOTE: Implementing contracts should always update these numbers + * before transferring any tokens with _safeMint() to mitigate + * consequences of malicious onERC721Received() hooks. + * + * @param minter The minter address. + */ + function getMintStats(address minter) + external + view + override + returns (uint256 minterNumMinted, uint256 currentTotalSupply, uint256 maxSupply) + { + minterNumMinted = _numberMinted(minter); + currentTotalSupply = _totalMinted(); + maxSupply = _maxSupply; + } + + /** + * @notice Returns whether the interface is supported. + * + * @param interfaceId The interface id to check against. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(IERC165, ERC721ContractMetadata) + returns (bool) + { + return interfaceId == type(INonFungibleSeaDropToken).interfaceId + || interfaceId == type(ISeaDropTokenContractMetadata).interfaceId + // ERC721ContractMetadata returns supportsInterface true for + // EIP-2981 + // ERC721A returns supportsInterface true for + // ERC165, ERC721, ERC721Metadata + || super.supportsInterface(interfaceId); + } + + /** + * @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. + * - The `operator` must be allowed. + * + * Emits an {ApprovalForAll} event. + */ + function setApprovalForAll(address operator, bool approved) public override { + super.setApprovalForAll(operator, approved); + } + + /** + * @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. + * - The `operator` mut be allowed. + * + * Emits an {Approval} event. + */ + function approve(address operator, uint256 tokenId) public override { + super.approve(operator, tokenId); + } + + /** + * @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}. + * - The operator must be allowed. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 tokenId) public override { + super.transferFrom(from, to, tokenId); + } + + /** + * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. + */ + function safeTransferFrom(address from, address to, uint256 tokenId) public override { + super.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. + * - The operator must be allowed. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public override { + super.safeTransferFrom(from, to, tokenId, data); + } + + /** + * @notice Configure multiple properties at a time. + * + * Note: The individual configure methods should be used + * to unset or reset any properties to zero, as this method + * will ignore zero-value properties in the config struct. + * + * @param config The configuration struct. + */ + function multiConfigure(MultiConfigureStruct calldata config) external onlyOwner { + if (config.maxSupply > 0) { + this.setMaxSupply(config.maxSupply); + } + if (bytes(config.baseURI).length != 0) { + this.setBaseURI(config.baseURI); + } + if (bytes(config.contractURI).length != 0) { + this.setContractURI(config.contractURI); + } + if (_cast(config.publicDrop.startTime != 0) | _cast(config.publicDrop.endTime != 0) == 1) { + this.updatePublicDrop(config.seaDropImpl, config.publicDrop); + } + if (bytes(config.dropURI).length != 0) { + this.updateDropURI(config.seaDropImpl, config.dropURI); + } + if (config.allowListData.merkleRoot != bytes32(0)) { + this.updateAllowList(config.seaDropImpl, config.allowListData); + } + if (config.creatorPayoutAddress != address(0)) { + this.updateCreatorPayoutAddress(config.seaDropImpl, config.creatorPayoutAddress); + } + if (config.provenanceHash != bytes32(0)) { + this.setProvenanceHash(config.provenanceHash); + } + if (config.allowedFeeRecipients.length > 0) { + for (uint256 i = 0; i < config.allowedFeeRecipients.length;) { + this.updateAllowedFeeRecipient(config.seaDropImpl, config.allowedFeeRecipients[i], true); + unchecked { + ++i; + } + } + } + if (config.disallowedFeeRecipients.length > 0) { + for (uint256 i = 0; i < config.disallowedFeeRecipients.length;) { + this.updateAllowedFeeRecipient(config.seaDropImpl, config.disallowedFeeRecipients[i], false); + unchecked { + ++i; + } + } + } + if (config.allowedPayers.length > 0) { + for (uint256 i = 0; i < config.allowedPayers.length;) { + this.updatePayer(config.seaDropImpl, config.allowedPayers[i], true); + unchecked { + ++i; + } + } + } + if (config.disallowedPayers.length > 0) { + for (uint256 i = 0; i < config.disallowedPayers.length;) { + this.updatePayer(config.seaDropImpl, config.disallowedPayers[i], false); + unchecked { + ++i; + } + } + } + if (config.tokenGatedDropStages.length > 0) { + if (config.tokenGatedDropStages.length != config.tokenGatedAllowedNftTokens.length) { + revert TokenGatedMismatch(); + } + for (uint256 i = 0; i < config.tokenGatedDropStages.length;) { + this.updateTokenGatedDrop( + config.seaDropImpl, config.tokenGatedAllowedNftTokens[i], config.tokenGatedDropStages[i] + ); + unchecked { + ++i; + } + } + } + if (config.disallowedTokenGatedAllowedNftTokens.length > 0) { + for (uint256 i = 0; i < config.disallowedTokenGatedAllowedNftTokens.length;) { + TokenGatedDropStage memory emptyStage; + this.updateTokenGatedDrop( + config.seaDropImpl, config.disallowedTokenGatedAllowedNftTokens[i], emptyStage + ); + unchecked { + ++i; + } + } + } + if (config.signedMintValidationParams.length > 0) { + if (config.signedMintValidationParams.length != config.signers.length) { + revert SignersMismatch(); + } + for (uint256 i = 0; i < config.signedMintValidationParams.length;) { + this.updateSignedMintValidationParams( + config.seaDropImpl, config.signers[i], config.signedMintValidationParams[i] + ); + unchecked { + ++i; + } + } + } + if (config.disallowedSigners.length > 0) { + for (uint256 i = 0; i < config.disallowedSigners.length;) { + SignedMintValidationParams memory emptyParams; + this.updateSignedMintValidationParams(config.seaDropImpl, config.disallowedSigners[i], emptyParams); + unchecked { + ++i; + } + } + } + } +} diff --git a/src/seadrop/src/SeaDrop.sol b/src/seadrop/src/SeaDrop.sol new file mode 100644 index 0000000..a3294a3 --- /dev/null +++ b/src/seadrop/src/SeaDrop.sol @@ -0,0 +1,1154 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {ISeaDrop} from "./interfaces/ISeaDrop.sol"; + +import {INonFungibleSeaDropToken} from "./interfaces/INonFungibleSeaDropToken.sol"; + +import { + AllowListData, + MintParams, + PublicDrop, + TokenGatedDropStage, + TokenGatedMintParams, + SignedMintValidationParams +} from "./lib/SeaDropStructs.sol"; + +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; + +import {ReentrancyGuard} from "openzeppelin-contracts/security/ReentrancyGuard.sol"; + +import {IERC721} from "openzeppelin-contracts/token/ERC721/IERC721.sol"; + +import {IERC165} from "openzeppelin-contracts/utils/introspection/IERC165.sol"; + +import {ECDSA} from "openzeppelin-contracts/utils/cryptography/ECDSA.sol"; + +import {MerkleProof} from "openzeppelin-contracts/utils/cryptography/MerkleProof.sol"; + +/** + * @title SeaDrop + * @author James Wenzel (emo.eth) + * @author Ryan Ghods (ralxz.eth) + * @author Stephan Min (stephanm.eth) + * @notice SeaDrop is a contract to help facilitate ERC721 token drops + * with functionality for public, allow list, server-side signed, + * and token-gated drops. + */ +contract SeaDrop is ISeaDrop, ReentrancyGuard { + using ECDSA for bytes32; + + /// @notice Track the public drops. + mapping(address => PublicDrop) private _publicDrops; + + /// @notice Track the creator payout addresses. + mapping(address => address) private _creatorPayoutAddresses; + + /// @notice Track the allow list merkle roots. + mapping(address => bytes32) private _allowListMerkleRoots; + + /// @notice Track the allowed fee recipients. + mapping(address => mapping(address => bool)) private _allowedFeeRecipients; + + /// @notice Track the enumerated allowed fee recipients. + mapping(address => address[]) private _enumeratedFeeRecipients; + + /// @notice Track the parameters for allowed signers for server-side drops. + mapping(address => mapping(address => SignedMintValidationParams)) private _signedMintValidationParams; + + /// @notice Track the signers for each server-side drop. + mapping(address => address[]) private _enumeratedSigners; + + /// @notice Track the used signature digests. + mapping(bytes32 => bool) private _usedDigests; + + /// @notice Track the allowed payers. + mapping(address => mapping(address => bool)) private _allowedPayers; + + /// @notice Track the enumerated allowed payers. + mapping(address => address[]) private _enumeratedPayers; + + /// @notice Track the token gated drop stages. + mapping(address => mapping(address => TokenGatedDropStage)) private _tokenGatedDrops; + + /// @notice Track the tokens for token gated drops. + mapping(address => address[]) private _enumeratedTokenGatedTokens; + + /// @notice Track the redeemed token IDs for token gated drop stages. + mapping(address => mapping(address => mapping(uint256 => bool))) private _tokenGatedRedeemed; + + /// @notice Internal constants for EIP-712: Typed structured + /// data hashing and signing + bytes32 internal constant _SIGNED_MINT_TYPEHASH = + // prettier-ignore + keccak256( + "SignedMint(" "address nftContract," "address minter," "address feeRecipient," "MintParams mintParams," + "uint256 salt" ")" "MintParams(" "uint256 mintPrice," "uint256 maxTotalMintableByWallet," "uint256 startTime," + "uint256 endTime," "uint256 dropStageIndex," "uint256 maxTokenSupplyForStage," "uint256 feeBps," + "bool restrictFeeRecipients" ")" + ); + bytes32 internal constant _MINT_PARAMS_TYPEHASH = + // prettier-ignore + keccak256( + "MintParams(" "uint256 mintPrice," "uint256 maxTotalMintableByWallet," "uint256 startTime," "uint256 endTime," + "uint256 dropStageIndex," "uint256 maxTokenSupplyForStage," "uint256 feeBps," "bool restrictFeeRecipients" ")" + ); + bytes32 internal constant _EIP_712_DOMAIN_TYPEHASH = + // prettier-ignore + keccak256("EIP712Domain(" "string name," "string version," "uint256 chainId," "address verifyingContract" ")"); + bytes32 internal constant _NAME_HASH = keccak256("SeaDrop"); + bytes32 internal constant _VERSION_HASH = keccak256("1.0"); + uint256 internal immutable _CHAIN_ID = block.chainid; + bytes32 internal immutable _DOMAIN_SEPARATOR; + + /// @notice Constant for an unlimited `maxTokenSupplyForStage`. + /// Used in `mintPublic` where no `maxTokenSupplyForStage` + /// is stored in the `PublicDrop` struct. + uint256 internal constant _UNLIMITED_MAX_TOKEN_SUPPLY_FOR_STAGE = type(uint256).max; + + /// @notice Constant for a public mint's `dropStageIndex`. + /// Used in `mintPublic` where no `dropStageIndex` + /// is stored in the `PublicDrop` struct. + uint256 internal constant _PUBLIC_DROP_STAGE_INDEX = 0; + + /** + * @notice Ensure only tokens implementing INonFungibleSeaDropToken can + * call the update methods. + */ + modifier onlyINonFungibleSeaDropToken() virtual { + if (!IERC165(msg.sender).supportsInterface(type(INonFungibleSeaDropToken).interfaceId)) { + revert OnlyINonFungibleSeaDropToken(msg.sender); + } + _; + } + + /** + * @notice Constructor for the contract deployment. + */ + constructor() { + // Derive the domain separator. + _DOMAIN_SEPARATOR = _deriveDomainSeparator(); + } + + /** + * @notice Mint a public drop. + * + * @param nftContract The nft contract to mint. + * @param feeRecipient The fee recipient. + * @param minterIfNotPayer The mint recipient if different than the payer. + * @param quantity The number of tokens to mint. + */ + function mintPublic(address nftContract, address feeRecipient, address minterIfNotPayer, uint256 quantity) + external + payable + override + { + // Get the public drop data. + PublicDrop memory publicDrop = _publicDrops[nftContract]; + + // Ensure that the drop has started. + _checkActive(publicDrop.startTime, publicDrop.endTime); + + // Put the mint price on the stack. + uint256 mintPrice = publicDrop.mintPrice; + + // Validate payment is correct for number minted. + _checkCorrectPayment(quantity, mintPrice); + + // Get the minter address. + address minter = minterIfNotPayer != address(0) ? minterIfNotPayer : msg.sender; + + // Ensure the payer is allowed if not the minter. + if (minter != msg.sender) { + if (!_allowedPayers[nftContract][msg.sender]) { + revert PayerNotAllowed(); + } + } + + // Check that the minter is allowed to mint the desired quantity. + _checkMintQuantity( + nftContract, minter, quantity, publicDrop.maxTotalMintableByWallet, _UNLIMITED_MAX_TOKEN_SUPPLY_FOR_STAGE + ); + + // Check that the fee recipient is allowed if restricted. + _checkFeeRecipientIsAllowed(nftContract, feeRecipient, publicDrop.restrictFeeRecipients); + + // Mint the token(s), split the payout, emit an event. + _mintAndPay(nftContract, minter, quantity, mintPrice, _PUBLIC_DROP_STAGE_INDEX, publicDrop.feeBps, feeRecipient); + } + + /** + * @notice Mint from an allow list. + * + * @param nftContract The nft contract to mint. + * @param feeRecipient The fee recipient. + * @param minterIfNotPayer The mint recipient if different than the payer. + * @param quantity The number of tokens to mint. + * @param mintParams The mint parameters. + * @param proof The proof for the leaf of the allow list. + */ + function mintAllowList( + address nftContract, + address feeRecipient, + address minterIfNotPayer, + uint256 quantity, + MintParams calldata mintParams, + bytes32[] calldata proof + ) external payable override { + // Check that the drop stage is active. + _checkActive(mintParams.startTime, mintParams.endTime); + + // Put the mint price on the stack. + uint256 mintPrice = mintParams.mintPrice; + + // Validate payment is correct for number minted. + _checkCorrectPayment(quantity, mintPrice); + + // Get the minter address. + address minter = minterIfNotPayer != address(0) ? minterIfNotPayer : msg.sender; + + // Ensure the payer is allowed if not the minter. + if (minter != msg.sender) { + if (!_allowedPayers[nftContract][msg.sender]) { + revert PayerNotAllowed(); + } + } + + // Check that the minter is allowed to mint the desired quantity. + _checkMintQuantity( + nftContract, minter, quantity, mintParams.maxTotalMintableByWallet, mintParams.maxTokenSupplyForStage + ); + + // Check that the fee recipient is allowed if restricted. + _checkFeeRecipientIsAllowed(nftContract, feeRecipient, mintParams.restrictFeeRecipients); + + // Verify the proof. + if (!MerkleProof.verify(proof, _allowListMerkleRoots[nftContract], keccak256(abi.encode(minter, mintParams)))) { + revert InvalidProof(); + } + + // Mint the token(s), split the payout, emit an event. + _mintAndPay( + nftContract, minter, quantity, mintPrice, mintParams.dropStageIndex, mintParams.feeBps, feeRecipient + ); + } + + /** + * @notice Mint with a server-side signature. + * Note that a signature can only be used once. + * + * @param nftContract The nft contract to mint. + * @param feeRecipient The fee recipient. + * @param minterIfNotPayer The mint recipient if different than the payer. + * @param quantity The number of tokens to mint. + * @param mintParams The mint parameters. + * @param salt The salt for the signed mint. + * @param signature The server-side signature, must be an allowed + * signer. + */ + function mintSigned( + address nftContract, + address feeRecipient, + address minterIfNotPayer, + uint256 quantity, + MintParams calldata mintParams, + uint256 salt, + bytes calldata signature + ) external payable override { + // Check that the drop stage is active. + _checkActive(mintParams.startTime, mintParams.endTime); + + // Validate payment is correct for number minted. + _checkCorrectPayment(quantity, mintParams.mintPrice); + + // Get the minter address. + address minter = minterIfNotPayer != address(0) ? minterIfNotPayer : msg.sender; + + // Ensure the payer is allowed if not the minter. + if (minter != msg.sender) { + if (!_allowedPayers[nftContract][msg.sender]) { + revert PayerNotAllowed(); + } + } + + // Check that the minter is allowed to mint the desired quantity. + _checkMintQuantity( + nftContract, minter, quantity, mintParams.maxTotalMintableByWallet, mintParams.maxTokenSupplyForStage + ); + + // Check that the fee recipient is allowed if restricted. + _checkFeeRecipientIsAllowed(nftContract, feeRecipient, mintParams.restrictFeeRecipients); + + // Validate the signature in a block scope to avoid "stack too deep". + { + // Get the digest to verify the EIP-712 signature. + bytes32 digest = _getDigest(nftContract, minter, feeRecipient, mintParams, salt); + + // Ensure the digest has not already been used. + if (_usedDigests[digest]) { + revert SignatureAlreadyUsed(); + } + + // Mark the digest as used. + _usedDigests[digest] = true; + + // Use the recover method to see what address was used to create + // the signature on this data. + // Note that if the digest doesn't exactly match what was signed we'll + // get a random recovered address. + address recoveredAddress = digest.recover(signature); + _validateSignerAndParams(nftContract, mintParams, recoveredAddress); + } + + // Mint the token(s), split the payout, emit an event. + _mintAndPay( + nftContract, + minter, + quantity, + mintParams.mintPrice, + mintParams.dropStageIndex, + mintParams.feeBps, + feeRecipient + ); + } + + /** + * @notice Enforce stored parameters for signed mints to mitigate + * the effects of a malicious signer. + */ + function _validateSignerAndParams(address nftContract, MintParams memory mintParams, address signer) + internal + view + { + SignedMintValidationParams memory signedMintValidationParams = _signedMintValidationParams[nftContract][signer]; + + // Check that SignedMintValidationParams have been initialized; if not, + // this is an invalid signer. + if (signedMintValidationParams.maxMaxTotalMintableByWallet == 0) { + revert InvalidSignature(signer); + } + + // Validate individual params. + if (mintParams.mintPrice < signedMintValidationParams.minMintPrice) { + revert InvalidSignedMintPrice(mintParams.mintPrice, signedMintValidationParams.minMintPrice); + } + if (mintParams.maxTotalMintableByWallet > signedMintValidationParams.maxMaxTotalMintableByWallet) { + revert InvalidSignedMaxTotalMintableByWallet( + mintParams.maxTotalMintableByWallet, signedMintValidationParams.maxMaxTotalMintableByWallet + ); + } + if (mintParams.startTime < signedMintValidationParams.minStartTime) { + revert InvalidSignedStartTime(mintParams.startTime, signedMintValidationParams.minStartTime); + } + if (mintParams.endTime > signedMintValidationParams.maxEndTime) { + revert InvalidSignedEndTime(mintParams.endTime, signedMintValidationParams.maxEndTime); + } + if (mintParams.maxTokenSupplyForStage > signedMintValidationParams.maxMaxTokenSupplyForStage) { + revert InvalidSignedMaxTokenSupplyForStage( + mintParams.maxTokenSupplyForStage, signedMintValidationParams.maxMaxTokenSupplyForStage + ); + } + if (mintParams.feeBps > signedMintValidationParams.maxFeeBps) { + revert InvalidSignedFeeBps(mintParams.feeBps, signedMintValidationParams.maxFeeBps); + } + if (mintParams.feeBps < signedMintValidationParams.minFeeBps) { + revert InvalidSignedFeeBps(mintParams.feeBps, signedMintValidationParams.minFeeBps); + } + if (!mintParams.restrictFeeRecipients) { + revert SignedMintsMustRestrictFeeRecipients(); + } + } + + /** + * @notice Mint as an allowed token holder. + * This will mark the token ids as redeemed and will revert if the + * same token id is attempted to be redeemed twice. + * + * @param nftContract The nft contract to mint. + * @param feeRecipient The fee recipient. + * @param minterIfNotPayer The mint recipient if different than the payer. + * @param mintParams The token gated mint params. + */ + function mintAllowedTokenHolder( + address nftContract, + address feeRecipient, + address minterIfNotPayer, + TokenGatedMintParams calldata mintParams + ) external payable override { + // Get the minter address. + address minter = minterIfNotPayer != address(0) ? minterIfNotPayer : msg.sender; + + // Ensure the payer is allowed if not the minter. + if (minter != msg.sender) { + if (!_allowedPayers[nftContract][msg.sender]) { + revert PayerNotAllowed(); + } + } + + // Put the allowedNftToken on the stack for more efficient access. + address allowedNftToken = mintParams.allowedNftToken; + + // Set the dropStage to a variable. + TokenGatedDropStage memory dropStage = _tokenGatedDrops[nftContract][allowedNftToken]; + + // Validate that the dropStage is active. + _checkActive(dropStage.startTime, dropStage.endTime); + + // Check that the fee recipient is allowed if restricted. + _checkFeeRecipientIsAllowed(nftContract, feeRecipient, dropStage.restrictFeeRecipients); + + // Put the mint quantity on the stack for more efficient access. + uint256 mintQuantity = mintParams.allowedNftTokenIds.length; + + // Validate payment is correct for number minted. + _checkCorrectPayment(mintQuantity, dropStage.mintPrice); + + // Check that the minter is allowed to mint the desired quantity. + _checkMintQuantity( + nftContract, minter, mintQuantity, dropStage.maxTotalMintableByWallet, dropStage.maxTokenSupplyForStage + ); + + // Iterate through each allowedNftTokenId + // to ensure it is not already redeemed. + for (uint256 i = 0; i < mintQuantity;) { + // Put the tokenId on the stack. + uint256 tokenId = mintParams.allowedNftTokenIds[i]; + + // Check that the minter is the owner of the allowedNftTokenId. + if (IERC721(allowedNftToken).ownerOf(tokenId) != minter) { + revert TokenGatedNotTokenOwner(nftContract, allowedNftToken, tokenId); + } + + // Cache the storage pointer for cheaper access. + mapping(uint256 => bool) storage redeemedTokenIds = _tokenGatedRedeemed[nftContract][allowedNftToken]; + + // Check that the token id has not already been redeemed. + if (redeemedTokenIds[tokenId]) { + revert TokenGatedTokenIdAlreadyRedeemed(nftContract, allowedNftToken, tokenId); + } + + // Mark the token id as redeemed. + redeemedTokenIds[tokenId] = true; + + unchecked { + ++i; + } + } + + // Mint the token(s), split the payout, emit an event. + _mintAndPay( + nftContract, + minter, + mintQuantity, + dropStage.mintPrice, + dropStage.dropStageIndex, + dropStage.feeBps, + feeRecipient + ); + } + + /** + * @notice Check that the drop stage is active. + * + * @param startTime The drop stage start time. + * @param endTime The drop stage end time. + */ + function _checkActive(uint256 startTime, uint256 endTime) internal view { + if (_cast(block.timestamp < startTime) | _cast(block.timestamp > endTime) == 1) { + // Revert if the drop stage is not active. + revert NotActive(block.timestamp, startTime, endTime); + } + } + + /** + * @notice Check that the fee recipient is allowed. + * + * @param nftContract The nft contract. + * @param feeRecipient The fee recipient. + * @param restrictFeeRecipients If the fee recipients are restricted. + */ + function _checkFeeRecipientIsAllowed(address nftContract, address feeRecipient, bool restrictFeeRecipients) + internal + view + { + // Ensure the fee recipient is not the zero address. + if (feeRecipient == address(0)) { + revert FeeRecipientCannotBeZeroAddress(); + } + + // Revert if the fee recipient is restricted and not allowed. + if (restrictFeeRecipients) { + if (!_allowedFeeRecipients[nftContract][feeRecipient]) { + revert FeeRecipientNotAllowed(); + } + } + } + + /** + * @notice Check that the wallet is allowed to mint the desired quantity. + * + * @param nftContract The nft contract. + * @param minter The mint recipient. + * @param quantity The number of tokens to mint. + * @param maxTotalMintableByWallet The max allowed mints per wallet. + * @param maxTokenSupplyForStage The max token supply for the drop stage. + */ + function _checkMintQuantity( + address nftContract, + address minter, + uint256 quantity, + uint256 maxTotalMintableByWallet, + uint256 maxTokenSupplyForStage + ) internal view { + // Mint quantity of zero is not valid. + if (quantity == 0) { + revert MintQuantityCannotBeZero(); + } + + // Get the mint stats. + (uint256 minterNumMinted, uint256 currentTotalSupply, uint256 maxSupply) = + INonFungibleSeaDropToken(nftContract).getMintStats(minter); + + // Ensure mint quantity doesn't exceed maxTotalMintableByWallet. + if (quantity + minterNumMinted > maxTotalMintableByWallet) { + revert MintQuantityExceedsMaxMintedPerWallet(quantity + minterNumMinted, maxTotalMintableByWallet); + } + + // Ensure mint quantity doesn't exceed maxSupply. + if (quantity + currentTotalSupply > maxSupply) { + revert MintQuantityExceedsMaxSupply(quantity + currentTotalSupply, maxSupply); + } + + // Ensure mint quantity doesn't exceed maxTokenSupplyForStage. + if (quantity + currentTotalSupply > maxTokenSupplyForStage) { + revert MintQuantityExceedsMaxTokenSupplyForStage(quantity + currentTotalSupply, maxTokenSupplyForStage); + } + } + + /** + * @notice Revert if the payment is not the quantity times the mint price. + * + * @param quantity The number of tokens to mint. + * @param mintPrice The mint price per token. + */ + function _checkCorrectPayment(uint256 quantity, uint256 mintPrice) internal view { + // Revert if the tx's value doesn't match the total cost. + if (msg.value != quantity * mintPrice) { + revert IncorrectPayment(msg.value, quantity * mintPrice); + } + } + + /** + * @notice Split the payment payout for the creator and fee recipient. + * + * @param nftContract The nft contract. + * @param feeRecipient The fee recipient. + * @param feeBps The fee basis points. + */ + function _splitPayout(address nftContract, address feeRecipient, uint256 feeBps) internal { + // Revert if the fee basis points is greater than 10_000. + if (feeBps > 10_000) { + revert InvalidFeeBps(feeBps); + } + + // Get the creator payout address. + address creatorPayoutAddress = _creatorPayoutAddresses[nftContract]; + + // Ensure the creator payout address is not the zero address. + if (creatorPayoutAddress == address(0)) { + revert CreatorPayoutAddressCannotBeZeroAddress(); + } + + // msg.value has already been validated by this point, so can use it directly. + + // If the fee is zero, just transfer to the creator and return. + if (feeBps == 0) { + SafeTransferLib.safeTransferETH(creatorPayoutAddress, msg.value); + return; + } + + // Get the fee amount. + // Note that the fee amount is rounded down in favor of the creator. + uint256 feeAmount = (msg.value * feeBps) / 10_000; + + // Get the creator payout amount. Fee amount is <= msg.value per above. + uint256 payoutAmount; + unchecked { + payoutAmount = msg.value - feeAmount; + } + + // Transfer the fee amount to the fee recipient. + if (feeAmount > 0) { + SafeTransferLib.safeTransferETH(feeRecipient, feeAmount); + } + + // Transfer the creator payout amount to the creator. + SafeTransferLib.safeTransferETH(creatorPayoutAddress, payoutAmount); + } + + /** + * @notice Mints a number of tokens, splits the payment, + * and emits an event. + * + * @param nftContract The nft contract. + * @param minter The mint recipient. + * @param quantity The number of tokens to mint. + * @param mintPrice The mint price per token. + * @param dropStageIndex The drop stage index. + * @param feeBps The fee basis points. + * @param feeRecipient The fee recipient. + */ + function _mintAndPay( + address nftContract, + address minter, + uint256 quantity, + uint256 mintPrice, + uint256 dropStageIndex, + uint256 feeBps, + address feeRecipient + ) internal nonReentrant { + // Mint the token(s). + INonFungibleSeaDropToken(nftContract).mintSeaDrop(minter, quantity); + + if (mintPrice != 0) { + // Split the payment between the creator and fee recipient. + _splitPayout(nftContract, feeRecipient, feeBps); + } + + // Emit an event for the mint. + emit SeaDropMint(nftContract, minter, feeRecipient, msg.sender, quantity, mintPrice, feeBps, dropStageIndex); + } + + /** + * @dev Internal view function to get the EIP-712 domain separator. If the + * chainId matches the chainId set on deployment, the cached domain + * separator will be returned; otherwise, it will be derived from + * scratch. + * + * @return The domain separator. + */ + function _domainSeparator() internal view returns (bytes32) { + // prettier-ignore + return block.chainid == _CHAIN_ID ? _DOMAIN_SEPARATOR : _deriveDomainSeparator(); + } + + /** + * @dev Internal view function to derive the EIP-712 domain separator. + * + * @return The derived domain separator. + */ + function _deriveDomainSeparator() internal view returns (bytes32) { + // prettier-ignore + return keccak256(abi.encode(_EIP_712_DOMAIN_TYPEHASH, _NAME_HASH, _VERSION_HASH, block.chainid, address(this))); + } + + /** + * @notice Returns the public drop data for the nft contract. + * + * @param nftContract The nft contract. + */ + function getPublicDrop(address nftContract) external view returns (PublicDrop memory) { + return _publicDrops[nftContract]; + } + + /** + * @notice Returns the creator payout address for the nft contract. + * + * @param nftContract The nft contract. + */ + function getCreatorPayoutAddress(address nftContract) external view returns (address) { + return _creatorPayoutAddresses[nftContract]; + } + + /** + * @notice Returns the allow list merkle root for the nft contract. + * + * @param nftContract The nft contract. + */ + function getAllowListMerkleRoot(address nftContract) external view returns (bytes32) { + return _allowListMerkleRoots[nftContract]; + } + + /** + * @notice Returns if the specified fee recipient is allowed + * for the nft contract. + * + * @param nftContract The nft contract. + */ + function getFeeRecipientIsAllowed(address nftContract, address feeRecipient) external view returns (bool) { + return _allowedFeeRecipients[nftContract][feeRecipient]; + } + + /** + * @notice Returns an enumeration of allowed fee recipients for an + * nft contract when fee recipients are enforced. + * + * @param nftContract The nft contract. + */ + function getAllowedFeeRecipients(address nftContract) external view returns (address[] memory) { + return _enumeratedFeeRecipients[nftContract]; + } + + /** + * @notice Returns the server-side signers for the nft contract. + * + * @param nftContract The nft contract. + */ + function getSigners(address nftContract) external view returns (address[] memory) { + return _enumeratedSigners[nftContract]; + } + + /** + * @notice Returns the struct of SignedMintValidationParams for a signer. + * + * @param nftContract The nft contract. + * @param signer The signer. + */ + function getSignedMintValidationParams(address nftContract, address signer) + external + view + returns (SignedMintValidationParams memory) + { + return _signedMintValidationParams[nftContract][signer]; + } + + /** + * @notice Returns the payers for the nft contract. + * + * @param nftContract The nft contract. + */ + function getPayers(address nftContract) external view returns (address[] memory) { + return _enumeratedPayers[nftContract]; + } + + /** + * @notice Returns if the specified payer is allowed + * for the nft contract. + * + * @param nftContract The nft contract. + * @param payer The payer. + */ + function getPayerIsAllowed(address nftContract, address payer) external view returns (bool) { + return _allowedPayers[nftContract][payer]; + } + + /** + * @notice Returns the allowed token gated drop tokens for the nft contract. + * + * @param nftContract The nft contract. + */ + function getTokenGatedAllowedTokens(address nftContract) external view returns (address[] memory) { + return _enumeratedTokenGatedTokens[nftContract]; + } + + /** + * @notice Returns the token gated drop data for the nft contract + * and token gated nft. + * + * @param nftContract The nft contract. + * @param allowedNftToken The token gated nft token. + */ + function getTokenGatedDrop(address nftContract, address allowedNftToken) + external + view + returns (TokenGatedDropStage memory) + { + return _tokenGatedDrops[nftContract][allowedNftToken]; + } + + /** + * @notice Returns whether the token id for a token gated drop has been + * redeemed. + * + * @param nftContract The nft contract. + * @param allowedNftToken The token gated nft token. + * @param allowedNftTokenId The token gated nft token id to check. + */ + function getAllowedNftTokenIdIsRedeemed(address nftContract, address allowedNftToken, uint256 allowedNftTokenId) + external + view + returns (bool) + { + return _tokenGatedRedeemed[nftContract][allowedNftToken][allowedNftTokenId]; + } + + /** + * @notice Emits an event to notify update of the drop URI. + * + * This method assume msg.sender is an nft contract and its + * ERC165 interface id matches INonFungibleSeaDropToken. + * + * Note: Be sure only authorized users can call this from + * token contracts that implement INonFungibleSeaDropToken. + * + * @param dropURI The new drop URI. + */ + function updateDropURI(string calldata dropURI) external onlyINonFungibleSeaDropToken { + // Emit an event with the update. + emit DropURIUpdated(msg.sender, dropURI); + } + + /** + * @notice Updates the public drop data for the nft contract + * and emits an event. + * + * This method assume msg.sender is an nft contract and its + * ERC165 interface id matches INonFungibleSeaDropToken. + * + * Note: Be sure only authorized users can call this from + * token contracts that implement INonFungibleSeaDropToken. + * + * @param publicDrop The public drop data. + */ + function updatePublicDrop(PublicDrop calldata publicDrop) external override onlyINonFungibleSeaDropToken { + // Revert if the fee basis points is greater than 10_000. + if (publicDrop.feeBps > 10_000) { + revert InvalidFeeBps(publicDrop.feeBps); + } + + // Set the public drop data. + _publicDrops[msg.sender] = publicDrop; + + // Emit an event with the update. + emit PublicDropUpdated(msg.sender, publicDrop); + } + + /** + * @notice Updates the allow list merkle root for the nft contract + * and emits an event. + * + * This method assume msg.sender is an nft contract and its + * ERC165 interface id matches INonFungibleSeaDropToken. + * + * Note: Be sure only authorized users can call this from + * token contracts that implement INonFungibleSeaDropToken. + * + * @param allowListData The allow list data. + */ + function updateAllowList(AllowListData calldata allowListData) external override onlyINonFungibleSeaDropToken { + // Track the previous root. + bytes32 prevRoot = _allowListMerkleRoots[msg.sender]; + + // Update the merkle root. + _allowListMerkleRoots[msg.sender] = allowListData.merkleRoot; + + // Emit an event with the update. + emit AllowListUpdated( + msg.sender, prevRoot, allowListData.merkleRoot, allowListData.publicKeyURIs, allowListData.allowListURI + ); + } + + /** + * @notice Updates the token gated drop stage for the nft contract + * and emits an event. + * + * This method assume msg.sender is an nft contract and its + * ERC165 interface id matches INonFungibleSeaDropToken. + * + * Note: Be sure only authorized users can call this from + * token contracts that implement INonFungibleSeaDropToken. + * + * Note: If two INonFungibleSeaDropToken tokens are doing + * simultaneous token gated drop promotions for each other, + * they can be minted by the same actor until + * `maxTokenSupplyForStage` is reached. Please ensure the + * `allowedNftToken` is not running an active drop during + * the `dropStage` time period. + * + * @param allowedNftToken The token gated nft token. + * @param dropStage The token gated drop stage data. + */ + function updateTokenGatedDrop(address allowedNftToken, TokenGatedDropStage calldata dropStage) + external + override + onlyINonFungibleSeaDropToken + { + // Ensure the allowedNftToken is not the zero address. + if (allowedNftToken == address(0)) { + revert TokenGatedDropAllowedNftTokenCannotBeZeroAddress(); + } + + // Ensure the allowedNftToken cannot be the drop token itself. + if (allowedNftToken == msg.sender) { + revert TokenGatedDropAllowedNftTokenCannotBeDropToken(); + } + + // Revert if the fee basis points is greater than 10_000. + if (dropStage.feeBps > 10_000) { + revert InvalidFeeBps(dropStage.feeBps); + } + + // Use maxTotalMintableByWallet != 0 as a signal that this update should + // add or update the drop stage, otherwise we will be removing. + bool addOrUpdateDropStage = dropStage.maxTotalMintableByWallet != 0; + + // Get pointers to the token gated drop data and enumerated addresses. + TokenGatedDropStage storage existingDropStageData = _tokenGatedDrops[msg.sender][allowedNftToken]; + address[] storage enumeratedTokens = _enumeratedTokenGatedTokens[msg.sender]; + + // Stage struct packs to a single slot, so load it + // as a uint256; if it is 0, it is empty. + bool dropStageDoesNotExist; + assembly { + dropStageDoesNotExist := iszero(sload(existingDropStageData.slot)) + } + + if (addOrUpdateDropStage) { + _tokenGatedDrops[msg.sender][allowedNftToken] = dropStage; + // Add to enumeration if it does not exist already. + if (dropStageDoesNotExist) { + enumeratedTokens.push(allowedNftToken); + } + } else { + // Check we are not deleting a drop stage that does not exist. + if (dropStageDoesNotExist) { + revert TokenGatedDropStageNotPresent(); + } + // Clear storage slot and remove from enumeration. + delete _tokenGatedDrops[msg.sender][allowedNftToken]; + _removeFromEnumeration(allowedNftToken, enumeratedTokens); + } + + // Emit an event with the update. + emit TokenGatedDropStageUpdated(msg.sender, allowedNftToken, dropStage); + } + + /** + * @notice Updates the creator payout address and emits an event. + * + * This method assume msg.sender is an nft contract and its + * ERC165 interface id matches INonFungibleSeaDropToken. + * + * Note: Be sure only authorized users can call this from + * token contracts that implement INonFungibleSeaDropToken. + * + * @param payoutAddress The creator payout address. + */ + function updateCreatorPayoutAddress(address payoutAddress) external onlyINonFungibleSeaDropToken { + if (payoutAddress == address(0)) { + revert CreatorPayoutAddressCannotBeZeroAddress(); + } + // Set the creator payout address. + _creatorPayoutAddresses[msg.sender] = payoutAddress; + + // Emit an event with the update. + emit CreatorPayoutAddressUpdated(msg.sender, payoutAddress); + } + + /** + * @notice Updates the allowed fee recipient and emits an event. + * + * This method assume msg.sender is an nft contract and its + * ERC165 interface id matches INonFungibleSeaDropToken. + * + * Note: Be sure only authorized users can call this from + * token contracts that implement INonFungibleSeaDropToken. + * + * @param feeRecipient The fee recipient. + * @param allowed If the fee recipient is allowed. + */ + function updateAllowedFeeRecipient(address feeRecipient, bool allowed) external onlyINonFungibleSeaDropToken { + if (feeRecipient == address(0)) { + revert FeeRecipientCannotBeZeroAddress(); + } + + // Track the enumerated storage. + address[] storage enumeratedStorage = _enumeratedFeeRecipients[msg.sender]; + mapping(address => bool) storage feeRecipientsMap = _allowedFeeRecipients[msg.sender]; + + if (allowed) { + if (feeRecipientsMap[feeRecipient]) { + revert DuplicateFeeRecipient(); + } + feeRecipientsMap[feeRecipient] = true; + enumeratedStorage.push(feeRecipient); + } else { + if (!feeRecipientsMap[feeRecipient]) { + revert FeeRecipientNotPresent(); + } + delete _allowedFeeRecipients[msg.sender][feeRecipient]; + _removeFromEnumeration(feeRecipient, enumeratedStorage); + } + + // Emit an event with the update. + emit AllowedFeeRecipientUpdated(msg.sender, feeRecipient, allowed); + } + + /** + * @notice Updates the allowed server-side signers and emits an event. + * + * This method assume msg.sender is an nft contract and its + * ERC165 interface id matches INonFungibleSeaDropToken. + * + * Note: Be sure only authorized users can call this from + * token contracts that implement INonFungibleSeaDropToken. + * + * @param signer The signer to update. + * @param signedMintValidationParams Minimum and maximum parameters + * to enforce for signed mints. + */ + function updateSignedMintValidationParams( + address signer, + SignedMintValidationParams calldata signedMintValidationParams + ) external onlyINonFungibleSeaDropToken { + if (signer == address(0)) { + revert SignerCannotBeZeroAddress(); + } + + if (signedMintValidationParams.minFeeBps > 10_000) { + revert InvalidFeeBps(signedMintValidationParams.minFeeBps); + } + if (signedMintValidationParams.maxFeeBps > 10_000) { + revert InvalidFeeBps(signedMintValidationParams.maxFeeBps); + } + + // Track the enumerated storage. + address[] storage enumeratedStorage = _enumeratedSigners[msg.sender]; + mapping(address => SignedMintValidationParams) storage signedMintValidationParamsMap = + _signedMintValidationParams[msg.sender]; + + SignedMintValidationParams storage existingSignedMintValidationParams = signedMintValidationParamsMap[signer]; + + bool signedMintValidationParamsDoNotExist; + assembly { + signedMintValidationParamsDoNotExist := iszero(sload(existingSignedMintValidationParams.slot)) + } + // Use maxMaxTotalMintableByWallet as sentry for add/update or delete. + bool addOrUpdate = signedMintValidationParams.maxMaxTotalMintableByWallet > 0; + + if (addOrUpdate) { + signedMintValidationParamsMap[signer] = signedMintValidationParams; + if (signedMintValidationParamsDoNotExist) { + enumeratedStorage.push(signer); + } + } else { + if (existingSignedMintValidationParams.maxMaxTotalMintableByWallet == 0) { + revert SignerNotPresent(); + } + delete _signedMintValidationParams[msg.sender][signer]; + _removeFromEnumeration(signer, enumeratedStorage); + } + + // Emit an event with the update. + emit SignedMintValidationParamsUpdated(msg.sender, signer, signedMintValidationParams); + } + + /** + * @notice Updates the allowed payer and emits an event. + * + * This method assume msg.sender is an nft contract and its + * ERC165 interface id matches INonFungibleSeaDropToken. + * + * Note: Be sure only authorized users can call this from + * token contracts that implement INonFungibleSeaDropToken. + * + * @param payer The payer to add or remove. + * @param allowed Whether to add or remove the payer. + */ + function updatePayer(address payer, bool allowed) external onlyINonFungibleSeaDropToken { + if (payer == address(0)) { + revert PayerCannotBeZeroAddress(); + } + + // Track the enumerated storage. + address[] storage enumeratedStorage = _enumeratedPayers[msg.sender]; + mapping(address => bool) storage payersMap = _allowedPayers[msg.sender]; + + if (allowed) { + if (payersMap[payer]) { + revert DuplicatePayer(); + } + payersMap[payer] = true; + enumeratedStorage.push(payer); + } else { + if (!payersMap[payer]) { + revert PayerNotPresent(); + } + delete _allowedPayers[msg.sender][payer]; + _removeFromEnumeration(payer, enumeratedStorage); + } + + // Emit an event with the update. + emit PayerUpdated(msg.sender, payer, allowed); + } + + /** + * @notice Remove an address from a supplied enumeration. + * + * @param toRemove The address to remove. + * @param enumeration The enumerated addresses to parse. + */ + function _removeFromEnumeration(address toRemove, address[] storage enumeration) internal { + // Cache the length. + uint256 enumerationLength = enumeration.length; + for (uint256 i = 0; i < enumerationLength;) { + // Check if the enumerated element is the one we are deleting. + if (enumeration[i] == toRemove) { + // Swap with the last element. + enumeration[i] = enumeration[enumerationLength - 1]; + // Delete the (now duplicated) last element. + enumeration.pop(); + // Exit the loop. + break; + } + unchecked { + ++i; + } + } + } + + /** + * @notice Verify an EIP-712 signature by recreating the data structure + * that we signed on the client side, and then using that to recover + * the address that signed the signature for this data. + * + * @param nftContract The nft contract. + * @param minter The mint recipient. + * @param feeRecipient The fee recipient. + * @param mintParams The mint params. + * @param salt The salt for the signed mint. + */ + function _getDigest( + address nftContract, + address minter, + address feeRecipient, + MintParams memory mintParams, + uint256 salt + ) internal view returns (bytes32 digest) { + bytes32 mintParamsHashStruct = keccak256( + abi.encode( + _MINT_PARAMS_TYPEHASH, + mintParams.mintPrice, + mintParams.maxTotalMintableByWallet, + mintParams.startTime, + mintParams.endTime, + mintParams.dropStageIndex, + mintParams.maxTokenSupplyForStage, + mintParams.feeBps, + mintParams.restrictFeeRecipients + ) + ); + digest = keccak256( + bytes.concat( + bytes2(0x1901), + _domainSeparator(), + keccak256( + abi.encode(_SIGNED_MINT_TYPEHASH, nftContract, minter, feeRecipient, mintParamsHashStruct, salt) + ) + ) + ); + } + + /** + * @dev Internal pure function to cast a `bool` value to a `uint256` value. + * + * @param b The `bool` value to cast. + * + * @return u The `uint256` value. + */ + function _cast(bool b) internal pure returns (uint256 u) { + assembly { + u := b + } + } +} diff --git a/src/seadrop/src/clones/ERC721ACloneable.sol b/src/seadrop/src/clones/ERC721ACloneable.sol new file mode 100644 index 0000000..7633867 --- /dev/null +++ b/src/seadrop/src/clones/ERC721ACloneable.sol @@ -0,0 +1,1237 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.2.2 +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +import { IERC721A } from "ERC721A/IERC721A.sol"; + +import { + Initializable +} from "openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; + +/** + * @dev Interface of ERC721 token receiver. + */ +interface ERC721A__IERC721Receiver { + 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 ERC721ACloneable is IERC721A, Initializable { + // Bypass for a `--via-ir` bug (https://github.com/chiru-labs/ERC721A/pull/364). + struct TokenApprovalRef { + address value; + } + + // ============================================================= + // 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; + + // ============================================================= + // STORAGE + // ============================================================= + + // The next token ID to be minted. + uint256 private _currentIndex; + + // The number of tokens burned. + uint256 private _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 {_packedOwnershipOf} implementation for details. + // + // Bits Layout: + // - [0..159] `addr` + // - [160..223] `startTimestamp` + // - [224] `burned` + // - [225] `nextInitialized` + // - [232..255] `extraData` + mapping(uint256 => uint256) private _packedOwnerships; + + // Mapping owner address to address data. + // + // Bits Layout: + // - [0..63] `balance` + // - [64..127] `numberMinted` + // - [128..191] `numberBurned` + // - [192..255] `aux` + mapping(address => uint256) private _packedAddressData; + + // Mapping from token ID to approved address. + mapping(uint256 => TokenApprovalRef) private _tokenApprovals; + + // Mapping from owner to operator approvals + mapping(address => mapping(address => bool)) private _operatorApprovals; + + // ============================================================= + // CONSTRUCTOR + // ============================================================= + + function __ERC721ACloneable__init( + string memory name_, + string memory symbol_ + ) internal onlyInitializing { + _name = name_; + _symbol = symbol_; + _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 _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 _currentIndex - _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 _currentIndex - _startTokenId(); + } + } + + /** + * @dev Returns the total number of tokens burned. + */ + function _totalBurned() internal view virtual returns (uint256) { + return _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(); + return _packedAddressData[owner] & _BITMASK_ADDRESS_DATA_ENTRY; + } + + /** + * Returns the number of tokens minted by `owner`. + */ + function _numberMinted(address owner) internal view returns (uint256) { + return + (_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 + (_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(_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 = _packedAddressData[owner]; + uint256 auxCasted; + // Cast `aux` with assembly to avoid redundant masking. + assembly { + auxCasted := aux + } + packed = + (packed & _BITMASK_AUX_COMPLEMENT) | + (auxCasted << _BITPOS_AUX); + _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 _name; + } + + /** + * @dev Returns the token collection symbol. + */ + function symbol() public view virtual override returns (string memory) { + return _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(); + + 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(_packedOwnerships[index]); + } + + /** + * @dev Initializes the ownership slot minted at `index` for efficiency purposes. + */ + function _initializeOwnershipAt(uint256 index) internal virtual { + if (_packedOwnerships[index] == 0) { + _packedOwnerships[index] = _packedOwnershipOf(index); + } + } + + /** + * Returns the packed ownership data of `tokenId`. + */ + function _packedOwnershipOf(uint256 tokenId) + private + view + returns (uint256) + { + uint256 curr = tokenId; + + unchecked { + if (_startTokenId() <= curr) { + if (curr < _currentIndex) { + uint256 packed = _packedOwnerships[curr]; + // If not burned. + if (packed & _BITMASK_BURNED == 0) { + // 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, `curr` will not underflow. + // + // We can directly compare the packed value. + // If the address is zero, packed will be zero. + while (packed == 0) { + packed = _packedOwnerships[--curr]; + } + return packed; + } + } + } + } + revert OwnerQueryForNonexistentToken(); + } + + /** + * @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. + * 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) public virtual override { + address owner = ownerOf(tokenId); + + if (_msgSenderERC721A() != owner) { + if (!isApprovedForAll(owner, _msgSenderERC721A())) { + revert ApprovalCallerNotOwnerNorApproved(); + } + } + + _tokenApprovals[tokenId].value = to; + emit Approval(owner, to, tokenId); + } + + /** + * @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(); + + return _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 + { + _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 _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) { + return + _startTokenId() <= tokenId && + tokenId < _currentIndex && // If within bounds, + _packedOwnerships[tokenId] & _BITMASK_BURNED == 0; // and not burned. + } + + /** + * @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) + { + TokenApprovalRef storage tokenApproval = _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 virtual override { + uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); + + if (address(uint160(prevOwnershipPacked)) != from) + revert TransferFromIncorrectOwner(); + + ( + 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(); + } + + if (to == address(0)) revert TransferToZeroAddress(); + + _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. + --_packedAddressData[from]; // Updates: `balance -= 1`. + ++_packedAddressData[to]; // Updates: `balance += 1`. + + // Updates: + // - `address` to the next owner. + // - `startTimestamp` to the timestamp of transfering. + // - `burned` to `false`. + // - `nextInitialized` to `true`. + _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 (_packedOwnerships[nextTokenId] == 0) { + // If the next slot is within bounds. + if (nextTokenId != _currentIndex) { + // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. + _packedOwnerships[nextTokenId] = prevOwnershipPacked; + } + } + } + } + + emit Transfer(from, to, tokenId); + _afterTokenTransfers(from, to, tokenId, 1); + } + + /** + * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public 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 virtual override { + transferFrom(from, to, tokenId); + if (to.code.length != 0) { + if (!_checkContractOnERC721Received(from, to, tokenId, _data)) { + revert TransferToNonERC721ReceiverImplementer(); + } + } + } + + /** + * @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__IERC721Receiver(to).onERC721Received( + _msgSenderERC721A(), + from, + tokenId, + _data + ) + returns (bytes4 retval) { + return + retval == + ERC721A__IERC721Receiver(to).onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + revert TransferToNonERC721ReceiverImplementer(); + } else { + 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 = _currentIndex; + if (quantity == 0) revert MintZeroQuantity(); + + _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: + // - `balance += quantity`. + // - `numberMinted += quantity`. + // + // We can directly add to the `balance` and `numberMinted`. + _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`. + _packedOwnerships[startTokenId] = _packOwnershipData( + to, + _nextInitializedFlag(quantity) | + _nextExtraData(address(0), to, 0) + ); + + uint256 toMasked; + uint256 end = startTokenId + quantity; + + // Use assembly to loop and emit the `Transfer` event for gas savings. + // The duplicated `log4` removes an extra check and reduces stack juggling. + // The assembly, together with the surrounding Solidity code, have been + // delicately arranged to nudge the compiler into producing optimized opcodes. + assembly { + // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean. + toMasked := and(to, _BITMASK_ADDRESS) + // 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`. + startTokenId // `tokenId`. + ) + + // The `iszero(eq(,))` check ensures that large values of `quantity` + // that overflows uint256 will make the loop run out of gas. + // The compiler will optimize the `iszero` away for performance. + for { + let tokenId := add(startTokenId, 1) + } iszero(eq(tokenId, end)) { + tokenId := add(tokenId, 1) + } { + // Emit the `Transfer` event. Similar to above. + log4(0, 0, _TRANSFER_EVENT_SIGNATURE, 0, toMasked, tokenId) + } + } + if (toMasked == 0) revert MintToZeroAddress(); + + _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 = _currentIndex; + if (to == address(0)) revert MintToZeroAddress(); + if (quantity == 0) revert MintZeroQuantity(); + if (quantity > _MAX_MINT_ERC2309_QUANTITY_LIMIT) + revert MintERC2309QuantityExceedsLimit(); + + _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`. + _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`. + _packedOwnerships[startTokenId] = _packOwnershipData( + to, + _nextInitializedFlag(quantity) | + _nextExtraData(address(0), to, 0) + ); + + emit ConsecutiveTransfer( + startTokenId, + startTokenId + quantity - 1, + address(0), + to + ); + + _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 = _currentIndex; + uint256 index = end - quantity; + do { + if ( + !_checkContractOnERC721Received( + address(0), + to, + index++, + _data + ) + ) { + revert TransferToNonERC721ReceiverImplementer(); + } + } while (index < end); + // Reentrancy protection. + if (_currentIndex != end) revert(); + } + } + } + + /** + * @dev Equivalent to `_safeMint(to, quantity, '')`. + */ + function _safeMint(address to, uint256 quantity) internal virtual { + _safeMint(to, quantity, ""); + } + + // ============================================================= + // 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(); + } + } + + _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;`. + _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`. + _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 (_packedOwnerships[nextTokenId] == 0) { + // If the next slot is within bounds. + if (nextTokenId != _currentIndex) { + // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. + _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 { + _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 = _packedOwnerships[index]; + if (packed == 0) revert OwnershipNotInitializedForExtraData(); + uint256 extraDataCasted; + // Cast `extraData` with assembly to avoid redundant masking. + assembly { + extraDataCasted := extraData + } + packed = + (packed & _BITMASK_EXTRA_DATA_COMPLEMENT) | + (extraDataCasted << _BITPOS_EXTRA_DATA); + _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) + } + } +} diff --git a/src/seadrop/src/clones/ERC721ContractMetadataCloneable.sol b/src/seadrop/src/clones/ERC721ContractMetadataCloneable.sol new file mode 100644 index 0000000..bf2e18f --- /dev/null +++ b/src/seadrop/src/clones/ERC721ContractMetadataCloneable.sol @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {ISeaDropTokenContractMetadata} from "../interfaces/ISeaDropTokenContractMetadata.sol"; + +import {ERC721ACloneable} from "./ERC721ACloneable.sol"; + +import {TwoStepOwnable} from "utility-contracts/TwoStepOwnable.sol"; + +import {IERC2981} from "openzeppelin-contracts/interfaces/IERC2981.sol"; + +import {IERC165} from "openzeppelin-contracts/utils/introspection/IERC165.sol"; + +/** + * @title ERC721ContractMetadataCloneable + * @author James Wenzel (emo.eth) + * @author Ryan Ghods (ralxz.eth) + * @author Stephan Min (stephanm.eth) + * @notice ERC721ContractMetadata is a token contract that extends ERC721A + * with additional metadata and ownership capabilities. + */ +contract ERC721ContractMetadataCloneable is ERC721ACloneable, TwoStepOwnable, ISeaDropTokenContractMetadata { + /// @notice Track the max supply. + uint256 _maxSupply; + + /// @notice Track the base URI for token metadata. + string _tokenBaseURI; + + /// @notice Track the contract URI for contract metadata. + string _contractURI; + + /// @notice Track the provenance hash for guaranteeing metadata order + /// for random reveals. + bytes32 _provenanceHash; + + /// @notice Track the royalty info: address to receive royalties, and + /// royalty basis points. + RoyaltyInfo _royaltyInfo; + + /** + * @dev Reverts if the sender is not the owner or the contract itself. + * This function is inlined instead of being a modifier + * to save contract space from being inlined N times. + */ + function _onlyOwnerOrSelf() internal view { + if (_cast(msg.sender == owner()) | _cast(msg.sender == address(this)) == 0) { + revert OnlyOwner(); + } + } + + /** + * @notice Sets the base URI for the token metadata and emits an event. + * + * @param newBaseURI The new base URI to set. + */ + function setBaseURI(string calldata newBaseURI) external override { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Set the new base URI. + _tokenBaseURI = newBaseURI; + + // Emit an event with the update. + if (totalSupply() != 0) { + emit BatchMetadataUpdate(1, _nextTokenId() - 1); + } + } + + /** + * @notice Sets the contract URI for contract metadata. + * + * @param newContractURI The new contract URI. + */ + function setContractURI(string calldata newContractURI) external override { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Set the new contract URI. + _contractURI = newContractURI; + + // Emit an event with the update. + emit ContractURIUpdated(newContractURI); + } + + /** + * @notice Emit an event notifying metadata updates for + * a range of token ids, according to EIP-4906. + * + * @param fromTokenId The start token id. + * @param toTokenId The end token id. + */ + function emitBatchMetadataUpdate(uint256 fromTokenId, uint256 toTokenId) external { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Emit an event with the update. + emit BatchMetadataUpdate(fromTokenId, toTokenId); + } + + /** + * @notice Sets the max token supply and emits an event. + * + * @param newMaxSupply The new max supply to set. + */ + function setMaxSupply(uint256 newMaxSupply) external { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the max supply does not exceed the maximum value of uint64. + if (newMaxSupply > 2 ** 64 - 1) { + revert CannotExceedMaxSupplyOfUint64(newMaxSupply); + } + + // Set the new max supply. + _maxSupply = newMaxSupply; + + // Emit an event with the update. + emit MaxSupplyUpdated(newMaxSupply); + } + + /** + * @notice Sets the provenance hash and emits an event. + * + * The provenance hash is used for random reveals, which + * is a hash of the ordered metadata to show it has not been + * modified after mint started. + * + * This function will revert after the first item has been minted. + * + * @param newProvenanceHash The new provenance hash to set. + */ + function setProvenanceHash(bytes32 newProvenanceHash) external { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Revert if any items have been minted. + if (_totalMinted() > 0) { + revert ProvenanceHashCannotBeSetAfterMintStarted(); + } + + // Keep track of the old provenance hash for emitting with the event. + bytes32 oldProvenanceHash = _provenanceHash; + + // Set the new provenance hash. + _provenanceHash = newProvenanceHash; + + // Emit an event with the update. + emit ProvenanceHashUpdated(oldProvenanceHash, newProvenanceHash); + } + + /** + * @notice Sets the address and basis points for royalties. + * + * @param newInfo The struct to configure royalties. + */ + function setRoyaltyInfo(RoyaltyInfo calldata newInfo) external { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Revert if the new royalty address is the zero address. + if (newInfo.royaltyAddress == address(0)) { + revert RoyaltyAddressCannotBeZeroAddress(); + } + + // Revert if the new basis points is greater than 10_000. + if (newInfo.royaltyBps > 10_000) { + revert InvalidRoyaltyBasisPoints(newInfo.royaltyBps); + } + + // Set the new royalty info. + _royaltyInfo = newInfo; + + // Emit an event with the updated params. + emit RoyaltyInfoUpdated(newInfo.royaltyAddress, newInfo.royaltyBps); + } + + /** + * @notice Returns the base URI for token metadata. + */ + function baseURI() external view override returns (string memory) { + return _baseURI(); + } + + /** + * @notice Returns the base URI for the contract, which ERC721A uses + * to return tokenURI. + */ + function _baseURI() internal view virtual override returns (string memory) { + return _tokenBaseURI; + } + + /** + * @notice Returns the contract URI for contract metadata. + */ + function contractURI() external view override returns (string memory) { + return _contractURI; + } + + /** + * @notice Returns the max token supply. + */ + function maxSupply() public view returns (uint256) { + return _maxSupply; + } + + /** + * @notice Returns the provenance hash. + * The provenance hash is used for random reveals, which + * is a hash of the ordered metadata to show it is unmodified + * after mint has started. + */ + function provenanceHash() external view override returns (bytes32) { + return _provenanceHash; + } + + /** + * @notice Returns the address that receives royalties. + */ + function royaltyAddress() external view returns (address) { + return _royaltyInfo.royaltyAddress; + } + + /** + * @notice Returns the royalty basis points out of 10_000. + */ + function royaltyBasisPoints() external view returns (uint256) { + return _royaltyInfo.royaltyBps; + } + + /** + * @notice Called with the sale price to determine how much royalty + * is owed and to whom. + * + * @ param _tokenId The NFT asset queried for royalty information. + * @param _salePrice The sale price of the NFT asset specified by + * _tokenId. + * + * @return receiver Address of who should be sent the royalty payment. + * @return royaltyAmount The royalty payment amount for _salePrice. + */ + function royaltyInfo( + uint256, + /* _tokenId */ + uint256 _salePrice + ) external view returns (address receiver, uint256 royaltyAmount) { + // Put the royalty info on the stack for more efficient access. + RoyaltyInfo storage info = _royaltyInfo; + + // Set the royalty amount to the sale price times the royalty basis + // points divided by 10_000. + royaltyAmount = (_salePrice * info.royaltyBps) / 10_000; + + // Set the receiver of the royalty. + receiver = info.royaltyAddress; + } + + /** + * @notice Returns whether the interface is supported. + * + * @param interfaceId The interface id to check against. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(IERC165, ERC721ACloneable) + returns (bool) + { + return interfaceId == type(IERC2981).interfaceId || interfaceId == 0x49064906 // ERC-4906 + || super.supportsInterface(interfaceId); + } + + /** + * @dev Internal pure function to cast a `bool` value to a `uint256` value. + * + * @param b The `bool` value to cast. + * + * @return u The `uint256` value. + */ + function _cast(bool b) internal pure returns (uint256 u) { + assembly { + u := b + } + } +} diff --git a/src/seadrop/src/clones/ERC721SeaDropCloneFactory.sol b/src/seadrop/src/clones/ERC721SeaDropCloneFactory.sol new file mode 100644 index 0000000..750ff1d --- /dev/null +++ b/src/seadrop/src/clones/ERC721SeaDropCloneFactory.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {ERC721SeaDropCloneable} from "./ERC721SeaDropCloneable.sol"; + +import {Clones} from "openzeppelin-contracts/proxy/Clones.sol"; + +contract ERC721SeaDropCloneFactory { + address public immutable seaDropCloneableUpgradeableImplementation; + address public constant DEFAULT_SEADROP = 0x00005EA00Ac477B1030CE78506496e8C2dE24bf5; + + constructor() { + ERC721SeaDropCloneable impl = new ERC721SeaDropCloneable(); + impl.initialize("", "", new address[](0), address(this)); + seaDropCloneableUpgradeableImplementation = address(impl); + } + + function createClone(string memory name, string memory symbol, bytes32 salt) external returns (address) { + // Derive a pseudo-random salt, so clone addresses don't collide + // across chains. + bytes32 cloneSalt = keccak256(abi.encodePacked(salt, blockhash(block.number))); + + address instance = Clones.cloneDeterministic(seaDropCloneableUpgradeableImplementation, cloneSalt); + address[] memory allowedSeaDrop = new address[](1); + allowedSeaDrop[0] = DEFAULT_SEADROP; + ERC721SeaDropCloneable(instance).initialize(name, symbol, allowedSeaDrop, msg.sender); + return instance; + } +} diff --git a/src/seadrop/src/clones/ERC721SeaDropCloneable.sol b/src/seadrop/src/clones/ERC721SeaDropCloneable.sol new file mode 100644 index 0000000..93677c9 --- /dev/null +++ b/src/seadrop/src/clones/ERC721SeaDropCloneable.sol @@ -0,0 +1,586 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {INonFungibleSeaDropToken} from "../interfaces/INonFungibleSeaDropToken.sol"; + +import {ISeaDrop} from "../interfaces/ISeaDrop.sol"; + +import {AllowListData, PublicDrop, TokenGatedDropStage, SignedMintValidationParams} from "../lib/SeaDropStructs.sol"; + +import {ERC721SeaDropStructsErrorsAndEvents} from "../lib/ERC721SeaDropStructsErrorsAndEvents.sol"; + +import {ERC721ACloneable} from "./ERC721ACloneable.sol"; + +import {ReentrancyGuardUpgradeable} from "openzeppelin-contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +import {IERC165} from "openzeppelin-contracts/utils/introspection/IERC165.sol"; + +/** + * @title ERC721SeaDrop + * @author James Wenzel (emo.eth) + * @author Ryan Ghods (ralxz.eth) + * @author Stephan Min (stephanm.eth) + * @notice ERC721SeaDrop is a token contract that contains methods + * to properly interact with SeaDrop. + */ +contract ERC721SeaDropCloneable is + ERC721ContractMetadataCloneable, + INonFungibleSeaDropToken, + ERC721SeaDropStructsErrorsAndEvents, + ReentrancyGuardUpgradeable +{ + /// @notice Track the allowed SeaDrop addresses. + mapping(address => bool) internal _allowedSeaDrop; + + /// @notice Track the enumerated allowed SeaDrop addresses. + address[] internal _enumeratedAllowedSeaDrop; + + /** + * @dev Reverts if not an allowed SeaDrop contract. + * This function is inlined instead of being a modifier + * to save contract space from being inlined N times. + * + * @param seaDrop The SeaDrop address to check if allowed. + */ + function _onlyAllowedSeaDrop(address seaDrop) internal view { + if (_allowedSeaDrop[seaDrop] != true) { + revert OnlyAllowedSeaDrop(); + } + } + + /** + * @notice Deploy the token contract with its name, symbol, + * and allowed SeaDrop addresses. + */ + function initialize( + string calldata __name, + string calldata __symbol, + address[] calldata allowedSeaDrop, + address initialOwner + ) public initializer { + __ERC721ACloneable__init(__name, __symbol); + __ReentrancyGuard_init(); + _updateAllowedSeaDrop(allowedSeaDrop); + _transferOwnership(initialOwner); + emit SeaDropTokenDeployed(); + } + + /** + * @notice Update the allowed SeaDrop contracts. + * Only the owner or administrator can use this function. + * + * @param allowedSeaDrop The allowed SeaDrop addresses. + */ + function updateAllowedSeaDrop(address[] calldata allowedSeaDrop) external virtual override onlyOwner { + _updateAllowedSeaDrop(allowedSeaDrop); + } + + /** + * @notice Internal function to update the allowed SeaDrop contracts. + * + * @param allowedSeaDrop The allowed SeaDrop addresses. + */ + function _updateAllowedSeaDrop(address[] calldata allowedSeaDrop) internal { + // Put the length on the stack for more efficient access. + uint256 enumeratedAllowedSeaDropLength = _enumeratedAllowedSeaDrop.length; + uint256 allowedSeaDropLength = allowedSeaDrop.length; + + // Reset the old mapping. + for (uint256 i = 0; i < enumeratedAllowedSeaDropLength;) { + _allowedSeaDrop[_enumeratedAllowedSeaDrop[i]] = false; + unchecked { + ++i; + } + } + + // Set the new mapping for allowed SeaDrop contracts. + for (uint256 i = 0; i < allowedSeaDropLength;) { + _allowedSeaDrop[allowedSeaDrop[i]] = true; + unchecked { + ++i; + } + } + + // Set the enumeration. + _enumeratedAllowedSeaDrop = allowedSeaDrop; + + // Emit an event for the update. + emit AllowedSeaDropUpdated(allowedSeaDrop); + } + + /** + * @dev Overrides the `_startTokenId` function from ERC721A + * to start at token id `1`. + * + * This is to avoid future possible problems since `0` is usually + * used to signal values that have not been set or have been removed. + */ + function _startTokenId() internal view virtual override returns (uint256) { + return 1; + } + + /** + * @dev Overrides the `tokenURI()` function from ERC721A + * to return just the base URI if it is implied to not be a directory. + * + * This is to help with ERC721 contracts in which the same token URI + * is desired for each token, such as when the tokenURI is 'unrevealed'. + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + + string memory baseURI = _baseURI(); + + // Exit early if the baseURI is empty. + if (bytes(baseURI).length == 0) { + return ""; + } + + // Check if the last character in baseURI is a slash. + if (bytes(baseURI)[bytes(baseURI).length - 1] != bytes("/")[0]) { + return baseURI; + } + + return string(abi.encodePacked(baseURI, _toString(tokenId))); + } + + /** + * @notice Mint tokens, restricted to the SeaDrop contract. + * + * @dev NOTE: If a token registers itself with multiple SeaDrop + * contracts, the implementation of this function should guard + * against reentrancy. If the implementing token uses + * _safeMint(), or a feeRecipient with a malicious receive() hook + * is specified, the token or fee recipients may be able to execute + * another mint in the same transaction via a separate SeaDrop + * contract. + * This is dangerous if an implementing token does not correctly + * update the minterNumMinted and currentTotalSupply values before + * transferring minted tokens, as SeaDrop references these values + * to enforce token limits on a per-wallet and per-stage basis. + * + * ERC721A tracks these values automatically, but this note and + * nonReentrant modifier are left here to encourage best-practices + * when referencing this contract. + * + * @param minter The address to mint to. + * @param quantity The number of tokens to mint. + */ + function mintSeaDrop(address minter, uint256 quantity) external virtual override nonReentrant { + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(msg.sender); + + // Extra safety check to ensure the max supply is not exceeded. + if (_totalMinted() + quantity > maxSupply()) { + revert MintQuantityExceedsMaxSupply(_totalMinted() + quantity, maxSupply()); + } + + // Mint the quantity of tokens to the minter. + _safeMint(minter, quantity); + } + + /** + * @notice Update the public drop data for this nft contract on SeaDrop. + * Only the owner can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param publicDrop The public drop data. + */ + function updatePublicDrop(address seaDropImpl, PublicDrop calldata publicDrop) external virtual override { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the public drop data on SeaDrop. + ISeaDrop(seaDropImpl).updatePublicDrop(publicDrop); + } + + /** + * @notice Update the allow list data for this nft contract on SeaDrop. + * Only the owner can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param allowListData The allow list data. + */ + function updateAllowList(address seaDropImpl, AllowListData calldata allowListData) external virtual override { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the allow list on SeaDrop. + ISeaDrop(seaDropImpl).updateAllowList(allowListData); + } + + /** + * @notice Update the token gated drop stage data for this nft contract + * on SeaDrop. + * Only the owner can use this function. + * + * Note: If two INonFungibleSeaDropToken tokens are doing + * simultaneous token gated drop promotions for each other, + * they can be minted by the same actor until + * `maxTokenSupplyForStage` is reached. Please ensure the + * `allowedNftToken` is not running an active drop during the + * `dropStage` time period. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param allowedNftToken The allowed nft token. + * @param dropStage The token gated drop stage data. + */ + function updateTokenGatedDrop(address seaDropImpl, address allowedNftToken, TokenGatedDropStage calldata dropStage) + external + virtual + override + { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the token gated drop stage. + ISeaDrop(seaDropImpl).updateTokenGatedDrop(allowedNftToken, dropStage); + } + + /** + * @notice Update the drop URI for this nft contract on SeaDrop. + * Only the owner can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param dropURI The new drop URI. + */ + function updateDropURI(address seaDropImpl, string calldata dropURI) external virtual override { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the drop URI. + ISeaDrop(seaDropImpl).updateDropURI(dropURI); + } + + /** + * @notice Update the creator payout address for this nft contract on + * SeaDrop. + * Only the owner can set the creator payout address. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param payoutAddress The new payout address. + */ + function updateCreatorPayoutAddress(address seaDropImpl, address payoutAddress) external { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the creator payout address. + ISeaDrop(seaDropImpl).updateCreatorPayoutAddress(payoutAddress); + } + + /** + * @notice Update the allowed fee recipient for this nft contract + * on SeaDrop. + * Only the owner can set the allowed fee recipient. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param feeRecipient The new fee recipient. + * @param allowed If the fee recipient is allowed. + */ + function updateAllowedFeeRecipient(address seaDropImpl, address feeRecipient, bool allowed) external virtual { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the allowed fee recipient. + ISeaDrop(seaDropImpl).updateAllowedFeeRecipient(feeRecipient, allowed); + } + + /** + * @notice Update the server-side signers for this nft contract + * on SeaDrop. + * Only the owner can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param signer The signer to update. + * @param signedMintValidationParams Minimum and maximum parameters to + * enforce for signed mints. + */ + function updateSignedMintValidationParams( + address seaDropImpl, + address signer, + SignedMintValidationParams memory signedMintValidationParams + ) external virtual override { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the signer. + ISeaDrop(seaDropImpl).updateSignedMintValidationParams(signer, signedMintValidationParams); + } + + /** + * @notice Update the allowed payers for this nft contract on SeaDrop. + * Only the owner can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param payer The payer to update. + * @param allowed Whether the payer is allowed. + */ + function updatePayer(address seaDropImpl, address payer, bool allowed) external virtual override { + // Ensure the sender is only the owner or contract itself. + _onlyOwnerOrSelf(); + + // Ensure the SeaDrop is allowed. + _onlyAllowedSeaDrop(seaDropImpl); + + // Update the payer. + ISeaDrop(seaDropImpl).updatePayer(payer, allowed); + } + + /** + * @notice Returns a set of mint stats for the address. + * This assists SeaDrop in enforcing maxSupply, + * maxTotalMintableByWallet, and maxTokenSupplyForStage checks. + * + * @dev NOTE: Implementing contracts should always update these numbers + * before transferring any tokens with _safeMint() to mitigate + * consequences of malicious onERC721Received() hooks. + * + * @param minter The minter address. + */ + function getMintStats(address minter) + external + view + override + returns (uint256 minterNumMinted, uint256 currentTotalSupply, uint256 maxSupply) + { + minterNumMinted = _numberMinted(minter); + currentTotalSupply = _totalMinted(); + maxSupply = _maxSupply; + } + + /** + * @notice Returns whether the interface is supported. + * + * @param interfaceId The interface id to check against. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(IERC165, ERC721ContractMetadataCloneable) + returns (bool) + { + return interfaceId == type(INonFungibleSeaDropToken).interfaceId + || interfaceId == type(ISeaDropTokenContractMetadata).interfaceId + // ERC721ContractMetadata returns supportsInterface true for + // EIP-2981 + // ERC721A returns supportsInterface true for + // ERC165, ERC721, ERC721Metadata + || super.supportsInterface(interfaceId); + } + + /** + * @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. + * - The `operator` must be allowed. + * + * Emits an {ApprovalForAll} event. + */ + function setApprovalForAll(address operator, bool approved) public override { + super.setApprovalForAll(operator, approved); + } + + /** + * @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. + * - The `operator` mut be allowed. + * + * Emits an {Approval} event. + */ + function approve(address operator, uint256 tokenId) public override { + super.approve(operator, tokenId); + } + + /** + * @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}. + * - The operator must be allowed. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 tokenId) public override { + super.transferFrom(from, to, tokenId); + } + + /** + * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. + */ + function safeTransferFrom(address from, address to, uint256 tokenId) public override { + super.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. + * - The operator must be allowed. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public override { + super.safeTransferFrom(from, to, tokenId, data); + } + + /** + * @notice Configure multiple properties at a time. + * + * Note: The individual configure methods should be used + * to unset or reset any properties to zero, as this method + * will ignore zero-value properties in the config struct. + * + * @param config The configuration struct. + */ + function multiConfigure(MultiConfigureStruct calldata config) external onlyOwner { + if (config.maxSupply > 0) { + this.setMaxSupply(config.maxSupply); + } + if (bytes(config.baseURI).length != 0) { + this.setBaseURI(config.baseURI); + } + if (bytes(config.contractURI).length != 0) { + this.setContractURI(config.contractURI); + } + if (_cast(config.publicDrop.startTime != 0) | _cast(config.publicDrop.endTime != 0) == 1) { + this.updatePublicDrop(config.seaDropImpl, config.publicDrop); + } + if (bytes(config.dropURI).length != 0) { + this.updateDropURI(config.seaDropImpl, config.dropURI); + } + if (config.allowListData.merkleRoot != bytes32(0)) { + this.updateAllowList(config.seaDropImpl, config.allowListData); + } + if (config.creatorPayoutAddress != address(0)) { + this.updateCreatorPayoutAddress(config.seaDropImpl, config.creatorPayoutAddress); + } + if (config.provenanceHash != bytes32(0)) { + this.setProvenanceHash(config.provenanceHash); + } + if (config.allowedFeeRecipients.length > 0) { + for (uint256 i = 0; i < config.allowedFeeRecipients.length;) { + this.updateAllowedFeeRecipient(config.seaDropImpl, config.allowedFeeRecipients[i], true); + unchecked { + ++i; + } + } + } + if (config.disallowedFeeRecipients.length > 0) { + for (uint256 i = 0; i < config.disallowedFeeRecipients.length;) { + this.updateAllowedFeeRecipient(config.seaDropImpl, config.disallowedFeeRecipients[i], false); + unchecked { + ++i; + } + } + } + if (config.allowedPayers.length > 0) { + for (uint256 i = 0; i < config.allowedPayers.length;) { + this.updatePayer(config.seaDropImpl, config.allowedPayers[i], true); + unchecked { + ++i; + } + } + } + if (config.disallowedPayers.length > 0) { + for (uint256 i = 0; i < config.disallowedPayers.length;) { + this.updatePayer(config.seaDropImpl, config.disallowedPayers[i], false); + unchecked { + ++i; + } + } + } + if (config.tokenGatedDropStages.length > 0) { + if (config.tokenGatedDropStages.length != config.tokenGatedAllowedNftTokens.length) { + revert TokenGatedMismatch(); + } + for (uint256 i = 0; i < config.tokenGatedDropStages.length;) { + this.updateTokenGatedDrop( + config.seaDropImpl, config.tokenGatedAllowedNftTokens[i], config.tokenGatedDropStages[i] + ); + unchecked { + ++i; + } + } + } + if (config.disallowedTokenGatedAllowedNftTokens.length > 0) { + for (uint256 i = 0; i < config.disallowedTokenGatedAllowedNftTokens.length;) { + TokenGatedDropStage memory emptyStage; + this.updateTokenGatedDrop( + config.seaDropImpl, config.disallowedTokenGatedAllowedNftTokens[i], emptyStage + ); + unchecked { + ++i; + } + } + } + if (config.signedMintValidationParams.length > 0) { + if (config.signedMintValidationParams.length != config.signers.length) { + revert SignersMismatch(); + } + for (uint256 i = 0; i < config.signedMintValidationParams.length;) { + this.updateSignedMintValidationParams( + config.seaDropImpl, config.signers[i], config.signedMintValidationParams[i] + ); + unchecked { + ++i; + } + } + } + if (config.disallowedSigners.length > 0) { + for (uint256 i = 0; i < config.disallowedSigners.length;) { + SignedMintValidationParams memory emptyParams; + this.updateSignedMintValidationParams(config.seaDropImpl, config.disallowedSigners[i], emptyParams); + unchecked { + ++i; + } + } + } + } +} diff --git a/src/seadrop/src/extensions/ERC721PartnerSeaDropBurnable.sol b/src/seadrop/src/extensions/ERC721PartnerSeaDropBurnable.sol new file mode 100644 index 0000000..0104107 --- /dev/null +++ b/src/seadrop/src/extensions/ERC721PartnerSeaDropBurnable.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +/** + * @title ERC721PartnerSeaDropBurnable + * @author James Wenzel (emo.eth) + * @author Ryan Ghods (ralxz.eth) + * @author Stephan Min (stephanm.eth) + * @notice ERC721PartnerSeaDropBurnable is a token contract that extends + * ERC721PartnerSeaDrop to additionally provide a burn function. + */ +contract ERC721PartnerSeaDropBurnable { + /** + * @notice Deploy the token contract with its name, symbol, + * administrator, and allowed SeaDrop addresses. + */ + constructor(string memory name, string memory symbol, address administrator, address[] memory allowedSeaDrop) + ERC721PartnerSeaDrop(name, symbol, administrator, allowedSeaDrop) + {} + + /** + * @notice Burns `tokenId`. The caller must own `tokenId` or be an + * approved operator. + * + * @param tokenId The token id to burn. + */ + // solhint-disable-next-line comprehensive-interface + function burn(uint256 tokenId) external { + _burn(tokenId, true); + } +} diff --git a/src/seadrop/src/extensions/ERC721PartnerSeaDropRandomOffset.sol b/src/seadrop/src/extensions/ERC721PartnerSeaDropRandomOffset.sol new file mode 100644 index 0000000..817eaf4 --- /dev/null +++ b/src/seadrop/src/extensions/ERC721PartnerSeaDropRandomOffset.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {ERC721PartnerSeaDrop} from "../ERC721PartnerSeaDrop.sol"; + +/** + * @title ERC721PartnerSeaDropRandomOffset + * @author James Wenzel (emo.eth) + * @author Ryan Ghods (ralxz.eth) + * @author Stephan Min (stephanm.eth) + * @notice ERC721PartnerSeaDropRandomOffset is a token contract that extends + * ERC721PartnerSeaDrop to apply a randomOffset to the tokenURI, + * to enable fair metadata reveals. + */ +contract ERC721PartnerSeaDropRandomOffset is ERC721PartnerSeaDrop { + /// @notice The random offset, between 1 and the MAX_SUPPLY at the time of + /// being set. + uint256 public randomOffset; + + /// @notice If the collection has been revealed and the randomOffset has + /// been set. 1=False, 2=True. + uint256 public revealed = _REVEALED_FALSE; + + /// @dev For gas efficiency, uint is used instead of bool for revealed. + uint256 private constant _REVEALED_FALSE = 1; + uint256 private constant _REVEALED_TRUE = 2; + + /// @notice Revert when setting the randomOffset if already set. + error AlreadyRevealed(); + + /// @notice Revert when setting the randomOffset if the collection is + /// not yet fully minted. + error NotFullyMinted(); + + /** + * @notice Deploy the token contract with its name, symbol, + * administrator, and allowed SeaDrop addresses. + */ + constructor(string memory name, string memory symbol, address administrator, address[] memory allowedSeaDrop) + ERC721PartnerSeaDrop(name, symbol, administrator, allowedSeaDrop) + {} + + /** + * @notice Set the random offset, for a fair metadata reveal. Only callable + * by the owner one time when the total number of minted tokens + * equals the max supply. Should be called immediately before + * reveal. + */ + // solhint-disable-next-line comprehensive-interface + function setRandomOffset() external onlyOwner { + // Revert setting the offset if already revealed. + if (revealed == _REVEALED_TRUE) { + revert AlreadyRevealed(); + } + + // Put maxSupply on the stack, since reading a state variable + // costs more gas than reading a local variable. + uint256 maxSupply = _maxSupply; + + // Revert if the collection is not yet fully minted. + if (_totalMinted() != maxSupply) { + revert NotFullyMinted(); + } + + // block.difficulty returns PREVRANDAO on Ethereum post-merge + // NOTE: do not use this on other chains + // randomOffset returns between 1 and MAX_SUPPLY + randomOffset = (uint256(keccak256(abi.encode(block.difficulty))) % (maxSupply - 1)) + 1; + + // Set revealed to true. + revealed = _REVEALED_TRUE; + } + + /** + * @notice The token URI, offset by randomOffset, to enable fair metadata + * reveals. + * + * @param tokenId The token id + */ + function tokenURI(uint256 tokenId) public view override returns (string memory) { + if (!_exists(tokenId)) { + revert URIQueryForNonexistentToken(); + } + + string memory base = _baseURI(); + if (bytes(base).length == 0) { + // If there is no baseURI set, return an empty string. + return ""; + } else if (revealed == _REVEALED_FALSE) { + // If the baseURI is set but the collection is not revealed yet, + // return just the baseURI. + return base; + } else { + // If the baseURI is set and the collection is revealed, + // return the tokenURI offset by the randomOffset. + return string.concat(base, _toString(((tokenId + randomOffset) % _maxSupply) + _startTokenId())); + } + } +} diff --git a/src/seadrop/src/extensions/ERC721SeaDropBurnable.sol b/src/seadrop/src/extensions/ERC721SeaDropBurnable.sol new file mode 100644 index 0000000..39828ca --- /dev/null +++ b/src/seadrop/src/extensions/ERC721SeaDropBurnable.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {ERC721SeaDrop} from "../ERC721SeaDrop.sol"; + +/** + * @title ERC721SeaDropBurnable + * @author James Wenzel (emo.eth) + * @author Ryan Ghods (ralxz.eth) + * @author Stephan Min (stephanm.eth) + * @author Michael Cohen (notmichael.eth) + * @notice ERC721SeaDropBurnable is a token contract that extends + * ERC721SeaDrop to additionally provide a burn function. + */ +contract ERC721SeaDropBurnable is ERC721SeaDrop { + /** + * @notice Deploy the token contract with its name, symbol, + * and allowed SeaDrop addresses. + */ + constructor(string memory name, string memory symbol, address[] memory allowedSeaDrop) + ERC721SeaDrop(name, symbol, allowedSeaDrop) + {} + + /** + * @notice Burns `tokenId`. The caller must own `tokenId` or be an + * approved operator. + * + * @param tokenId The token id to burn. + */ + // solhint-disable-next-line comprehensive-interface + function burn(uint256 tokenId) external { + _burn(tokenId, true); + } +} diff --git a/src/seadrop/src/extensions/ERC721SeaDropRandomOffset.sol b/src/seadrop/src/extensions/ERC721SeaDropRandomOffset.sol new file mode 100644 index 0000000..7d61c32 --- /dev/null +++ b/src/seadrop/src/extensions/ERC721SeaDropRandomOffset.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {ERC721SeaDrop} from "../ERC721SeaDrop.sol"; + +/** + * @title ERC721SeaDropRandomOffset + * @author James Wenzel (emo.eth) + * @author Ryan Ghods (ralxz.eth) + * @author Stephan Min (stephanm.eth) + * @author Michael Cohen (notmichael.eth) + * @notice ERC721SeaDropRandomOffset is a token contract that extends + * ERC721SeaDrop to apply a randomOffset to the tokenURI, + * to enable fair metadata reveals. + */ +contract ERC721SeaDropRandomOffset is ERC721SeaDrop { + /// @notice The random offset, between 1 and the MAX_SUPPLY at the time of + /// being set. + uint256 public randomOffset; + + /// @notice If the collection has been revealed and the randomOffset has + /// been set. 1=False, 2=True. + uint256 public revealed = _REVEALED_FALSE; + + /// @dev For gas efficiency, uint is used instead of bool for revealed. + uint256 private constant _REVEALED_FALSE = 1; + uint256 private constant _REVEALED_TRUE = 2; + + /// @notice Revert when setting the randomOffset if already set. + error AlreadyRevealed(); + + /// @notice Revert when setting the randomOffset if the collection is + /// not yet fully minted. + error NotFullyMinted(); + + /** + * @notice Deploy the token contract with its name, symbol, + * and allowed SeaDrop addresses. + */ + constructor(string memory name, string memory symbol, address[] memory allowedSeaDrop) + ERC721SeaDrop(name, symbol, allowedSeaDrop) + {} + + /** + * @notice Set the random offset, for a fair metadata reveal. Only callable + * by the owner one time when the total number of minted tokens + * equals the max supply. Should be called immediately before + * reveal. + */ + // solhint-disable-next-line comprehensive-interface + function setRandomOffset() external onlyOwner { + // Revert setting the offset if already revealed. + if (revealed == _REVEALED_TRUE) { + revert AlreadyRevealed(); + } + + // Put maxSupply on the stack, since reading a state variable + // costs more gas than reading a local variable. + uint256 maxSupply = _maxSupply; + + // Revert if the collection is not yet fully minted. + if (_totalMinted() != maxSupply) { + revert NotFullyMinted(); + } + + // block.difficulty returns PREVRANDAO on Ethereum post-merge + // NOTE: do not use this on other chains + // randomOffset returns between 1 and MAX_SUPPLY + randomOffset = (uint256(keccak256(abi.encode(block.difficulty))) % (maxSupply - 1)) + 1; + + // Set revealed to true. + revealed = _REVEALED_TRUE; + } + + /** + * @notice The token URI, offset by randomOffset, to enable fair metadata + * reveals. + * + * @param tokenId The token id + */ + function tokenURI(uint256 tokenId) public view override returns (string memory) { + if (!_exists(tokenId)) { + revert URIQueryForNonexistentToken(); + } + + string memory base = _baseURI(); + if (bytes(base).length == 0) { + // If there is no baseURI set, return an empty string. + return ""; + } else if (revealed == _REVEALED_FALSE) { + // If the baseURI is set but the collection is not revealed yet, + // return just the baseURI. + return base; + } else { + // If the baseURI is set and the collection is revealed, + // return the tokenURI offset by the randomOffset. + return string.concat(base, _toString(((tokenId + randomOffset) % _maxSupply) + _startTokenId())); + } + } +} diff --git a/src/seadrop/src/extensions/ERC721SeaDropRedeemable.sol b/src/seadrop/src/extensions/ERC721SeaDropRedeemable.sol new file mode 100644 index 0000000..d45eaaa --- /dev/null +++ b/src/seadrop/src/extensions/ERC721SeaDropRedeemable.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {ERC721SeaDrop} from "../ERC721SeaDrop.sol"; + +import {OfferItem, ConsiderationItem} from "seaport-types/lib/ConsiderationStructs.sol"; + +interface IERC721RedemptionMintable { + enum ItemType { + NATIVE, + ERC20, + ERC721, + ERC1155, + ERC721_WITH_CRITERIA, + ERC1155_WITH_CRITERIA + } + + struct SpentItem { + ItemType itemType; + address token; + uint256 identifier; + uint256 amount; + } + + function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256 tokenId); +} + +struct CampaignParams { + uint32 startTime; + uint32 endTime; + uint32 maxCampaignRedemptions; + address manager; + address signer; + OfferItem[] offer; + ConsiderationItem[] consideration; +} + +/** + * @title ERC721SeaDropRedemptionMintable + * @author James Wenzel (emo.eth) + * @author Ryan Ghods (ralxz.eth) + * @author Stephan Min (stephanm.eth) + * @author Michael Cohen (notmichael.eth) + * @notice ERC721SeaDropRedemptionMintable is a token contract that extends + * ERC721SeaDrop to additionally add a mintRedemption function. + */ +contract ERC721SeaDropRedeemable is ERC721SeaDrop, IERC721RedemptionMintable { + address internal immutable _REDEEMABLE_CONTRACT_OFFERER; + address internal immutable _REDEEM_TOKEN; + + mapping(uint256 => uint256) public tokenURINumbers; + + /// @dev Revert if the sender of mintRedemption is not the redeemable contract offerer. + error InvalidSender(); + + /// @dev Revert if the redemption spent is not the required token. + error InvalidRedemption(); + + /** + * @notice Deploy the token contract with its name, symbol, + * and allowed SeaDrop addresses. + */ + constructor( + string memory name, + string memory symbol, + address[] memory allowedSeaDrop, + address redeemableContractOfferer, + address redeemToken + ) ERC721SeaDrop(name, symbol, allowedSeaDrop) { + _REDEEMABLE_CONTRACT_OFFERER = redeemableContractOfferer; + _REDEEM_TOKEN = redeemToken; + } + + /** + * @notice Only callable by the Redeemable Contract Offerer. + */ + function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256 tokenId) { + if (msg.sender != _REDEEMABLE_CONTRACT_OFFERER) revert InvalidSender(); + + SpentItem memory spentItem = spent[0]; + if (spentItem.token != _REDEEM_TOKEN) revert InvalidRedemption(); + + // Mint the same token ID redeemed. + _mint(to, 1); + + return _nextTokenId() - 1; + } + + /** + * @notice Hook to set tokenURINumber on mint. + */ + function _beforeTokenTransfers(address from, address, /* to */ uint256 startTokenId, uint256 quantity) + internal + virtual + override + { + // Set tokenURINumbers on mint. + if (from == address(0)) { + for (uint256 i = 0; i < quantity; i++) { + // 60% chance of tokenURI 1 + // 30% chance of tokenURI 2 + // 10% chance of tokenURI 3 + + // block.difficulty returns PREVRANDAO on Ethereum post-merge + // NOTE: do not use this on other chains + uint256 randomness = (uint256(keccak256(abi.encode(block.difficulty))) % 100) + 1; + + uint256 tokenURINumber = 1; + if (randomness >= 60 && randomness < 90) { + tokenURINumber = 2; + } else if (randomness >= 90) { + tokenURINumber = 3; + } + + tokenURINumbers[startTokenId + i] = tokenURINumber; + } + } + } + + /* + * @notice Overrides the `tokenURI()` function to return baseURI + 1, 2, or 3 + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + + string memory baseURI = _baseURI(); + uint256 tokenURINumber = tokenURINumbers[tokenId]; + + return string(abi.encodePacked(baseURI, _toString(tokenURINumber))); + } +} diff --git a/src/seadrop/src/extensions/ERC721SeaDropRedemptionMintable.sol b/src/seadrop/src/extensions/ERC721SeaDropRedemptionMintable.sol new file mode 100644 index 0000000..ed2516f --- /dev/null +++ b/src/seadrop/src/extensions/ERC721SeaDropRedemptionMintable.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {ERC721SeaDrop} from "../ERC721SeaDrop.sol"; + +interface IERC721RedemptionMintable { + enum ItemType { + NATIVE, + ERC20, + ERC721, + ERC1155, + ERC721_WITH_CRITERIA, + ERC1155_WITH_CRITERIA + } + + struct SpentItem { + ItemType itemType; + address token; + uint256 identifier; + uint256 amount; + } + + function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256 tokenId); +} + +/** + * @title ERC721SeaDropRedemptionMintable + * @author James Wenzel (emo.eth) + * @author Ryan Ghods (ralxz.eth) + * @author Stephan Min (stephanm.eth) + * @author Michael Cohen (notmichael.eth) + * @notice ERC721SeaDropRedemptionMintable is a token contract that extends + * ERC721SeaDrop to additionally add a mintRedemption function. + */ +contract ERC721SeaDropRedemptionMintable is ERC721SeaDrop, IERC721RedemptionMintable { + address internal immutable _REDEEMABLE_CONTRACT_OFFERER; + address internal immutable _REDEEM_TOKEN; + + mapping(uint256 => uint256) public tokenURINumbers; + + /// @dev Revert if the sender of mintRedemption is not the redeemable contract offerer. + error InvalidSender(); + + /// @dev Revert if the redemption spent is not the required token. + error InvalidRedemption(); + + /** + * @notice Deploy the token contract with its name, symbol, + * and allowed SeaDrop addresses. + */ + constructor( + string memory name, + string memory symbol, + address[] memory allowedSeaDrop, + address redeemableContractOfferer, + address redeemToken + ) ERC721SeaDrop(name, symbol, allowedSeaDrop) { + _REDEEMABLE_CONTRACT_OFFERER = redeemableContractOfferer; + _REDEEM_TOKEN = redeemToken; + } + + /** + * @notice Only callable by the Redeemable Contract Offerer. + */ + function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256 tokenId) { + if (msg.sender != _REDEEMABLE_CONTRACT_OFFERER) revert InvalidSender(); + + SpentItem memory spentItem = spent[0]; + if (spentItem.token != _REDEEM_TOKEN) revert InvalidRedemption(); + + // Mint the same token ID redeemed. + _mint(to, 1); + + return _nextTokenId() - 1; + } + + /** + * @notice Hook to set tokenURINumber on mint. + */ + function _beforeTokenTransfers(address from, address, /* to */ uint256 startTokenId, uint256 quantity) + internal + virtual + override + { + // Set tokenURINumbers on mint. + if (from == address(0)) { + for (uint256 i = 0; i < quantity; i++) { + // 60% chance of tokenURI 1 + // 30% chance of tokenURI 2 + // 10% chance of tokenURI 3 + + // block.difficulty returns PREVRANDAO on Ethereum post-merge + // NOTE: do not use this on other chains + uint256 randomness = (uint256(keccak256(abi.encode(block.difficulty))) % 100) + 1; + + uint256 tokenURINumber = 1; + if (randomness >= 60 && randomness < 90) { + tokenURINumber = 2; + } else if (randomness >= 90) { + tokenURINumber = 3; + } + + tokenURINumbers[startTokenId + i] = tokenURINumber; + } + } + } + + /* + * @notice Overrides the `tokenURI()` function to return baseURI + 1, 2, or 3 + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + + string memory baseURI = _baseURI(); + uint256 tokenURINumber = tokenURINumbers[tokenId]; + + return string(abi.encodePacked(baseURI, _toString(tokenURINumber))); + } +} diff --git a/src/seadrop/src/interfaces/INonFungibleSeaDropToken.sol b/src/seadrop/src/interfaces/INonFungibleSeaDropToken.sol new file mode 100644 index 0000000..a5f8c70 --- /dev/null +++ b/src/seadrop/src/interfaces/INonFungibleSeaDropToken.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {ISeaDropTokenContractMetadata} from "./ISeaDropTokenContractMetadata.sol"; + +import {AllowListData, PublicDrop, TokenGatedDropStage, SignedMintValidationParams} from "../lib/SeaDropStructs.sol"; + +interface INonFungibleSeaDropToken is ISeaDropTokenContractMetadata { + /** + * @dev Revert with an error if a contract is not an allowed + * SeaDrop address. + */ + error OnlyAllowedSeaDrop(); + + /** + * @dev Emit an event when allowed SeaDrop contracts are updated. + */ + event AllowedSeaDropUpdated(address[] allowedSeaDrop); + + /** + * @notice Update the allowed SeaDrop contracts. + * Only the owner or administrator can use this function. + * + * @param allowedSeaDrop The allowed SeaDrop addresses. + */ + function updateAllowedSeaDrop(address[] calldata allowedSeaDrop) external; + + /** + * @notice Mint tokens, restricted to the SeaDrop contract. + * + * @dev NOTE: If a token registers itself with multiple SeaDrop + * contracts, the implementation of this function should guard + * against reentrancy. If the implementing token uses + * _safeMint(), or a feeRecipient with a malicious receive() hook + * is specified, the token or fee recipients may be able to execute + * another mint in the same transaction via a separate SeaDrop + * contract. + * This is dangerous if an implementing token does not correctly + * update the minterNumMinted and currentTotalSupply values before + * transferring minted tokens, as SeaDrop references these values + * to enforce token limits on a per-wallet and per-stage basis. + * + * @param minter The address to mint to. + * @param quantity The number of tokens to mint. + */ + function mintSeaDrop(address minter, uint256 quantity) external; + + /** + * @notice Returns a set of mint stats for the address. + * This assists SeaDrop in enforcing maxSupply, + * maxTotalMintableByWallet, and maxTokenSupplyForStage checks. + * + * @dev NOTE: Implementing contracts should always update these numbers + * before transferring any tokens with _safeMint() to mitigate + * consequences of malicious onERC721Received() hooks. + * + * @param minter The minter address. + */ + function getMintStats(address minter) + external + view + returns (uint256 minterNumMinted, uint256 currentTotalSupply, uint256 maxSupply); + + /** + * @notice Update the public drop data for this nft contract on SeaDrop. + * Only the owner or administrator can use this function. + * + * The administrator can only update `feeBps`. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param publicDrop The public drop data. + */ + function updatePublicDrop(address seaDropImpl, PublicDrop calldata publicDrop) external; + + /** + * @notice Update the allow list data for this nft contract on SeaDrop. + * Only the owner or administrator can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param allowListData The allow list data. + */ + function updateAllowList(address seaDropImpl, AllowListData calldata allowListData) external; + + /** + * @notice Update the token gated drop stage data for this nft contract + * on SeaDrop. + * Only the owner or administrator can use this function. + * + * The administrator, when present, must first set `feeBps`. + * + * Note: If two INonFungibleSeaDropToken tokens are doing + * simultaneous token gated drop promotions for each other, + * they can be minted by the same actor until + * `maxTokenSupplyForStage` is reached. Please ensure the + * `allowedNftToken` is not running an active drop during the + * `dropStage` time period. + * + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param allowedNftToken The allowed nft token. + * @param dropStage The token gated drop stage data. + */ + function updateTokenGatedDrop(address seaDropImpl, address allowedNftToken, TokenGatedDropStage calldata dropStage) + external; + + /** + * @notice Update the drop URI for this nft contract on SeaDrop. + * Only the owner or administrator can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param dropURI The new drop URI. + */ + function updateDropURI(address seaDropImpl, string calldata dropURI) external; + + /** + * @notice Update the creator payout address for this nft contract on + * SeaDrop. + * Only the owner can set the creator payout address. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param payoutAddress The new payout address. + */ + function updateCreatorPayoutAddress(address seaDropImpl, address payoutAddress) external; + + /** + * @notice Update the allowed fee recipient for this nft contract + * on SeaDrop. + * Only the administrator can set the allowed fee recipient. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param feeRecipient The new fee recipient. + */ + function updateAllowedFeeRecipient(address seaDropImpl, address feeRecipient, bool allowed) external; + + /** + * @notice Update the server-side signers for this nft contract + * on SeaDrop. + * Only the owner or administrator can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param signer The signer to update. + * @param signedMintValidationParams Minimum and maximum parameters + * to enforce for signed mints. + */ + function updateSignedMintValidationParams( + address seaDropImpl, + address signer, + SignedMintValidationParams memory signedMintValidationParams + ) external; + + /** + * @notice Update the allowed payers for this nft contract on SeaDrop. + * Only the owner or administrator can use this function. + * + * @param seaDropImpl The allowed SeaDrop contract. + * @param payer The payer to update. + * @param allowed Whether the payer is allowed. + */ + function updatePayer(address seaDropImpl, address payer, bool allowed) external; +} diff --git a/src/seadrop/src/interfaces/ISeaDrop.sol b/src/seadrop/src/interfaces/ISeaDrop.sol new file mode 100644 index 0000000..8e2e7c6 --- /dev/null +++ b/src/seadrop/src/interfaces/ISeaDrop.sol @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import { + AllowListData, + MintParams, + PublicDrop, + TokenGatedDropStage, + TokenGatedMintParams, + SignedMintValidationParams +} from "../lib/SeaDropStructs.sol"; + +import {SeaDropErrorsAndEvents} from "../lib/SeaDropErrorsAndEvents.sol"; + +interface ISeaDrop is SeaDropErrorsAndEvents { + /** + * @notice Mint a public drop. + * + * @param nftContract The nft contract to mint. + * @param feeRecipient The fee recipient. + * @param minterIfNotPayer The mint recipient if different than the payer. + * @param quantity The number of tokens to mint. + */ + function mintPublic(address nftContract, address feeRecipient, address minterIfNotPayer, uint256 quantity) + external + payable; + + /** + * @notice Mint from an allow list. + * + * @param nftContract The nft contract to mint. + * @param feeRecipient The fee recipient. + * @param minterIfNotPayer The mint recipient if different than the payer. + * @param quantity The number of tokens to mint. + * @param mintParams The mint parameters. + * @param proof The proof for the leaf of the allow list. + */ + function mintAllowList( + address nftContract, + address feeRecipient, + address minterIfNotPayer, + uint256 quantity, + MintParams calldata mintParams, + bytes32[] calldata proof + ) external payable; + + /** + * @notice Mint with a server-side signature. + * Note that a signature can only be used once. + * + * @param nftContract The nft contract to mint. + * @param feeRecipient The fee recipient. + * @param minterIfNotPayer The mint recipient if different than the payer. + * @param quantity The number of tokens to mint. + * @param mintParams The mint parameters. + * @param salt The sale for the signed mint. + * @param signature The server-side signature, must be an allowed + * signer. + */ + function mintSigned( + address nftContract, + address feeRecipient, + address minterIfNotPayer, + uint256 quantity, + MintParams calldata mintParams, + uint256 salt, + bytes calldata signature + ) external payable; + + /** + * @notice Mint as an allowed token holder. + * This will mark the token id as redeemed and will revert if the + * same token id is attempted to be redeemed twice. + * + * @param nftContract The nft contract to mint. + * @param feeRecipient The fee recipient. + * @param minterIfNotPayer The mint recipient if different than the payer. + * @param mintParams The token gated mint params. + */ + function mintAllowedTokenHolder( + address nftContract, + address feeRecipient, + address minterIfNotPayer, + TokenGatedMintParams calldata mintParams + ) external payable; + + /** + * @notice Emits an event to notify update of the drop URI. + * + * This method assume msg.sender is an nft contract and its + * ERC165 interface id matches INonFungibleSeaDropToken. + * + * Note: Be sure only authorized users can call this from + * token contracts that implement INonFungibleSeaDropToken. + * + * @param dropURI The new drop URI. + */ + function updateDropURI(string calldata dropURI) external; + + /** + * @notice Updates the public drop data for the nft contract + * and emits an event. + * + * This method assume msg.sender is an nft contract and its + * ERC165 interface id matches INonFungibleSeaDropToken. + * + * Note: Be sure only authorized users can call this from + * token contracts that implement INonFungibleSeaDropToken. + * + * @param publicDrop The public drop data. + */ + function updatePublicDrop(PublicDrop calldata publicDrop) external; + + /** + * @notice Updates the allow list merkle root for the nft contract + * and emits an event. + * + * This method assume msg.sender is an nft contract and its + * ERC165 interface id matches INonFungibleSeaDropToken. + * + * Note: Be sure only authorized users can call this from + * token contracts that implement INonFungibleSeaDropToken. + * + * @param allowListData The allow list data. + */ + function updateAllowList(AllowListData calldata allowListData) external; + + /** + * @notice Updates the token gated drop stage for the nft contract + * and emits an event. + * + * This method assume msg.sender is an nft contract and its + * ERC165 interface id matches INonFungibleSeaDropToken. + * + * Note: Be sure only authorized users can call this from + * token contracts that implement INonFungibleSeaDropToken. + * + * Note: If two INonFungibleSeaDropToken tokens are doing + * simultaneous token gated drop promotions for each other, + * they can be minted by the same actor until + * `maxTokenSupplyForStage` is reached. Please ensure the + * `allowedNftToken` is not running an active drop during + * the `dropStage` time period. + * + * @param allowedNftToken The token gated nft token. + * @param dropStage The token gated drop stage data. + */ + function updateTokenGatedDrop(address allowedNftToken, TokenGatedDropStage calldata dropStage) external; + + /** + * @notice Updates the creator payout address and emits an event. + * + * This method assume msg.sender is an nft contract and its + * ERC165 interface id matches INonFungibleSeaDropToken. + * + * Note: Be sure only authorized users can call this from + * token contracts that implement INonFungibleSeaDropToken. + * + * @param payoutAddress The creator payout address. + */ + function updateCreatorPayoutAddress(address payoutAddress) external; + + /** + * @notice Updates the allowed fee recipient and emits an event. + * + * This method assume msg.sender is an nft contract and its + * ERC165 interface id matches INonFungibleSeaDropToken. + * + * Note: Be sure only authorized users can call this from + * token contracts that implement INonFungibleSeaDropToken. + * + * @param feeRecipient The fee recipient. + * @param allowed If the fee recipient is allowed. + */ + function updateAllowedFeeRecipient(address feeRecipient, bool allowed) external; + + /** + * @notice Updates the allowed server-side signers and emits an event. + * + * This method assume msg.sender is an nft contract and its + * ERC165 interface id matches INonFungibleSeaDropToken. + * + * Note: Be sure only authorized users can call this from + * token contracts that implement INonFungibleSeaDropToken. + * + * @param signer The signer to update. + * @param signedMintValidationParams Minimum and maximum parameters + * to enforce for signed mints. + */ + function updateSignedMintValidationParams( + address signer, + SignedMintValidationParams calldata signedMintValidationParams + ) external; + + /** + * @notice Updates the allowed payer and emits an event. + * + * This method assume msg.sender is an nft contract and its + * ERC165 interface id matches INonFungibleSeaDropToken. + * + * Note: Be sure only authorized users can call this from + * token contracts that implement INonFungibleSeaDropToken. + * + * @param payer The payer to add or remove. + * @param allowed Whether to add or remove the payer. + */ + function updatePayer(address payer, bool allowed) external; + + /** + * @notice Returns the public drop data for the nft contract. + * + * @param nftContract The nft contract. + */ + function getPublicDrop(address nftContract) external view returns (PublicDrop memory); + + /** + * @notice Returns the creator payout address for the nft contract. + * + * @param nftContract The nft contract. + */ + function getCreatorPayoutAddress(address nftContract) external view returns (address); + + /** + * @notice Returns the allow list merkle root for the nft contract. + * + * @param nftContract The nft contract. + */ + function getAllowListMerkleRoot(address nftContract) external view returns (bytes32); + + /** + * @notice Returns if the specified fee recipient is allowed + * for the nft contract. + * + * @param nftContract The nft contract. + * @param feeRecipient The fee recipient. + */ + function getFeeRecipientIsAllowed(address nftContract, address feeRecipient) external view returns (bool); + + /** + * @notice Returns an enumeration of allowed fee recipients for an + * nft contract when fee recipients are enforced + * + * @param nftContract The nft contract. + */ + function getAllowedFeeRecipients(address nftContract) external view returns (address[] memory); + + /** + * @notice Returns the server-side signers for the nft contract. + * + * @param nftContract The nft contract. + */ + function getSigners(address nftContract) external view returns (address[] memory); + + /** + * @notice Returns the struct of SignedMintValidationParams for a signer. + * + * @param nftContract The nft contract. + * @param signer The signer. + */ + function getSignedMintValidationParams(address nftContract, address signer) + external + view + returns (SignedMintValidationParams memory); + + /** + * @notice Returns the payers for the nft contract. + * + * @param nftContract The nft contract. + */ + function getPayers(address nftContract) external view returns (address[] memory); + + /** + * @notice Returns if the specified payer is allowed + * for the nft contract. + * + * @param nftContract The nft contract. + * @param payer The payer. + */ + function getPayerIsAllowed(address nftContract, address payer) external view returns (bool); + + /** + * @notice Returns the allowed token gated drop tokens for the nft contract. + * + * @param nftContract The nft contract. + */ + function getTokenGatedAllowedTokens(address nftContract) external view returns (address[] memory); + + /** + * @notice Returns the token gated drop data for the nft contract + * and token gated nft. + * + * @param nftContract The nft contract. + * @param allowedNftToken The token gated nft token. + */ + function getTokenGatedDrop(address nftContract, address allowedNftToken) + external + view + returns (TokenGatedDropStage memory); + + /** + * @notice Returns whether the token id for a token gated drop has been + * redeemed. + * + * @param nftContract The nft contract. + * @param allowedNftToken The token gated nft token. + * @param allowedNftTokenId The token gated nft token id to check. + */ + function getAllowedNftTokenIdIsRedeemed(address nftContract, address allowedNftToken, uint256 allowedNftTokenId) + external + view + returns (bool); +} diff --git a/src/seadrop/src/interfaces/ISeaDropTokenContractMetadata.sol b/src/seadrop/src/interfaces/ISeaDropTokenContractMetadata.sol new file mode 100644 index 0000000..ce7faa0 --- /dev/null +++ b/src/seadrop/src/interfaces/ISeaDropTokenContractMetadata.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {IERC2981} from "openzeppelin-contracts/interfaces/IERC2981.sol"; + +interface ISeaDropTokenContractMetadata is IERC2981 { + /** + * @notice Throw if the max supply exceeds uint64, a limit + * due to the storage of bit-packed variables in ERC721A. + */ + error CannotExceedMaxSupplyOfUint64(uint256 newMaxSupply); + + /** + * @dev Revert with an error when attempting to set the provenance + * hash after the mint has started. + */ + error ProvenanceHashCannotBeSetAfterMintStarted(); + + /** + * @dev Revert if the royalty basis points is greater than 10_000. + */ + error InvalidRoyaltyBasisPoints(uint256 basisPoints); + + /** + * @dev Revert if the royalty address is being set to the zero address. + */ + error RoyaltyAddressCannotBeZeroAddress(); + + /** + * @dev Emit an event for token metadata reveals/updates, + * according to EIP-4906. + * + * @param _fromTokenId The start token id. + * @param _toTokenId The end token id. + */ + event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); + + /** + * @dev Emit an event when the URI for the collection-level metadata + * is updated. + */ + event ContractURIUpdated(string newContractURI); + + /** + * @dev Emit an event when the max token supply is updated. + */ + event MaxSupplyUpdated(uint256 newMaxSupply); + + /** + * @dev Emit an event with the previous and new provenance hash after + * being updated. + */ + event ProvenanceHashUpdated(bytes32 previousHash, bytes32 newHash); + + /** + * @dev Emit an event when the royalties info is updated. + */ + event RoyaltyInfoUpdated(address receiver, uint256 bps); + + /** + * @notice A struct defining royalty info for the contract. + */ + struct RoyaltyInfo { + address royaltyAddress; + uint96 royaltyBps; + } + + /** + * @notice Sets the base URI for the token metadata and emits an event. + * + * @param tokenURI The new base URI to set. + */ + function setBaseURI(string calldata tokenURI) external; + + /** + * @notice Sets the contract URI for contract metadata. + * + * @param newContractURI The new contract URI. + */ + function setContractURI(string calldata newContractURI) external; + + /** + * @notice Sets the max supply and emits an event. + * + * @param newMaxSupply The new max supply to set. + */ + function setMaxSupply(uint256 newMaxSupply) external; + + /** + * @notice Sets the provenance hash and emits an event. + * + * The provenance hash is used for random reveals, which + * is a hash of the ordered metadata to show it has not been + * modified after mint started. + * + * This function will revert after the first item has been minted. + * + * @param newProvenanceHash The new provenance hash to set. + */ + function setProvenanceHash(bytes32 newProvenanceHash) external; + + /** + * @notice Sets the address and basis points for royalties. + * + * @param newInfo The struct to configure royalties. + */ + function setRoyaltyInfo(RoyaltyInfo calldata newInfo) external; + + /** + * @notice Returns the base URI for token metadata. + */ + function baseURI() external view returns (string memory); + + /** + * @notice Returns the contract URI. + */ + function contractURI() external view returns (string memory); + + /** + * @notice Returns the max token supply. + */ + function maxSupply() external view returns (uint256); + + /** + * @notice Returns the provenance hash. + * The provenance hash is used for random reveals, which + * is a hash of the ordered metadata to show it is unmodified + * after mint has started. + */ + function provenanceHash() external view returns (bytes32); + + /** + * @notice Returns the address that receives royalties. + */ + function royaltyAddress() external view returns (address); + + /** + * @notice Returns the royalty basis points out of 10_000. + */ + function royaltyBasisPoints() external view returns (uint256); +} diff --git a/src/seadrop/src/lib/ERC721SeaDropStructsErrorsAndEvents.sol b/src/seadrop/src/lib/ERC721SeaDropStructsErrorsAndEvents.sol new file mode 100644 index 0000000..4b63654 --- /dev/null +++ b/src/seadrop/src/lib/ERC721SeaDropStructsErrorsAndEvents.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {AllowListData, PublicDrop, SignedMintValidationParams, TokenGatedDropStage} from "./SeaDropStructs.sol"; + +interface ERC721SeaDropStructsErrorsAndEvents { + /** + * @notice Revert with an error if mint exceeds the max supply. + */ + error MintQuantityExceedsMaxSupply(uint256 total, uint256 maxSupply); + + /** + * @notice Revert with an error if the number of token gated + * allowedNftTokens doesn't match the length of supplied + * drop stages. + */ + error TokenGatedMismatch(); + + /** + * @notice Revert with an error if the number of signers doesn't match + * the length of supplied signedMintValidationParams + */ + error SignersMismatch(); + + /** + * @notice An event to signify that a SeaDrop token contract was deployed. + */ + event SeaDropTokenDeployed(); + + /** + * @notice A struct to configure multiple contract options at a time. + */ + struct MultiConfigureStruct { + uint256 maxSupply; + string baseURI; + string contractURI; + address seaDropImpl; + PublicDrop publicDrop; + string dropURI; + AllowListData allowListData; + address creatorPayoutAddress; + bytes32 provenanceHash; + address[] allowedFeeRecipients; + address[] disallowedFeeRecipients; + address[] allowedPayers; + address[] disallowedPayers; + // Token-gated + address[] tokenGatedAllowedNftTokens; + TokenGatedDropStage[] tokenGatedDropStages; + address[] disallowedTokenGatedAllowedNftTokens; + // Server-signed + address[] signers; + SignedMintValidationParams[] signedMintValidationParams; + address[] disallowedSigners; + } +} diff --git a/src/seadrop/src/lib/SeaDropErrorsAndEvents.sol b/src/seadrop/src/lib/SeaDropErrorsAndEvents.sol new file mode 100644 index 0000000..2fe2375 --- /dev/null +++ b/src/seadrop/src/lib/SeaDropErrorsAndEvents.sol @@ -0,0 +1,288 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {PublicDrop, TokenGatedDropStage, SignedMintValidationParams} from "./SeaDropStructs.sol"; + +interface SeaDropErrorsAndEvents { + /** + * @dev Revert with an error if the drop stage is not active. + */ + error NotActive(uint256 currentTimestamp, uint256 startTimestamp, uint256 endTimestamp); + + /** + * @dev Revert with an error if the mint quantity is zero. + */ + error MintQuantityCannotBeZero(); + + /** + * @dev Revert with an error if the mint quantity exceeds the max allowed + * to be minted per wallet. + */ + error MintQuantityExceedsMaxMintedPerWallet(uint256 total, uint256 allowed); + + /** + * @dev Revert with an error if the mint quantity exceeds the max token + * supply. + */ + error MintQuantityExceedsMaxSupply(uint256 total, uint256 maxSupply); + + /** + * @dev Revert with an error if the mint quantity exceeds the max token + * supply for the stage. + * Note: The `maxTokenSupplyForStage` for public mint is + * always `type(uint).max`. + */ + error MintQuantityExceedsMaxTokenSupplyForStage(uint256 total, uint256 maxTokenSupplyForStage); + + /** + * @dev Revert if the fee recipient is the zero address. + */ + error FeeRecipientCannotBeZeroAddress(); + + /** + * @dev Revert if the fee recipient is not already included. + */ + error FeeRecipientNotPresent(); + + /** + * @dev Revert if the fee basis points is greater than 10_000. + */ + error InvalidFeeBps(uint256 feeBps); + + /** + * @dev Revert if the fee recipient is already included. + */ + error DuplicateFeeRecipient(); + + /** + * @dev Revert if the fee recipient is restricted and not allowed. + */ + error FeeRecipientNotAllowed(); + + /** + * @dev Revert if the creator payout address is the zero address. + */ + error CreatorPayoutAddressCannotBeZeroAddress(); + + /** + * @dev Revert with an error if the received payment is incorrect. + */ + error IncorrectPayment(uint256 got, uint256 want); + + /** + * @dev Revert with an error if the allow list proof is invalid. + */ + error InvalidProof(); + + /** + * @dev Revert if a supplied signer address is the zero address. + */ + error SignerCannotBeZeroAddress(); + + /** + * @dev Revert with an error if signer's signature is invalid. + */ + error InvalidSignature(address recoveredSigner); + + /** + * @dev Revert with an error if a signer is not included in + * the enumeration when removing. + */ + error SignerNotPresent(); + + /** + * @dev Revert with an error if a payer is not included in + * the enumeration when removing. + */ + error PayerNotPresent(); + + /** + * @dev Revert with an error if a payer is already included in mapping + * when adding. + * Note: only applies when adding a single payer, as duplicates in + * enumeration can be removed with updatePayer. + */ + error DuplicatePayer(); + + /** + * @dev Revert with an error if the payer is not allowed. The minter must + * pay for their own mint. + */ + error PayerNotAllowed(); + + /** + * @dev Revert if a supplied payer address is the zero address. + */ + error PayerCannotBeZeroAddress(); + + /** + * @dev Revert with an error if the sender does not + * match the INonFungibleSeaDropToken interface. + */ + error OnlyINonFungibleSeaDropToken(address sender); + + /** + * @dev Revert with an error if the sender of a token gated supplied + * drop stage redeem is not the owner of the token. + */ + error TokenGatedNotTokenOwner(address nftContract, address allowedNftToken, uint256 allowedNftTokenId); + + /** + * @dev Revert with an error if the token id has already been used to + * redeem a token gated drop stage. + */ + error TokenGatedTokenIdAlreadyRedeemed(address nftContract, address allowedNftToken, uint256 allowedNftTokenId); + + /** + * @dev Revert with an error if an empty TokenGatedDropStage is provided + * for an already-empty TokenGatedDropStage. + */ + error TokenGatedDropStageNotPresent(); + + /** + * @dev Revert with an error if an allowedNftToken is set to + * the zero address. + */ + error TokenGatedDropAllowedNftTokenCannotBeZeroAddress(); + + /** + * @dev Revert with an error if an allowedNftToken is set to + * the drop token itself. + */ + error TokenGatedDropAllowedNftTokenCannotBeDropToken(); + + /** + * @dev Revert with an error if supplied signed mint price is less than + * the minimum specified. + */ + error InvalidSignedMintPrice(uint256 got, uint256 minimum); + + /** + * @dev Revert with an error if supplied signed maxTotalMintableByWallet + * is greater than the maximum specified. + */ + error InvalidSignedMaxTotalMintableByWallet(uint256 got, uint256 maximum); + + /** + * @dev Revert with an error if supplied signed start time is less than + * the minimum specified. + */ + error InvalidSignedStartTime(uint256 got, uint256 minimum); + + /** + * @dev Revert with an error if supplied signed end time is greater than + * the maximum specified. + */ + error InvalidSignedEndTime(uint256 got, uint256 maximum); + + /** + * @dev Revert with an error if supplied signed maxTokenSupplyForStage + * is greater than the maximum specified. + */ + error InvalidSignedMaxTokenSupplyForStage(uint256 got, uint256 maximum); + + /** + * @dev Revert with an error if supplied signed feeBps is greater than + * the maximum specified, or less than the minimum. + */ + error InvalidSignedFeeBps(uint256 got, uint256 minimumOrMaximum); + + /** + * @dev Revert with an error if signed mint did not specify to restrict + * fee recipients. + */ + error SignedMintsMustRestrictFeeRecipients(); + + /** + * @dev Revert with an error if a signature for a signed mint has already + * been used. + */ + error SignatureAlreadyUsed(); + + /** + * @dev An event with details of a SeaDrop mint, for analytical purposes. + * + * @param nftContract The nft contract. + * @param minter The mint recipient. + * @param feeRecipient The fee recipient. + * @param payer The address who payed for the tx. + * @param quantityMinted The number of tokens minted. + * @param unitMintPrice The amount paid for each token. + * @param feeBps The fee out of 10_000 basis points collected. + * @param dropStageIndex The drop stage index. Items minted + * through mintPublic() have + * dropStageIndex of 0. + */ + event SeaDropMint( + address indexed nftContract, + address indexed minter, + address indexed feeRecipient, + address payer, + uint256 quantityMinted, + uint256 unitMintPrice, + uint256 feeBps, + uint256 dropStageIndex + ); + + /** + * @dev An event with updated public drop data for an nft contract. + */ + event PublicDropUpdated(address indexed nftContract, PublicDrop publicDrop); + + /** + * @dev An event with updated token gated drop stage data + * for an nft contract. + */ + event TokenGatedDropStageUpdated( + address indexed nftContract, address indexed allowedNftToken, TokenGatedDropStage dropStage + ); + + /** + * @dev An event with updated allow list data for an nft contract. + * + * @param nftContract The nft contract. + * @param previousMerkleRoot The previous allow list merkle root. + * @param newMerkleRoot The new allow list merkle root. + * @param publicKeyURI If the allow list is encrypted, the public key + * URIs that can decrypt the list. + * Empty if unencrypted. + * @param allowListURI The URI for the allow list. + */ + event AllowListUpdated( + address indexed nftContract, + bytes32 indexed previousMerkleRoot, + bytes32 indexed newMerkleRoot, + string[] publicKeyURI, + string allowListURI + ); + + /** + * @dev An event with updated drop URI for an nft contract. + */ + event DropURIUpdated(address indexed nftContract, string newDropURI); + + /** + * @dev An event with the updated creator payout address for an nft + * contract. + */ + event CreatorPayoutAddressUpdated(address indexed nftContract, address indexed newPayoutAddress); + + /** + * @dev An event with the updated allowed fee recipient for an nft + * contract. + */ + event AllowedFeeRecipientUpdated(address indexed nftContract, address indexed feeRecipient, bool indexed allowed); + + /** + * @dev An event with the updated validation parameters for server-side + * signers. + */ + event SignedMintValidationParamsUpdated( + address indexed nftContract, address indexed signer, SignedMintValidationParams signedMintValidationParams + ); + + /** + * @dev An event with the updated payer for an nft contract. + */ + event PayerUpdated(address indexed nftContract, address indexed payer, bool indexed allowed); +} diff --git a/src/seadrop/src/lib/SeaDropStructs.sol b/src/seadrop/src/lib/SeaDropStructs.sol new file mode 100644 index 0000000..88b3f7d --- /dev/null +++ b/src/seadrop/src/lib/SeaDropStructs.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +/** + * @notice A struct defining public drop data. + * Designed to fit efficiently in one storage slot. + * + * @param mintPrice The mint price per token. (Up to 1.2m + * of native token, e.g. ETH, MATIC) + * @param startTime The start time, ensure this is not zero. + * @param endTIme The end time, ensure this is not zero. + * @param maxTotalMintableByWallet Maximum total number of mints a user is + * allowed. (The limit for this field is + * 2^16 - 1) + * @param feeBps Fee out of 10_000 basis points to be + * collected. + * @param restrictFeeRecipients If false, allow any fee recipient; + * if true, check fee recipient is allowed. + */ +struct PublicDrop { + uint80 mintPrice; // 80/256 bits + uint48 startTime; // 128/256 bits + uint48 endTime; // 176/256 bits + uint16 maxTotalMintableByWallet; // 224/256 bits + uint16 feeBps; // 240/256 bits + bool restrictFeeRecipients; // 248/256 bits +} + +/** + * @notice A struct defining token gated drop stage data. + * Designed to fit efficiently in one storage slot. + * + * @param mintPrice The mint price per token. (Up to 1.2m + * of native token, e.g.: ETH, MATIC) + * @param maxTotalMintableByWallet Maximum total number of mints a user is + * allowed. (The limit for this field is + * 2^16 - 1) + * @param startTime The start time, ensure this is not zero. + * @param endTime The end time, ensure this is not zero. + * @param dropStageIndex The drop stage index to emit with the event + * for analytical purposes. This should be + * non-zero since the public mint emits + * with index zero. + * @param maxTokenSupplyForStage The limit of token supply this stage can + * mint within. (The limit for this field is + * 2^16 - 1) + * @param feeBps Fee out of 10_000 basis points to be + * collected. + * @param restrictFeeRecipients If false, allow any fee recipient; + * if true, check fee recipient is allowed. + */ +struct TokenGatedDropStage { + uint80 mintPrice; // 80/256 bits + uint16 maxTotalMintableByWallet; // 96/256 bits + uint48 startTime; // 144/256 bits + uint48 endTime; // 192/256 bits + uint8 dropStageIndex; // non-zero. 200/256 bits + uint32 maxTokenSupplyForStage; // 232/256 bits + uint16 feeBps; // 248/256 bits + bool restrictFeeRecipients; // 256/256 bits +} + +/** + * @notice A struct defining mint params for an allow list. + * An allow list leaf will be composed of `msg.sender` and + * the following params. + * + * Note: Since feeBps is encoded in the leaf, backend should ensure + * that feeBps is acceptable before generating a proof. + * + * @param mintPrice The mint price per token. + * @param maxTotalMintableByWallet Maximum total number of mints a user is + * allowed. + * @param startTime The start time, ensure this is not zero. + * @param endTime The end time, ensure this is not zero. + * @param dropStageIndex The drop stage index to emit with the event + * for analytical purposes. This should be + * non-zero since the public mint emits with + * index zero. + * @param maxTokenSupplyForStage The limit of token supply this stage can + * mint within. + * @param feeBps Fee out of 10_000 basis points to be + * collected. + * @param restrictFeeRecipients If false, allow any fee recipient; + * if true, check fee recipient is allowed. + */ +struct MintParams { + uint256 mintPrice; + uint256 maxTotalMintableByWallet; + uint256 startTime; + uint256 endTime; + uint256 dropStageIndex; // non-zero + uint256 maxTokenSupplyForStage; + uint256 feeBps; + bool restrictFeeRecipients; +} + +/** + * @notice A struct defining token gated mint params. + * + * @param allowedNftToken The allowed nft token contract address. + * @param allowedNftTokenIds The token ids to redeem. + */ +struct TokenGatedMintParams { + address allowedNftToken; + uint256[] allowedNftTokenIds; +} + +/** + * @notice A struct defining allow list data (for minting an allow list). + * + * @param merkleRoot The merkle root for the allow list. + * @param publicKeyURIs If the allowListURI is encrypted, a list of URIs + * pointing to the public keys. Empty if unencrypted. + * @param allowListURI The URI for the allow list. + */ +struct AllowListData { + bytes32 merkleRoot; + string[] publicKeyURIs; + string allowListURI; +} + +/** + * @notice A struct defining minimum and maximum parameters to validate for + * signed mints, to minimize negative effects of a compromised signer. + * + * @param minMintPrice The minimum mint price allowed. + * @param maxMaxTotalMintableByWallet The maximum total number of mints allowed + * by a wallet. + * @param minStartTime The minimum start time allowed. + * @param maxEndTime The maximum end time allowed. + * @param maxMaxTokenSupplyForStage The maximum token supply allowed. + * @param minFeeBps The minimum fee allowed. + * @param maxFeeBps The maximum fee allowed. + */ +struct SignedMintValidationParams { + uint80 minMintPrice; // 80/256 bits + uint24 maxMaxTotalMintableByWallet; // 104/256 bits + uint40 minStartTime; // 144/256 bits + uint40 maxEndTime; // 184/256 bits + uint40 maxMaxTokenSupplyForStage; // 224/256 bits + uint16 minFeeBps; // 240/256 bits + uint16 maxFeeBps; // 256/256 bits +} diff --git a/src/seadrop/src/shim/Shim.sol b/src/seadrop/src/shim/Shim.sol new file mode 100644 index 0000000..1d3ba37 --- /dev/null +++ b/src/seadrop/src/shim/Shim.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +/** + * @dev HardHat doesn't support multiple source folders; so import everything + * extra that hardhat tests rely on so they get compiled. Allows for faster + * feedback than running an extra yarn build. + */ +import {TestERC721} from "../test/TestERC721.sol"; +import {MaliciousRecipient} from "../test/MaliciousRecipient.sol"; diff --git a/src/seadrop/src/test/MaliciousRecipient.sol b/src/seadrop/src/test/MaliciousRecipient.sol new file mode 100644 index 0000000..b558859 --- /dev/null +++ b/src/seadrop/src/test/MaliciousRecipient.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {SeaDrop} from "../SeaDrop.sol"; + +contract MaliciousRecipient { + bool public startAttack; + address public token; + SeaDrop public seaDrop; + + receive() external payable { + if (startAttack) { + startAttack = false; + seaDrop.mintPublic{value: 1 ether}({ + nftContract: token, + feeRecipient: address(this), + minterIfNotPayer: address(this), + quantity: 1 + }); + } + } + + // Also receive some eth in the process + function setStartAttack() public payable { + startAttack = true; + } + + function attack(SeaDrop _seaDrop, address _token) external payable { + token = _token; + seaDrop = _seaDrop; + + _seaDrop.mintPublic{value: 1 ether}({ + nftContract: _token, + feeRecipient: address(this), + minterIfNotPayer: address(this), + quantity: 1 + }); + + token = address(0); + seaDrop = SeaDrop(address(0)); + } +} diff --git a/src/seadrop/src/test/TestERC721.sol b/src/seadrop/src/test/TestERC721.sol new file mode 100644 index 0000000..30a8e47 --- /dev/null +++ b/src/seadrop/src/test/TestERC721.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.17; + +import {ERC721} from "solady/tokens/ERC721.sol"; + +// Used for minting test ERC721s in our tests +contract TestERC721 is ERC721("Test721", "TST721") { + function mint(address to, uint256 tokenId) public returns (bool) { + _mint(to, tokenId); + return true; + } + + function tokenURI(uint256) public pure override returns (string memory) { + return "tokenURI"; + } +} diff --git a/test/dynamic-traits/ERC721DynamicTraits.t.sol b/test/dynamic-traits/ERC721DynamicTraits.t.sol index 2fa576e..6e22618 100644 --- a/test/dynamic-traits/ERC721DynamicTraits.t.sol +++ b/test/dynamic-traits/ERC721DynamicTraits.t.sol @@ -4,11 +4,16 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; import {IERC7496} from "src/dynamic-traits/interfaces/IERC7496.sol"; -import {ERC721DynamicTraits, DynamicTraits} from "src/dynamic-traits/ERC721DynamicTraits.sol"; +import {AbstractDynamicTraits, DynamicTraits} from "src/dynamic-traits/AbstractDynamicTraits.sol"; +import {ERC721} from "solady/tokens/ERC721.sol"; import {Solarray} from "solarray/Solarray.sol"; +contract ERC721DynamicTraits is AbstractDynamicTraits, ERC721 { + function supportsInterface(bytes4) public view override(AbstractDynamicTraits, ERC721) returns (bool) {} +} + contract ERC721DynamicTraitsTest is Test { - ERC721DynamicTraits token; + AbstractDynamicTraits token; /* Events */ event TraitUpdated(bytes32 indexed traitKey, uint256 indexed tokenId, bytes32 value); @@ -17,7 +22,7 @@ contract ERC721DynamicTraitsTest is Test { event TraitLabelsURIUpdated(string uri); function setUp() public { - token = new ERC721DynamicTraits(); + token = new AbstractDynamicTraits(); } function testSupportsInterfaceId() public { From 072ffb77ae73624be3aacad7ee711fe532292fa5 Mon Sep 17 00:00:00 2001 From: James Wenzel Date: Tue, 19 Sep 2023 13:47:03 -0700 Subject: [PATCH 05/10] compiles --- .gitmodules | 3 - foundry.toml | 2 +- lib/utility-contracts | 1 - src/IOwnable.sol | 6 ++ src/TwoStepAdministered.sol | 88 +++++++++++++++++++ src/dynamic-traits/AbstractDynamicTraits.sol | 5 +- src/redeemables/ERC7498DynamicTraits.sol | 14 ++- .../{src => }/ERC721ContractMetadata.sol | 4 +- .../{src => }/ERC721PartnerSeaDrop.sol | 7 +- src/seadrop/{src => }/ERC721SeaDrop.sol | 5 +- src/seadrop/{src => }/SeaDrop.sol | 2 +- .../{src => }/clones/ERC721ACloneable.sol | 0 .../ERC721ContractMetadataCloneable.sol | 8 +- .../clones/ERC721SeaDropCloneFactory.sol | 0 .../clones/ERC721SeaDropCloneable.sol | 8 +- .../ERC721PartnerSeaDropBurnable.sol | 4 +- .../ERC721PartnerSeaDropRandomOffset.sol | 4 +- .../extensions/ERC721SeaDropBurnable.sol | 0 .../extensions/ERC721SeaDropRandomOffset.sol | 4 +- .../extensions/ERC721SeaDropRedeemable.sol | 4 +- .../ERC721SeaDropRedemptionMintable.sol | 4 +- .../interfaces/INonFungibleSeaDropToken.sol | 0 src/seadrop/{src => }/interfaces/ISeaDrop.sol | 0 .../ISeaDropTokenContractMetadata.sol | 0 .../ERC721SeaDropStructsErrorsAndEvents.sol | 0 .../{src => }/lib/SeaDropErrorsAndEvents.sol | 0 src/seadrop/{src => }/lib/SeaDropStructs.sol | 0 src/seadrop/{src => }/shim/Shim.sol | 0 .../{src => }/test/MaliciousRecipient.sol | 0 src/seadrop/{src => }/test/TestERC721.sol | 10 ++- test/dynamic-traits/ERC721DynamicTraits.t.sol | 24 +++-- 31 files changed, 168 insertions(+), 39 deletions(-) delete mode 160000 lib/utility-contracts create mode 100644 src/IOwnable.sol create mode 100644 src/TwoStepAdministered.sol rename src/seadrop/{src => }/ERC721ContractMetadata.sol (98%) rename src/seadrop/{src => }/ERC721PartnerSeaDrop.sol (97%) rename src/seadrop/{src => }/ERC721SeaDrop.sol (99%) rename src/seadrop/{src => }/SeaDrop.sol (99%) rename src/seadrop/{src => }/clones/ERC721ACloneable.sol (100%) rename src/seadrop/{src => }/clones/ERC721ContractMetadataCloneable.sol (96%) rename src/seadrop/{src => }/clones/ERC721SeaDropCloneFactory.sol (100%) rename src/seadrop/{src => }/clones/ERC721SeaDropCloneable.sol (98%) rename src/seadrop/{src => }/extensions/ERC721PartnerSeaDropBurnable.sol (88%) rename src/seadrop/{src => }/extensions/ERC721PartnerSeaDropRandomOffset.sol (95%) rename src/seadrop/{src => }/extensions/ERC721SeaDropBurnable.sol (100%) rename src/seadrop/{src => }/extensions/ERC721SeaDropRandomOffset.sol (95%) rename src/seadrop/{src => }/extensions/ERC721SeaDropRedeemable.sol (97%) rename src/seadrop/{src => }/extensions/ERC721SeaDropRedemptionMintable.sol (97%) rename src/seadrop/{src => }/interfaces/INonFungibleSeaDropToken.sol (100%) rename src/seadrop/{src => }/interfaces/ISeaDrop.sol (100%) rename src/seadrop/{src => }/interfaces/ISeaDropTokenContractMetadata.sol (100%) rename src/seadrop/{src => }/lib/ERC721SeaDropStructsErrorsAndEvents.sol (100%) rename src/seadrop/{src => }/lib/SeaDropErrorsAndEvents.sol (100%) rename src/seadrop/{src => }/lib/SeaDropStructs.sol (100%) rename src/seadrop/{src => }/shim/Shim.sol (100%) rename src/seadrop/{src => }/test/MaliciousRecipient.sol (100%) rename src/seadrop/{src => }/test/TestERC721.sol (63%) diff --git a/.gitmodules b/.gitmodules index 483a33b..4824ee2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,6 +25,3 @@ [submodule "lib/openzeppelin-contracts-upgradeable"] path = lib/openzeppelin-contracts-upgradeable url = https://github.com/openzeppelin/openzeppelin-contracts-upgradeable -[submodule "lib/utility-contracts"] - path = lib/utility-contracts - url = https://github.com/jameswenzel/utility-contracts diff --git a/foundry.toml b/foundry.toml index 5173517..cc01aa4 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,7 +5,7 @@ out = "out" libs = ["lib"] remappings = [ 'forge-std/=lib/forge-std/src', - 'ERC721A/=lib/ERC721A/contracts', + 'ERC721A/=lib/ERC721A/contracts/', 'solady/=lib/solady/src/', 'solady-test/=lib/solady/test/', 'openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/', diff --git a/lib/utility-contracts b/lib/utility-contracts deleted file mode 160000 index 6543a1d..0000000 --- a/lib/utility-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6543a1dd10160936849de2f2fbcc18503906fb1e diff --git a/src/IOwnable.sol b/src/IOwnable.sol new file mode 100644 index 0000000..4fa4f01 --- /dev/null +++ b/src/IOwnable.sol @@ -0,0 +1,6 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IOwnable { + function owner() external view returns (address); +} diff --git a/src/TwoStepAdministered.sol b/src/TwoStepAdministered.sol new file mode 100644 index 0000000..873021e --- /dev/null +++ b/src/TwoStepAdministered.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {IOwnable} from "./IOwnable.sol"; + +abstract contract TwoStepAdministered is IOwnable { + event AdministratorUpdated(address indexed previousAdministrator, address indexed newAdministrator); + event PotentialAdministratorUpdated(address newPotentialAdministrator); + + error OnlyAdministrator(); + error OnlyOwnerOrAdministrator(); + error NotNextAdministrator(); + error AlreadyInitialized(); + error NewAdministratorIsZeroAddress(); + + address public administrator; + address public potentialAdministrator; + + modifier onlyAdministrator() virtual { + if (msg.sender != administrator) { + revert OnlyAdministrator(); + } + + _; + } + + modifier onlyOwnerOrAdministrator() virtual { + if (msg.sender != owner()) { + if (msg.sender != administrator) { + revert OnlyOwnerOrAdministrator(); + } + } + _; + } + + modifier onlyConstructor() { + if (address(this).code.length > 0) { + revert AlreadyInitialized(); + } + _; + } + + constructor(address _administrator) { + _initialize(_administrator); + } + + function _initialize(address _administrator) private onlyConstructor { + administrator = _administrator; + emit AdministratorUpdated(address(0), _administrator); + } + + function transferAdministration(address newAdministrator) public virtual onlyAdministrator { + if (newAdministrator == address(0)) { + revert NewAdministratorIsZeroAddress(); + } + potentialAdministrator = newAdministrator; + emit PotentialAdministratorUpdated(newAdministrator); + } + + function _transferAdministration(address newAdministrator) internal virtual { + administrator = newAdministrator; + + emit AdministratorUpdated(msg.sender, newAdministrator); + } + + ///@notice Acept administration of smart contract, after the current administrator has initiated the process with transferAdministration + function acceptAdministration() public virtual { + address _potentialAdministrator = potentialAdministrator; + if (msg.sender != _potentialAdministrator) { + revert NotNextAdministrator(); + } + _transferAdministration(_potentialAdministrator); + delete potentialAdministrator; + } + + ///@notice cancel administration transfer + function cancelAdministrationTransfer() public virtual onlyAdministrator { + delete potentialAdministrator; + emit PotentialAdministratorUpdated(address(0)); + } + + function renounceAdministration() public virtual onlyAdministrator { + delete administrator; + emit AdministratorUpdated(msg.sender, address(0)); + } + + function owner() public view virtual returns (address); +} diff --git a/src/dynamic-traits/AbstractDynamicTraits.sol b/src/dynamic-traits/AbstractDynamicTraits.sol index e777880..71d90ec 100644 --- a/src/dynamic-traits/AbstractDynamicTraits.sol +++ b/src/dynamic-traits/AbstractDynamicTraits.sol @@ -1,12 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {Ownable} from "openzeppelin-contracts/access/Ownable.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; import {DynamicTraits} from "./DynamicTraits.sol"; abstract contract AbstractDynamicTraits is DynamicTraits, Ownable { - constructor() Ownable(msg.sender) { + constructor() { _traitLabelsURI = "https://example.com"; + _initializeOwner(msg.sender); } function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 value) external virtual override onlyOwner { diff --git a/src/redeemables/ERC7498DynamicTraits.sol b/src/redeemables/ERC7498DynamicTraits.sol index 85bf184..47f54c6 100644 --- a/src/redeemables/ERC7498DynamicTraits.sol +++ b/src/redeemables/ERC7498DynamicTraits.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.19; import {AbstractDynamicTraits} from "../dynamic-traits/AbstractDynamicTraits.sol"; import {ERC20} from "solady/tokens/ERC20.sol"; import {ERC721} from "solady/tokens/ERC721.sol"; -import {ERC721SeaDrop} from "../seadrop/src/ERC721SeaDrop.sol"; +import {ERC721SeaDrop} from "../seadrop/ERC721SeaDrop.sol"; import {IERC7498} from "./interfaces/IERC7498.sol"; import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/lib/ConsiderationStructs.sol"; import {ItemType} from "seaport-types/lib/ConsiderationEnums.sol"; @@ -25,7 +25,7 @@ contract ERC7498NFTRedeemables is AbstractDynamicTraits, ERC721SeaDrop, IERC7498 /// @dev The total current redemptions by campaign id. mapping(uint256 campaignId => uint256 count) private _totalRedemptions; - constructor() AbstractDynamicTraits() ERC721SeaDrop() {} + constructor(address[] memory allowedSeaDrop) ERC721SeaDrop("ERC7498 NFT Redeemables", "NFTR", allowedSeaDrop) {} function name() public pure override returns (string memory) { return "ERC7498 NFT Redeemables"; @@ -240,4 +240,14 @@ contract ERC7498NFTRedeemables is AbstractDynamicTraits, ERC721SeaDrop, IERC7498 inactive := or(iszero(gt(endTime, timestamp())), gt(startTime, timestamp())) } } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(AbstractDynamicTraits, ERC721SeaDrop) + returns (bool) + { + return interfaceId == type(IERC7498).interfaceId || super.supportsInterface(interfaceId); + } } diff --git a/src/seadrop/src/ERC721ContractMetadata.sol b/src/seadrop/ERC721ContractMetadata.sol similarity index 98% rename from src/seadrop/src/ERC721ContractMetadata.sol rename to src/seadrop/ERC721ContractMetadata.sol index e6e6857..7badba0 100644 --- a/src/seadrop/src/ERC721ContractMetadata.sol +++ b/src/seadrop/ERC721ContractMetadata.sol @@ -7,9 +7,7 @@ import {ERC721A} from "ERC721A/ERC721A.sol"; import {Ownable} from "solady/auth/Ownable.sol"; -import {IERC2981} from "openzeppelin-contracts/interfaces/IERC2981.sol"; - -import {IERC165} from "openzeppelin-contracts/utils/introspection/IERC165.sol"; +import {IERC2981, IERC165} from "openzeppelin-contracts/interfaces/IERC2981.sol"; /** * @title ERC721ContractMetadata diff --git a/src/seadrop/src/ERC721PartnerSeaDrop.sol b/src/seadrop/ERC721PartnerSeaDrop.sol similarity index 97% rename from src/seadrop/src/ERC721PartnerSeaDrop.sol rename to src/seadrop/ERC721PartnerSeaDrop.sol index 74e5f89..1f8aad3 100644 --- a/src/seadrop/src/ERC721PartnerSeaDrop.sol +++ b/src/seadrop/ERC721PartnerSeaDrop.sol @@ -7,7 +7,8 @@ import {ISeaDrop} from "./interfaces/ISeaDrop.sol"; import {AllowListData, PublicDrop, TokenGatedDropStage, SignedMintValidationParams} from "./lib/SeaDropStructs.sol"; -import {TwoStepAdministered} from "utility-contracts/TwoStepAdministered.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; +import {TwoStepAdministered, IOwnable} from "src/TwoStepAdministered.sol"; /** * @title ERC721PartnerSeaDrop @@ -34,6 +35,10 @@ contract ERC721PartnerSeaDrop is ERC721SeaDrop, TwoStepAdministered { /// first initialize with fee. error AdministratorMustInitializeWithFee(); + function owner() public view virtual override(TwoStepAdministered, Ownable) returns (address) { + return super.owner(); + } + /** * @notice Deploy the token contract with its name, symbol, * administrator, and allowed SeaDrop addresses. diff --git a/src/seadrop/src/ERC721SeaDrop.sol b/src/seadrop/ERC721SeaDrop.sol similarity index 99% rename from src/seadrop/src/ERC721SeaDrop.sol rename to src/seadrop/ERC721SeaDrop.sol index 9d62e32..a23696a 100644 --- a/src/seadrop/src/ERC721SeaDrop.sol +++ b/src/seadrop/ERC721SeaDrop.sol @@ -14,9 +14,7 @@ import {ERC721SeaDropStructsErrorsAndEvents} from "./lib/ERC721SeaDropStructsErr import {ERC721A} from "ERC721A/ERC721A.sol"; import {ReentrancyGuard} from "openzeppelin-contracts/security/ReentrancyGuard.sol"; - -import {IERC165} from "openzeppelin-contracts/utils/introspection/IERC165.sol"; - +import {IERC165} from "openzeppelin-contracts/interfaces/IERC165.sol"; /** * @title ERC721SeaDrop * @author James Wenzel (emo.eth) @@ -26,6 +24,7 @@ import {IERC165} from "openzeppelin-contracts/utils/introspection/IERC165.sol"; * @notice ERC721SeaDrop is a token contract that contains methods * to properly interact with SeaDrop. */ + contract ERC721SeaDrop is ERC721ContractMetadata, INonFungibleSeaDropToken, diff --git a/src/seadrop/src/SeaDrop.sol b/src/seadrop/SeaDrop.sol similarity index 99% rename from src/seadrop/src/SeaDrop.sol rename to src/seadrop/SeaDrop.sol index a3294a3..46c3239 100644 --- a/src/seadrop/src/SeaDrop.sol +++ b/src/seadrop/SeaDrop.sol @@ -20,7 +20,7 @@ import {ReentrancyGuard} from "openzeppelin-contracts/security/ReentrancyGuard.s import {IERC721} from "openzeppelin-contracts/token/ERC721/IERC721.sol"; -import {IERC165} from "openzeppelin-contracts/utils/introspection/IERC165.sol"; +import {IERC165} from "forge-std/interfaces/IERC165.sol"; import {ECDSA} from "openzeppelin-contracts/utils/cryptography/ECDSA.sol"; diff --git a/src/seadrop/src/clones/ERC721ACloneable.sol b/src/seadrop/clones/ERC721ACloneable.sol similarity index 100% rename from src/seadrop/src/clones/ERC721ACloneable.sol rename to src/seadrop/clones/ERC721ACloneable.sol diff --git a/src/seadrop/src/clones/ERC721ContractMetadataCloneable.sol b/src/seadrop/clones/ERC721ContractMetadataCloneable.sol similarity index 96% rename from src/seadrop/src/clones/ERC721ContractMetadataCloneable.sol rename to src/seadrop/clones/ERC721ContractMetadataCloneable.sol index bf2e18f..e3a87c0 100644 --- a/src/seadrop/src/clones/ERC721ContractMetadataCloneable.sol +++ b/src/seadrop/clones/ERC721ContractMetadataCloneable.sol @@ -5,11 +5,11 @@ import {ISeaDropTokenContractMetadata} from "../interfaces/ISeaDropTokenContract import {ERC721ACloneable} from "./ERC721ACloneable.sol"; -import {TwoStepOwnable} from "utility-contracts/TwoStepOwnable.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; import {IERC2981} from "openzeppelin-contracts/interfaces/IERC2981.sol"; -import {IERC165} from "openzeppelin-contracts/utils/introspection/IERC165.sol"; +import {IERC165} from "openzeppelin-contracts/interfaces/IERC165.sol"; /** * @title ERC721ContractMetadataCloneable @@ -19,7 +19,7 @@ import {IERC165} from "openzeppelin-contracts/utils/introspection/IERC165.sol"; * @notice ERC721ContractMetadata is a token contract that extends ERC721A * with additional metadata and ownership capabilities. */ -contract ERC721ContractMetadataCloneable is ERC721ACloneable, TwoStepOwnable, ISeaDropTokenContractMetadata { +contract ERC721ContractMetadataCloneable is ERC721ACloneable, Ownable, ISeaDropTokenContractMetadata { /// @notice Track the max supply. uint256 _maxSupply; @@ -44,7 +44,7 @@ contract ERC721ContractMetadataCloneable is ERC721ACloneable, TwoStepOwnable, IS */ function _onlyOwnerOrSelf() internal view { if (_cast(msg.sender == owner()) | _cast(msg.sender == address(this)) == 0) { - revert OnlyOwner(); + revert Unauthorized(); } } diff --git a/src/seadrop/src/clones/ERC721SeaDropCloneFactory.sol b/src/seadrop/clones/ERC721SeaDropCloneFactory.sol similarity index 100% rename from src/seadrop/src/clones/ERC721SeaDropCloneFactory.sol rename to src/seadrop/clones/ERC721SeaDropCloneFactory.sol diff --git a/src/seadrop/src/clones/ERC721SeaDropCloneable.sol b/src/seadrop/clones/ERC721SeaDropCloneable.sol similarity index 98% rename from src/seadrop/src/clones/ERC721SeaDropCloneable.sol rename to src/seadrop/clones/ERC721SeaDropCloneable.sol index 93677c9..145b58e 100644 --- a/src/seadrop/src/clones/ERC721SeaDropCloneable.sol +++ b/src/seadrop/clones/ERC721SeaDropCloneable.sol @@ -13,8 +13,9 @@ import {ERC721ACloneable} from "./ERC721ACloneable.sol"; import {ReentrancyGuardUpgradeable} from "openzeppelin-contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -import {IERC165} from "openzeppelin-contracts/utils/introspection/IERC165.sol"; - +import {IERC165} from "openzeppelin-contracts/interfaces/IERC165.sol"; +import {ERC721ContractMetadataCloneable} from "./ERC721ContractMetadataCloneable.sol"; +import {ISeaDropTokenContractMetadata} from "../interfaces/ISeaDropTokenContractMetadata.sol"; /** * @title ERC721SeaDrop * @author James Wenzel (emo.eth) @@ -23,6 +24,7 @@ import {IERC165} from "openzeppelin-contracts/utils/introspection/IERC165.sol"; * @notice ERC721SeaDrop is a token contract that contains methods * to properly interact with SeaDrop. */ + contract ERC721SeaDropCloneable is ERC721ContractMetadataCloneable, INonFungibleSeaDropToken, @@ -61,7 +63,7 @@ contract ERC721SeaDropCloneable is __ERC721ACloneable__init(__name, __symbol); __ReentrancyGuard_init(); _updateAllowedSeaDrop(allowedSeaDrop); - _transferOwnership(initialOwner); + _initializeOwner(initialOwner); emit SeaDropTokenDeployed(); } diff --git a/src/seadrop/src/extensions/ERC721PartnerSeaDropBurnable.sol b/src/seadrop/extensions/ERC721PartnerSeaDropBurnable.sol similarity index 88% rename from src/seadrop/src/extensions/ERC721PartnerSeaDropBurnable.sol rename to src/seadrop/extensions/ERC721PartnerSeaDropBurnable.sol index 0104107..7ade010 100644 --- a/src/seadrop/src/extensions/ERC721PartnerSeaDropBurnable.sol +++ b/src/seadrop/extensions/ERC721PartnerSeaDropBurnable.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; +import {ERC721PartnerSeaDrop} from "../ERC721PartnerSeaDrop.sol"; /** * @title ERC721PartnerSeaDropBurnable * @author James Wenzel (emo.eth) @@ -9,7 +10,8 @@ pragma solidity ^0.8.17; * @notice ERC721PartnerSeaDropBurnable is a token contract that extends * ERC721PartnerSeaDrop to additionally provide a burn function. */ -contract ERC721PartnerSeaDropBurnable { + +contract ERC721PartnerSeaDropBurnable is ERC721PartnerSeaDrop { /** * @notice Deploy the token contract with its name, symbol, * administrator, and allowed SeaDrop addresses. diff --git a/src/seadrop/src/extensions/ERC721PartnerSeaDropRandomOffset.sol b/src/seadrop/extensions/ERC721PartnerSeaDropRandomOffset.sol similarity index 95% rename from src/seadrop/src/extensions/ERC721PartnerSeaDropRandomOffset.sol rename to src/seadrop/extensions/ERC721PartnerSeaDropRandomOffset.sol index 817eaf4..16d2dd3 100644 --- a/src/seadrop/src/extensions/ERC721PartnerSeaDropRandomOffset.sol +++ b/src/seadrop/extensions/ERC721PartnerSeaDropRandomOffset.sol @@ -62,10 +62,10 @@ contract ERC721PartnerSeaDropRandomOffset is ERC721PartnerSeaDrop { revert NotFullyMinted(); } - // block.difficulty returns PREVRANDAO on Ethereum post-merge + // block.prevrandao returns PREVRANDAO on Ethereum post-merge // NOTE: do not use this on other chains // randomOffset returns between 1 and MAX_SUPPLY - randomOffset = (uint256(keccak256(abi.encode(block.difficulty))) % (maxSupply - 1)) + 1; + randomOffset = (uint256(keccak256(abi.encode(block.prevrandao))) % (maxSupply - 1)) + 1; // Set revealed to true. revealed = _REVEALED_TRUE; diff --git a/src/seadrop/src/extensions/ERC721SeaDropBurnable.sol b/src/seadrop/extensions/ERC721SeaDropBurnable.sol similarity index 100% rename from src/seadrop/src/extensions/ERC721SeaDropBurnable.sol rename to src/seadrop/extensions/ERC721SeaDropBurnable.sol diff --git a/src/seadrop/src/extensions/ERC721SeaDropRandomOffset.sol b/src/seadrop/extensions/ERC721SeaDropRandomOffset.sol similarity index 95% rename from src/seadrop/src/extensions/ERC721SeaDropRandomOffset.sol rename to src/seadrop/extensions/ERC721SeaDropRandomOffset.sol index 7d61c32..3eff13d 100644 --- a/src/seadrop/src/extensions/ERC721SeaDropRandomOffset.sol +++ b/src/seadrop/extensions/ERC721SeaDropRandomOffset.sol @@ -63,10 +63,10 @@ contract ERC721SeaDropRandomOffset is ERC721SeaDrop { revert NotFullyMinted(); } - // block.difficulty returns PREVRANDAO on Ethereum post-merge + // block.prevrandao returns PREVRANDAO on Ethereum post-merge // NOTE: do not use this on other chains // randomOffset returns between 1 and MAX_SUPPLY - randomOffset = (uint256(keccak256(abi.encode(block.difficulty))) % (maxSupply - 1)) + 1; + randomOffset = (uint256(keccak256(abi.encode(block.prevrandao))) % (maxSupply - 1)) + 1; // Set revealed to true. revealed = _REVEALED_TRUE; diff --git a/src/seadrop/src/extensions/ERC721SeaDropRedeemable.sol b/src/seadrop/extensions/ERC721SeaDropRedeemable.sol similarity index 97% rename from src/seadrop/src/extensions/ERC721SeaDropRedeemable.sol rename to src/seadrop/extensions/ERC721SeaDropRedeemable.sol index d45eaaa..782fa20 100644 --- a/src/seadrop/src/extensions/ERC721SeaDropRedeemable.sol +++ b/src/seadrop/extensions/ERC721SeaDropRedeemable.sol @@ -101,9 +101,9 @@ contract ERC721SeaDropRedeemable is ERC721SeaDrop, IERC721RedemptionMintable { // 30% chance of tokenURI 2 // 10% chance of tokenURI 3 - // block.difficulty returns PREVRANDAO on Ethereum post-merge + // block.prevrandao returns PREVRANDAO on Ethereum post-merge // NOTE: do not use this on other chains - uint256 randomness = (uint256(keccak256(abi.encode(block.difficulty))) % 100) + 1; + uint256 randomness = (uint256(keccak256(abi.encode(block.prevrandao))) % 100) + 1; uint256 tokenURINumber = 1; if (randomness >= 60 && randomness < 90) { diff --git a/src/seadrop/src/extensions/ERC721SeaDropRedemptionMintable.sol b/src/seadrop/extensions/ERC721SeaDropRedemptionMintable.sol similarity index 97% rename from src/seadrop/src/extensions/ERC721SeaDropRedemptionMintable.sol rename to src/seadrop/extensions/ERC721SeaDropRedemptionMintable.sol index ed2516f..2aacff0 100644 --- a/src/seadrop/src/extensions/ERC721SeaDropRedemptionMintable.sol +++ b/src/seadrop/extensions/ERC721SeaDropRedemptionMintable.sol @@ -89,9 +89,9 @@ contract ERC721SeaDropRedemptionMintable is ERC721SeaDrop, IERC721RedemptionMint // 30% chance of tokenURI 2 // 10% chance of tokenURI 3 - // block.difficulty returns PREVRANDAO on Ethereum post-merge + // block.prevrandao returns PREVRANDAO on Ethereum post-merge // NOTE: do not use this on other chains - uint256 randomness = (uint256(keccak256(abi.encode(block.difficulty))) % 100) + 1; + uint256 randomness = (uint256(keccak256(abi.encode(block.prevrandao))) % 100) + 1; uint256 tokenURINumber = 1; if (randomness >= 60 && randomness < 90) { diff --git a/src/seadrop/src/interfaces/INonFungibleSeaDropToken.sol b/src/seadrop/interfaces/INonFungibleSeaDropToken.sol similarity index 100% rename from src/seadrop/src/interfaces/INonFungibleSeaDropToken.sol rename to src/seadrop/interfaces/INonFungibleSeaDropToken.sol diff --git a/src/seadrop/src/interfaces/ISeaDrop.sol b/src/seadrop/interfaces/ISeaDrop.sol similarity index 100% rename from src/seadrop/src/interfaces/ISeaDrop.sol rename to src/seadrop/interfaces/ISeaDrop.sol diff --git a/src/seadrop/src/interfaces/ISeaDropTokenContractMetadata.sol b/src/seadrop/interfaces/ISeaDropTokenContractMetadata.sol similarity index 100% rename from src/seadrop/src/interfaces/ISeaDropTokenContractMetadata.sol rename to src/seadrop/interfaces/ISeaDropTokenContractMetadata.sol diff --git a/src/seadrop/src/lib/ERC721SeaDropStructsErrorsAndEvents.sol b/src/seadrop/lib/ERC721SeaDropStructsErrorsAndEvents.sol similarity index 100% rename from src/seadrop/src/lib/ERC721SeaDropStructsErrorsAndEvents.sol rename to src/seadrop/lib/ERC721SeaDropStructsErrorsAndEvents.sol diff --git a/src/seadrop/src/lib/SeaDropErrorsAndEvents.sol b/src/seadrop/lib/SeaDropErrorsAndEvents.sol similarity index 100% rename from src/seadrop/src/lib/SeaDropErrorsAndEvents.sol rename to src/seadrop/lib/SeaDropErrorsAndEvents.sol diff --git a/src/seadrop/src/lib/SeaDropStructs.sol b/src/seadrop/lib/SeaDropStructs.sol similarity index 100% rename from src/seadrop/src/lib/SeaDropStructs.sol rename to src/seadrop/lib/SeaDropStructs.sol diff --git a/src/seadrop/src/shim/Shim.sol b/src/seadrop/shim/Shim.sol similarity index 100% rename from src/seadrop/src/shim/Shim.sol rename to src/seadrop/shim/Shim.sol diff --git a/src/seadrop/src/test/MaliciousRecipient.sol b/src/seadrop/test/MaliciousRecipient.sol similarity index 100% rename from src/seadrop/src/test/MaliciousRecipient.sol rename to src/seadrop/test/MaliciousRecipient.sol diff --git a/src/seadrop/src/test/TestERC721.sol b/src/seadrop/test/TestERC721.sol similarity index 63% rename from src/seadrop/src/test/TestERC721.sol rename to src/seadrop/test/TestERC721.sol index 30a8e47..b627634 100644 --- a/src/seadrop/src/test/TestERC721.sol +++ b/src/seadrop/test/TestERC721.sol @@ -4,7 +4,15 @@ pragma solidity ^0.8.17; import {ERC721} from "solady/tokens/ERC721.sol"; // Used for minting test ERC721s in our tests -contract TestERC721 is ERC721("Test721", "TST721") { +contract TestERC721 is ERC721 { + function name() public pure override returns (string memory) { + return "TestERC721"; + } + + function symbol() public pure override returns (string memory) { + return "TEST"; + } + function mint(address to, uint256 tokenId) public returns (bool) { _mint(to, tokenId); return true; diff --git a/test/dynamic-traits/ERC721DynamicTraits.t.sol b/test/dynamic-traits/ERC721DynamicTraits.t.sol index 6e22618..d102a89 100644 --- a/test/dynamic-traits/ERC721DynamicTraits.t.sol +++ b/test/dynamic-traits/ERC721DynamicTraits.t.sol @@ -2,14 +2,28 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; -import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; import {IERC7496} from "src/dynamic-traits/interfaces/IERC7496.sol"; import {AbstractDynamicTraits, DynamicTraits} from "src/dynamic-traits/AbstractDynamicTraits.sol"; import {ERC721} from "solady/tokens/ERC721.sol"; import {Solarray} from "solarray/Solarray.sol"; contract ERC721DynamicTraits is AbstractDynamicTraits, ERC721 { - function supportsInterface(bytes4) public view override(AbstractDynamicTraits, ERC721) returns (bool) {} + function name() public pure override returns (string memory) { + return "ERC721DynamicTraits"; + } + + function symbol() public pure override returns (string memory) { + return "ERC721DT"; + } + + function tokenURI(uint256) public pure override returns (string memory) { + return "tokenURI"; + } + + function supportsInterface(bytes4 interfaceId) public view override(AbstractDynamicTraits, ERC721) returns (bool) { + return AbstractDynamicTraits.supportsInterface(interfaceId) || ERC721.supportsInterface(interfaceId); + } } contract ERC721DynamicTraitsTest is Test { @@ -22,7 +36,7 @@ contract ERC721DynamicTraitsTest is Test { event TraitLabelsURIUpdated(string uri); function setUp() public { - token = new AbstractDynamicTraits(); + token = new ERC721DynamicTraits(); } function testSupportsInterfaceId() public { @@ -45,7 +59,7 @@ contract ERC721DynamicTraitsTest is Test { function testOnlyOwnerCanSetValues() public { address alice = makeAddr("alice"); vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice)); + vm.expectRevert(abi.encodeWithSelector(Ownable.Unauthorized.selector)); token.setTrait(bytes32("test"), 0, bytes32("test")); } @@ -129,7 +143,7 @@ contract ERC721DynamicTraitsTest is Test { assertEq(token.getTraitLabelsURI(), uri); vm.prank(address(0x1234)); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(0x1234))); + vm.expectRevert(abi.encodeWithSelector(Ownable.Unauthorized.selector)); token.setTraitLabelsURI(uri); } From d560764f09459546658be98c91d86635542e8fab Mon Sep 17 00:00:00 2001 From: Stephan Min Date: Wed, 20 Sep 2023 11:03:41 -0400 Subject: [PATCH 06/10] add setMinter to ERC721RedemptionMintable, campaign methods to ERC7498NFTRedeemables --- src/redeemables/ERC721RedemptionMintable.sol | 51 ++++ src/redeemables/ERC7498DynamicTraits.sol | 253 ---------------- src/redeemables/ERC7498NFTRedeemables.sol | 291 +++++++++++++++++++ 3 files changed, 342 insertions(+), 253 deletions(-) create mode 100644 src/redeemables/ERC721RedemptionMintable.sol delete mode 100644 src/redeemables/ERC7498DynamicTraits.sol create mode 100644 src/redeemables/ERC7498NFTRedeemables.sol diff --git a/src/redeemables/ERC721RedemptionMintable.sol b/src/redeemables/ERC721RedemptionMintable.sol new file mode 100644 index 0000000..edae169 --- /dev/null +++ b/src/redeemables/ERC721RedemptionMintable.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ERC721} from "solady/tokens/ERC721.sol"; +import {IERC721RedemptionMintable} from "./interfaces/IERC721RedemptionMintable.sol"; +import {SpentItem} from "seaport-types/lib/ConsiderationStructs.sol"; + +contract ERC721RedemptionMintable is ERC721, IERC721RedemptionMintable { + address internal _MINTER; + + /// @dev Revert if the sender of mintRedemption is not the redeemable contract offerer. + error InvalidSender(); + + /// @dev Revert if the redemption spent is not the required token. + error InvalidRedemption(); + + constructor(address minter) { + _MINTER = minter; + } + + function mintRedemption(address to, uint256 tokenId) external returns (uint256) { + if (msg.sender != _MINTER) revert InvalidSender(); + + // Mint the same token ID redeemed. + _mint(to, tokenId); + + return tokenId; + } + + function name() public pure override returns (string memory) { + return "ERC721RedemptionMintable"; + } + + function symbol() public pure override returns (string memory) { + return "721RM"; + } + + function tokenURI(uint256 tokenId) public pure override returns (string memory) { + return string(abi.encodePacked("https://example.com/", tokenId)); + } + + function setMinter(address newMinter) external { + if (msg.sender != _MINTER) revert InvalidSender(); + + _setMinter(newMinter); + } + + function _setMinter(address newMinter) internal { + _MINTER = newMinter; + } +} diff --git a/src/redeemables/ERC7498DynamicTraits.sol b/src/redeemables/ERC7498DynamicTraits.sol deleted file mode 100644 index 47f54c6..0000000 --- a/src/redeemables/ERC7498DynamicTraits.sol +++ /dev/null @@ -1,253 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import {AbstractDynamicTraits} from "../dynamic-traits/AbstractDynamicTraits.sol"; -import {ERC20} from "solady/tokens/ERC20.sol"; -import {ERC721} from "solady/tokens/ERC721.sol"; -import {ERC721SeaDrop} from "../seadrop/ERC721SeaDrop.sol"; -import {IERC7498} from "./interfaces/IERC7498.sol"; -import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/lib/ConsiderationStructs.sol"; -import {ItemType} from "seaport-types/lib/ConsiderationEnums.sol"; -import {IERC721RedemptionMintable} from "./interfaces/IERC721RedemptionMintable.sol"; -import {CampaignParams, TraitRedemption} from "./lib/RedeemablesStructs.sol"; -import {RedeemablesErrorsAndEvents} from "./lib/RedeemablesErrorsAndEvents.sol"; - -contract ERC7498NFTRedeemables is AbstractDynamicTraits, ERC721SeaDrop, IERC7498, RedeemablesErrorsAndEvents { - /// @dev Counter for next campaign id. - uint256 private _nextCampaignId = 1; - - /// @dev The campaign parameters by campaign id. - mapping(uint256 campaignId => CampaignParams params) private _campaignParams; - - /// @dev The campaign URIs by campaign id. - mapping(uint256 campaignId => string campaignURI) private _campaignURIs; - - /// @dev The total current redemptions by campaign id. - mapping(uint256 campaignId => uint256 count) private _totalRedemptions; - - constructor(address[] memory allowedSeaDrop) ERC721SeaDrop("ERC7498 NFT Redeemables", "NFTR", allowedSeaDrop) {} - - function name() public pure override returns (string memory) { - return "ERC7498 NFT Redeemables"; - } - - function symbol() public pure override returns (string memory) { - return "NFTR"; - } - - function tokenURI(uint256 tokenId) public view override returns (string memory) { - return "https://example.com/"; - } - - function mint(address to, uint256 tokenId) public { - _mint(to, tokenId); - } - - // at 64 will be pointer to array - // mload pointer, pointer points to length - // next word is start of array - function redeem(uint256[] calldata tokenIds, address recipient, bytes calldata extraData) public virtual override { - // Get the campaign. - uint256 campaignId = uint256(bytes32(extraData[0:32])); - CampaignParams storage params = _campaignParams[campaignId]; - ConsiderationItem[] memory consideration = params.consideration; - - // Revert if campaign is inactive. - if (_isInactive(params.startTime, params.endTime)) { - revert NotActive(block.timestamp, params.startTime, params.endTime); - } - - // Revert if max total redemptions would be exceeded. - if (_totalRedemptions[campaignId] + tokenIds.length > params.maxCampaignRedemptions) { - revert MaxCampaignRedemptionsReached(_totalRedemptions[campaignId] + 1, params.maxCampaignRedemptions); - } - - address considerationRecipient; - - for (uint256 i; i < consideration.length;) { - if (consideration[i].token == address(this)) { - considerationRecipient = consideration[i].recipient; - } - - unchecked { - ++i; - } - } - - TraitRedemption[] calldata traitRedemptions; - - // calldata array is two vars on stack (length, ptr to start of array) - assembly { - // Get the pointer to the length of the trait redemptions array by adding 0x40 to the extraData offset. - let traitRedemptionsLengthPtr := calldataload(add(0x40, extraData.offset)) - - // Set the length of the trait redeptions array to the value at the array length pointer. - traitRedemptions.length := calldataload(traitRedemptionsLengthPtr) - - // Set the pointer to the start of the trait redemptions array to the word after the length. - traitRedemptions.offset := add(0x20, traitRedemptionsLengthPtr) - } - - // Iterate over the trait redemptions and set traits on the tokens. - for (uint256 i; i < traitRedemptions.length;) { - // Get the trait redemption token address and place on the stack. - address token = traitRedemptions[i].token; - - uint256 identifier = traitRedemptions[i].identifier; - - // Revert if the trait redemption token is not this token contract. - if (token != address(this)) { - revert InvalidToken(token); - } - - // Revert if the trait redemption identifier is not owned by the caller. - if (ERC721(token).ownerOf(identifier) != msg.sender) { - revert InvalidCaller(token); - } - - // Declare a new block to manage stack depth. - { - // Get the substandard and place on the stack. - uint8 substandard = traitRedemptions[i].substandard; - - // Get the substandard value and place on the stack. - bytes32 substandardValue = traitRedemptions[i].substandardValue; - - // Get the trait key and place on the stack. - bytes32 traitKey = traitRedemptions[i].traitKey; - - bytes32 traitValue = traitRedemptions[i].traitValue; - - // Get the current trait value and place on the stack. - bytes32 currentTraitValue = getTraitValue(traitKey, identifier); - - // If substandard is 1, set trait to traitValue. - if (substandard == 1) { - // Revert if the current trait value does not match the substandard value. - if (currentTraitValue != substandardValue) { - revert InvalidRequiredValue(currentTraitValue, substandardValue); - } - - // Set the trait to the trait value. - _setTrait(traitRedemptions[i].traitKey, identifier, traitValue); - // If substandard is 2, increment trait by traitValue. - } else if (substandard == 2) { - // Revert if the current trait value is greater than the substandard value. - if (currentTraitValue > substandardValue) { - revert InvalidRequiredValue(currentTraitValue, substandardValue); - } - - // Increment the trait by the trait value. - uint256 newTraitValue = uint256(currentTraitValue) + uint256(traitValue); - - _setTrait(traitRedemptions[i].traitKey, identifier, bytes32(newTraitValue)); - } else if (substandard == 3) { - // Revert if the current trait value is less than the substandard value. - if (currentTraitValue < substandardValue) { - revert InvalidRequiredValue(currentTraitValue, substandardValue); - } - - uint256 newTraitValue = uint256(currentTraitValue) - uint256(traitValue); - - // Decrement the trait by the trait value. - _setTrait(traitRedemptions[i].traitKey, traitRedemptions[i].identifier, bytes32(newTraitValue)); - } - } - unchecked { - ++i; - } - } - - // Iterate over the token IDs and check if caller is the owner or approved operator. - // Redeem the token if the caller is valid. - for (uint256 i; i < tokenIds.length;) { - // Get the identifier. - uint256 identifier = tokenIds[i]; - - // Get the token owner. - address owner = ownerOf(identifier); - - // Check the caller is either the owner or approved operator. - if (msg.sender != owner && !isApprovedForAll(owner, msg.sender)) { - revert InvalidCaller(msg.sender); - } - - // Transfer the token to the consideration recipient. - ERC721(consideration[i].token).safeTransferFrom(owner, considerationRecipient, identifier); - - // Mint the redemption token. - IERC721RedemptionMintable(params.offer[0].token).mintRedemption(recipient, identifier); - - unchecked { - ++i; - } - } - } - - function getCampaign(uint256 campaignId) - external - view - override - returns (CampaignParams memory params, string memory uri, uint256 totalRedemptions) - {} - - function createCampaign(CampaignParams calldata params, string calldata uri) - external - override - returns (uint256 campaignId) - { - // Revert if there are no consideration items, since the redemption should require at least something. - if (params.consideration.length == 0) revert NoConsiderationItems(); - - // Revert if startTime is past endTime. - if (params.startTime > params.endTime) revert InvalidTime(); - - // Revert if any of the consideration item recipients is the zero address. The 0xdead address should be used instead. - for (uint256 i = 0; i < params.consideration.length;) { - if (params.consideration[i].recipient == address(0)) { - revert ConsiderationItemRecipientCannotBeZeroAddress(); - } - unchecked { - ++i; - } - } - - // Set the campaign params for the next campaignId. - _campaignParams[_nextCampaignId] = params; - - // Set the campaign URI for the next campaignId. - _campaignURIs[_nextCampaignId] = uri; - - // Set the correct current campaignId to return before incrementing - // the next campaignId. - campaignId = _nextCampaignId; - - // Increment the next campaignId. - _nextCampaignId++; - - emit CampaignUpdated(campaignId, params, _campaignURIs[campaignId]); - } - - function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata uri) - external - override - {} - - function _isInactive(uint256 startTime, uint256 endTime) internal view returns (bool inactive) { - // Using the same check for time boundary from Seaport. - // startTime <= block.timestamp < endTime - assembly { - inactive := or(iszero(gt(endTime, timestamp())), gt(startTime, timestamp())) - } - } - - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(AbstractDynamicTraits, ERC721SeaDrop) - returns (bool) - { - return interfaceId == type(IERC7498).interfaceId || super.supportsInterface(interfaceId); - } -} diff --git a/src/redeemables/ERC7498NFTRedeemables.sol b/src/redeemables/ERC7498NFTRedeemables.sol new file mode 100644 index 0000000..76f073d --- /dev/null +++ b/src/redeemables/ERC7498NFTRedeemables.sol @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {AbstractDynamicTraits} from "../dynamic-traits/AbstractDynamicTraits.sol"; +import {ERC20} from "solady/tokens/ERC20.sol"; +import {ERC721} from "solady/tokens/ERC721.sol"; +import {ERC721SeaDrop} from "../seadrop/ERC721SeaDrop.sol"; +import {IERC7498} from "./interfaces/IERC7498.sol"; +import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/lib/ConsiderationStructs.sol"; +import {ItemType} from "seaport-types/lib/ConsiderationEnums.sol"; +import {IERC721RedemptionMintable} from "./interfaces/IERC721RedemptionMintable.sol"; +import {CampaignParams, TraitRedemption} from "./lib/RedeemablesStructs.sol"; +import {RedeemablesErrorsAndEvents} from "./lib/RedeemablesErrorsAndEvents.sol"; + +contract ERC7498NFTRedeemables is AbstractDynamicTraits, ERC721SeaDrop, IERC7498, RedeemablesErrorsAndEvents { + /// @dev Counter for next campaign id. + uint256 private _nextCampaignId = 1; + + /// @dev The campaign parameters by campaign id. + mapping(uint256 campaignId => CampaignParams params) private _campaignParams; + + /// @dev The campaign URIs by campaign id. + mapping(uint256 campaignId => string campaignURI) private _campaignURIs; + + /// @dev The total current redemptions by campaign id. + mapping(uint256 campaignId => uint256 count) private _totalRedemptions; + + constructor(address[] memory allowedSeaDrop) ERC721SeaDrop("ERC7498 NFT Redeemables", "NFTR", allowedSeaDrop) {} + + function name() public pure override returns (string memory) { + return "ERC7498 NFT Redeemables"; + } + + function symbol() public pure override returns (string memory) { + return "NFTR"; + } + + function tokenURI(uint256 tokenId) public view override returns (string memory) { + return "https://example.com/"; + } + + function mint(address to, uint256 tokenId) public { + _mint(to, tokenId); + } + + // at 64 will be pointer to array + // mload pointer, pointer points to length + // next word is start of array + function redeem(uint256[] calldata tokenIds, address recipient, bytes calldata extraData) public virtual override { + // Get the campaign id from extraData. + uint256 campaignId = uint256(bytes32(extraData[0:32])); + + // Get the campaign params. + CampaignParams storage params = _campaignParams[campaignId]; + + // Revert if campaign is inactive. + if (_isInactive(params.startTime, params.endTime)) { + revert NotActive(block.timestamp, params.startTime, params.endTime); + } + + // Revert if max total redemptions would be exceeded. + if (_totalRedemptions[campaignId] + tokenIds.length > params.maxCampaignRedemptions) { + revert MaxCampaignRedemptionsReached(_totalRedemptions[campaignId] + 1, params.maxCampaignRedemptions); + } + + // Get the campaign consideration. + ConsiderationItem[] memory consideration = params.consideration; + + // TraitRedemption[] calldata traitRedemptions; + + // // calldata array is two vars on stack (length, ptr to start of array) + // assembly { + // // Get the pointer to the length of the trait redemptions array by adding 0x40 to the extraData offset. + // let traitRedemptionsLengthPtr := calldataload(add(0x40, extraData.offset)) + + // // Set the length of the trait redeptions array to the value at the array length pointer. + // traitRedemptions.length := calldataload(traitRedemptionsLengthPtr) + + // // Set the pointer to the start of the trait redemptions array to the word after the length. + // traitRedemptions.offset := add(0x20, traitRedemptionsLengthPtr) + // } + + // // Iterate over the trait redemptions and set traits on the tokens. + // for (uint256 i; i < traitRedemptions.length;) { + // // Get the trait redemption token address and place on the stack. + // address token = traitRedemptions[i].token; + + // uint256 identifier = traitRedemptions[i].identifier; + + // // Revert if the trait redemption token is not this token contract. + // if (token != address(this)) { + // revert InvalidToken(token); + // } + + // // Revert if the trait redemption identifier is not owned by the caller. + // if (ERC721(token).ownerOf(identifier) != msg.sender) { + // revert InvalidCaller(token); + // } + + // // Declare a new block to manage stack depth. + // { + // // Get the substandard and place on the stack. + // uint8 substandard = traitRedemptions[i].substandard; + + // // Get the substandard value and place on the stack. + // bytes32 substandardValue = traitRedemptions[i].substandardValue; + + // // Get the trait key and place on the stack. + // bytes32 traitKey = traitRedemptions[i].traitKey; + + // bytes32 traitValue = traitRedemptions[i].traitValue; + + // // Get the current trait value and place on the stack. + // bytes32 currentTraitValue = getTraitValue(traitKey, identifier); + + // // If substandard is 1, set trait to traitValue. + // if (substandard == 1) { + // // Revert if the current trait value does not match the substandard value. + // if (currentTraitValue != substandardValue) { + // revert InvalidRequiredValue(currentTraitValue, substandardValue); + // } + + // // Set the trait to the trait value. + // _setTrait(traitRedemptions[i].traitKey, identifier, traitValue); + // // If substandard is 2, increment trait by traitValue. + // } else if (substandard == 2) { + // // Revert if the current trait value is greater than the substandard value. + // if (currentTraitValue > substandardValue) { + // revert InvalidRequiredValue(currentTraitValue, substandardValue); + // } + + // // Increment the trait by the trait value. + // uint256 newTraitValue = uint256(currentTraitValue) + uint256(traitValue); + + // _setTrait(traitRedemptions[i].traitKey, identifier, bytes32(newTraitValue)); + // } else if (substandard == 3) { + // // Revert if the current trait value is less than the substandard value. + // if (currentTraitValue < substandardValue) { + // revert InvalidRequiredValue(currentTraitValue, substandardValue); + // } + + // uint256 newTraitValue = uint256(currentTraitValue) - uint256(traitValue); + + // // Decrement the trait by the trait value. + // _setTrait(traitRedemptions[i].traitKey, traitRedemptions[i].identifier, bytes32(newTraitValue)); + // } + // } + // unchecked { + // ++i; + // } + // } + + // Iterate over the token IDs and check if caller is the owner or approved operator. + // Redeem the token if the caller is valid. + for (uint256 i; i < tokenIds.length;) { + // Get the identifier. + uint256 identifier = tokenIds[i]; + + // Get the token owner. + address owner = ownerOf(identifier); + + // Check the caller is either the owner or approved operator. + if (msg.sender != owner && !isApprovedForAll(owner, msg.sender)) { + revert InvalidCaller(msg.sender); + } + + // Burn or transfer the token to the consideration recipient. + if (consideration[0].recipient == payable(address(0x000000000000000000000000000000000000dEaD))) { + _burn(identifier); + } else { + ERC721(consideration[0].token).safeTransferFrom(owner, consideration[0].recipient, identifier); + } + + // Mint the redemption token. + IERC721RedemptionMintable(params.offer[0].token).mintRedemption(recipient, identifier); + + unchecked { + ++i; + } + } + } + + function getCampaign(uint256 campaignId) + external + view + override + returns (CampaignParams memory params, string memory uri, uint256 totalRedemptions) + { + if (campaignId >= _nextCampaignId) revert InvalidCampaignId(); + params = _campaignParams[campaignId]; + uri = _campaignURIs[campaignId]; + totalRedemptions = _totalRedemptions[campaignId]; + } + + function createCampaign(CampaignParams calldata params, string calldata uri) + external + override + returns (uint256 campaignId) + { + // Revert if there are no consideration items, since the redemption should require at least something. + if (params.consideration.length == 0) revert NoConsiderationItems(); + + // Revert if startTime is past endTime. + if (params.startTime > params.endTime) revert InvalidTime(); + + // Revert if any of the consideration item recipients is the zero address. The 0xdead address should be used instead. + for (uint256 i = 0; i < params.consideration.length;) { + if (params.consideration[i].recipient == address(0)) { + revert ConsiderationItemRecipientCannotBeZeroAddress(); + } + unchecked { + ++i; + } + } + + // Set the campaign params for the next campaignId. + _campaignParams[_nextCampaignId] = params; + + // Set the campaign URI for the next campaignId. + _campaignURIs[_nextCampaignId] = uri; + + // Set the correct current campaignId to return before incrementing + // the next campaignId. + campaignId = _nextCampaignId; + + // Increment the next campaignId. + _nextCampaignId++; + + emit CampaignUpdated(campaignId, params, _campaignURIs[campaignId]); + } + + function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata uri) + external + override + { + // Revert if campaign id is invalid. + if (campaignId == 0 || campaignId >= _nextCampaignId) { + revert InvalidCampaignId(); + } + + // Revert if there are no consideration items, since the redemption should require at least something. + if (params.consideration.length == 0) revert NoConsiderationItems(); + + // Revert if startTime is past endTime. + if (params.startTime > params.endTime) revert InvalidTime(); + + // Revert if msg.sender is not the manager. + address existingManager = _campaignParams[campaignId].manager; + if (params.manager != msg.sender && (existingManager != address(0) && existingManager != params.manager)) { + revert NotManager(); + } + + // Revert if any of the consideration item recipients is the zero address. The 0xdead address should be used instead. + for (uint256 i = 0; i < params.consideration.length;) { + if (params.consideration[i].recipient == address(0)) { + revert ConsiderationItemRecipientCannotBeZeroAddress(); + } + unchecked { + ++i; + } + } + + // Set the campaign params for the given campaignId. + _campaignParams[campaignId] = params; + + // Update campaign uri if it was provided. + if (bytes(uri).length != 0) { + _campaignURIs[campaignId] = uri; + } + + emit CampaignUpdated(campaignId, params, _campaignURIs[campaignId]); + } + + function _isInactive(uint256 startTime, uint256 endTime) internal view returns (bool inactive) { + // Using the same check for time boundary from Seaport. + // startTime <= block.timestamp < endTime + assembly { + inactive := or(iszero(gt(endTime, timestamp())), gt(startTime, timestamp())) + } + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(AbstractDynamicTraits, ERC721SeaDrop) + returns (bool) + { + return interfaceId == type(IERC7498).interfaceId || super.supportsInterface(interfaceId); + } +} From 7806e23da16c4bfe97f74166fd413a33faf22aba Mon Sep 17 00:00:00 2001 From: Stephan Min Date: Wed, 20 Sep 2023 11:27:35 -0400 Subject: [PATCH 07/10] comments --- src/redeemables/ERC7498NFTRedeemables.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/redeemables/ERC7498NFTRedeemables.sol b/src/redeemables/ERC7498NFTRedeemables.sol index 76f073d..3c33318 100644 --- a/src/redeemables/ERC7498NFTRedeemables.sol +++ b/src/redeemables/ERC7498NFTRedeemables.sol @@ -186,9 +186,16 @@ contract ERC7498NFTRedeemables is AbstractDynamicTraits, ERC721SeaDrop, IERC7498 override returns (CampaignParams memory params, string memory uri, uint256 totalRedemptions) { + // Revert if campaign id is invalid. if (campaignId >= _nextCampaignId) revert InvalidCampaignId(); + + // Get the campaign params. params = _campaignParams[campaignId]; + + // Get the campaign URI. uri = _campaignURIs[campaignId]; + + // Get the total redemptions. totalRedemptions = _totalRedemptions[campaignId]; } From e56ef8a15783c15f63b0d8ce7a35b91d57d9c6de Mon Sep 17 00:00:00 2001 From: Stephan Min Date: Wed, 20 Sep 2023 12:27:37 -0400 Subject: [PATCH 08/10] don't inherit dynamic traits for now --- src/redeemables/ERC7498NFTRedeemables.sol | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/redeemables/ERC7498NFTRedeemables.sol b/src/redeemables/ERC7498NFTRedeemables.sol index 3c33318..59c2a59 100644 --- a/src/redeemables/ERC7498NFTRedeemables.sol +++ b/src/redeemables/ERC7498NFTRedeemables.sol @@ -12,7 +12,7 @@ import {IERC721RedemptionMintable} from "./interfaces/IERC721RedemptionMintable. import {CampaignParams, TraitRedemption} from "./lib/RedeemablesStructs.sol"; import {RedeemablesErrorsAndEvents} from "./lib/RedeemablesErrorsAndEvents.sol"; -contract ERC7498NFTRedeemables is AbstractDynamicTraits, ERC721SeaDrop, IERC7498, RedeemablesErrorsAndEvents { +contract ERC7498NFTRedeemables is ERC721SeaDrop, IERC7498, RedeemablesErrorsAndEvents { /// @dev Counter for next campaign id. uint256 private _nextCampaignId = 1; @@ -286,13 +286,7 @@ contract ERC7498NFTRedeemables is AbstractDynamicTraits, ERC721SeaDrop, IERC7498 } } - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(AbstractDynamicTraits, ERC721SeaDrop) - returns (bool) - { + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721SeaDrop) returns (bool) { return interfaceId == type(IERC7498).interfaceId || super.supportsInterface(interfaceId); } } From 214526d4f28fa18e67ad664f616d429dfe50d2db Mon Sep 17 00:00:00 2001 From: Stephan Min Date: Wed, 20 Sep 2023 12:33:01 -0400 Subject: [PATCH 09/10] add check to prevent redeeming external tokens --- src/redeemables/ERC7498NFTRedeemables.sol | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/redeemables/ERC7498NFTRedeemables.sol b/src/redeemables/ERC7498NFTRedeemables.sol index 59c2a59..b7655b5 100644 --- a/src/redeemables/ERC7498NFTRedeemables.sol +++ b/src/redeemables/ERC7498NFTRedeemables.sol @@ -60,7 +60,9 @@ contract ERC7498NFTRedeemables is ERC721SeaDrop, IERC7498, RedeemablesErrorsAndE // Revert if max total redemptions would be exceeded. if (_totalRedemptions[campaignId] + tokenIds.length > params.maxCampaignRedemptions) { - revert MaxCampaignRedemptionsReached(_totalRedemptions[campaignId] + 1, params.maxCampaignRedemptions); + revert MaxCampaignRedemptionsReached( + _totalRedemptions[campaignId] + tokenIds.length, params.maxCampaignRedemptions + ); } // Get the campaign consideration. @@ -210,8 +212,14 @@ contract ERC7498NFTRedeemables is ERC721SeaDrop, IERC7498, RedeemablesErrorsAndE // Revert if startTime is past endTime. if (params.startTime > params.endTime) revert InvalidTime(); - // Revert if any of the consideration item recipients is the zero address. The 0xdead address should be used instead. for (uint256 i = 0; i < params.consideration.length;) { + // Revert if any of the consideration items is not this token contract. + if (params.consideration[i].token != address(this)) { + revert InvalidConsiderationItem(params.consideration[i].token, address(this)); + } + + // Revert if any of the consideration item recipients is the zero address. + // The 0xdead address should be used instead. if (params.consideration[i].recipient == address(0)) { revert ConsiderationItemRecipientCannotBeZeroAddress(); } From e171858cf5167e5310b021001872071bbd0e385d Mon Sep 17 00:00:00 2001 From: Stephan Min Date: Wed, 20 Sep 2023 16:11:32 -0400 Subject: [PATCH 10/10] get ERC7498NFTRedeemables to build --- .gitmodules | 7 + foundry.toml | 7 +- src/dynamic-traits/AbstractDynamicTraits.sol | 7 +- src/redeemables/ERC7498NFTRedeemables.sol | 199 +-- .../interfaces/IERC721RedemptionMintable.sol | 18 - .../interfaces/IRedemptionMintable.sol | 8 + .../lib/RedeemablesErrorsAndEvents.sol | 4 +- src/seadrop/ERC721ContractMetadata.sol | 282 ---- src/seadrop/ERC721PartnerSeaDrop.sol | 314 ----- src/seadrop/ERC721SeaDrop.sol | 596 -------- src/seadrop/SeaDrop.sol | 1154 --------------- src/seadrop/clones/ERC721ACloneable.sol | 1237 ----------------- .../ERC721ContractMetadataCloneable.sol | 285 ---- .../clones/ERC721SeaDropCloneFactory.sol | 29 - src/seadrop/clones/ERC721SeaDropCloneable.sol | 588 -------- .../ERC721PartnerSeaDropBurnable.sol | 33 - .../ERC721PartnerSeaDropRandomOffset.sol | 99 -- .../extensions/ERC721SeaDropBurnable.sol | 34 - .../extensions/ERC721SeaDropRandomOffset.sol | 100 -- .../extensions/ERC721SeaDropRedeemable.sol | 131 -- .../ERC721SeaDropRedemptionMintable.sol | 119 -- .../interfaces/INonFungibleSeaDropToken.sol | 160 --- src/seadrop/interfaces/ISeaDrop.sol | 312 ----- .../ISeaDropTokenContractMetadata.sol | 141 -- .../ERC721SeaDropStructsErrorsAndEvents.sol | 56 - src/seadrop/lib/SeaDropErrorsAndEvents.sol | 288 ---- src/seadrop/lib/SeaDropStructs.sol | 144 -- src/seadrop/shim/Shim.sol | 10 - src/seadrop/test/MaliciousRecipient.sol | 42 - src/seadrop/test/TestERC721.sol | 24 - 30 files changed, 128 insertions(+), 6300 deletions(-) delete mode 100644 src/redeemables/interfaces/IERC721RedemptionMintable.sol create mode 100644 src/redeemables/interfaces/IRedemptionMintable.sol delete mode 100644 src/seadrop/ERC721ContractMetadata.sol delete mode 100644 src/seadrop/ERC721PartnerSeaDrop.sol delete mode 100644 src/seadrop/ERC721SeaDrop.sol delete mode 100644 src/seadrop/SeaDrop.sol delete mode 100644 src/seadrop/clones/ERC721ACloneable.sol delete mode 100644 src/seadrop/clones/ERC721ContractMetadataCloneable.sol delete mode 100644 src/seadrop/clones/ERC721SeaDropCloneFactory.sol delete mode 100644 src/seadrop/clones/ERC721SeaDropCloneable.sol delete mode 100644 src/seadrop/extensions/ERC721PartnerSeaDropBurnable.sol delete mode 100644 src/seadrop/extensions/ERC721PartnerSeaDropRandomOffset.sol delete mode 100644 src/seadrop/extensions/ERC721SeaDropBurnable.sol delete mode 100644 src/seadrop/extensions/ERC721SeaDropRandomOffset.sol delete mode 100644 src/seadrop/extensions/ERC721SeaDropRedeemable.sol delete mode 100644 src/seadrop/extensions/ERC721SeaDropRedemptionMintable.sol delete mode 100644 src/seadrop/interfaces/INonFungibleSeaDropToken.sol delete mode 100644 src/seadrop/interfaces/ISeaDrop.sol delete mode 100644 src/seadrop/interfaces/ISeaDropTokenContractMetadata.sol delete mode 100644 src/seadrop/lib/ERC721SeaDropStructsErrorsAndEvents.sol delete mode 100644 src/seadrop/lib/SeaDropErrorsAndEvents.sol delete mode 100644 src/seadrop/lib/SeaDropStructs.sol delete mode 100644 src/seadrop/shim/Shim.sol delete mode 100644 src/seadrop/test/MaliciousRecipient.sol delete mode 100644 src/seadrop/test/TestERC721.sol diff --git a/.gitmodules b/.gitmodules index 4824ee2..2a44d42 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,10 @@ [submodule "lib/openzeppelin-contracts-upgradeable"] path = lib/openzeppelin-contracts-upgradeable url = https://github.com/openzeppelin/openzeppelin-contracts-upgradeable +[submodule "lib/seadrop"] + path = lib/seadrop + url = https://github.com/ProjectOpenSea/seadrop + branch = v2 +[submodule "lib/ds-test"] + path = lib/ds-test + url = https://github.com/dapphub/ds-test diff --git a/foundry.toml b/foundry.toml index cc01aa4..caf43fa 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,21 +5,26 @@ out = "out" libs = ["lib"] remappings = [ 'forge-std/=lib/forge-std/src', + 'ds-test/=lib/ds-test/src', 'ERC721A/=lib/ERC721A/contracts/', 'solady/=lib/solady/src/', 'solady-test/=lib/solady/test/', 'openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/', + 'operator-filter-registry/=lib/seadrop/lib/operator-filter-registry/src/', 'shipyard-core/=src/', 'seaport-types/=lib/seaport-types/src/', 'solarray/=lib/solarray/src/', 'dynamic-traits/=lib/dynamic-traits/src/', 'openzeppelin-contracts/contracts/=lib/openzeppelin-contracts/contracts/', + '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', 'openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/', 'utility-contracts/=lib/utility-contracts/src/', + 'seadrop/=lib/seadrop/src/', ] +auto_detect_remappings = false # bytecode_hash = 'none' ignored_error_codes = ['license', 'code-size', 'init-code-size', 2519] -optimizer_runs = 99_999_999 +optimizer_runs = 2_000_000 [profile.lite.fuzz] runs = 1 diff --git a/src/dynamic-traits/AbstractDynamicTraits.sol b/src/dynamic-traits/AbstractDynamicTraits.sol index 71d90ec..a62ea6c 100644 --- a/src/dynamic-traits/AbstractDynamicTraits.sol +++ b/src/dynamic-traits/AbstractDynamicTraits.sol @@ -1,13 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {Ownable} from "solady/auth/Ownable.sol"; +import {TwoStepOwnable} from "utility-contracts/TwoStepOwnable.sol"; import {DynamicTraits} from "./DynamicTraits.sol"; -abstract contract AbstractDynamicTraits is DynamicTraits, Ownable { - constructor() { +abstract contract AbstractDynamicTraits is DynamicTraits, TwoStepOwnable { + constructor() TwoStepOwnable() { _traitLabelsURI = "https://example.com"; - _initializeOwner(msg.sender); } function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 value) external virtual override onlyOwner { diff --git a/src/redeemables/ERC7498NFTRedeemables.sol b/src/redeemables/ERC7498NFTRedeemables.sol index b7655b5..53e1473 100644 --- a/src/redeemables/ERC7498NFTRedeemables.sol +++ b/src/redeemables/ERC7498NFTRedeemables.sol @@ -4,15 +4,16 @@ pragma solidity ^0.8.19; import {AbstractDynamicTraits} from "../dynamic-traits/AbstractDynamicTraits.sol"; import {ERC20} from "solady/tokens/ERC20.sol"; import {ERC721} from "solady/tokens/ERC721.sol"; -import {ERC721SeaDrop} from "../seadrop/ERC721SeaDrop.sol"; +import {ERC721SeaDrop} from "seadrop/ERC721SeaDrop.sol"; +import {ERC721SeaDropContractOfferer} from "seadrop/lib/ERC721SeaDropContractOfferer.sol"; import {IERC7498} from "./interfaces/IERC7498.sol"; import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/lib/ConsiderationStructs.sol"; import {ItemType} from "seaport-types/lib/ConsiderationEnums.sol"; -import {IERC721RedemptionMintable} from "./interfaces/IERC721RedemptionMintable.sol"; +import {IRedemptionMintable} from "./interfaces/IRedemptionMintable.sol"; import {CampaignParams, TraitRedemption} from "./lib/RedeemablesStructs.sol"; import {RedeemablesErrorsAndEvents} from "./lib/RedeemablesErrorsAndEvents.sol"; -contract ERC7498NFTRedeemables is ERC721SeaDrop, IERC7498, RedeemablesErrorsAndEvents { +contract ERC7498NFTRedeemables is AbstractDynamicTraits, ERC721SeaDrop, IERC7498, RedeemablesErrorsAndEvents { /// @dev Counter for next campaign id. uint256 private _nextCampaignId = 1; @@ -25,15 +26,13 @@ contract ERC7498NFTRedeemables is ERC721SeaDrop, IERC7498, RedeemablesErrorsAndE /// @dev The total current redemptions by campaign id. mapping(uint256 campaignId => uint256 count) private _totalRedemptions; - constructor(address[] memory allowedSeaDrop) ERC721SeaDrop("ERC7498 NFT Redeemables", "NFTR", allowedSeaDrop) {} - - function name() public pure override returns (string memory) { - return "ERC7498 NFT Redeemables"; - } - - function symbol() public pure override returns (string memory) { - return "NFTR"; - } + constructor( + address allowedConfigurer, + address allowedConduit, + address allowedSeaport, + string memory _name, + string memory _symbol + ) ERC721SeaDrop(allowedConfigurer, allowedConduit, allowedSeaport, _name, _symbol) {} function tokenURI(uint256 tokenId) public view override returns (string memory) { return "https://example.com/"; @@ -68,89 +67,89 @@ contract ERC7498NFTRedeemables is ERC721SeaDrop, IERC7498, RedeemablesErrorsAndE // Get the campaign consideration. ConsiderationItem[] memory consideration = params.consideration; - // TraitRedemption[] calldata traitRedemptions; - - // // calldata array is two vars on stack (length, ptr to start of array) - // assembly { - // // Get the pointer to the length of the trait redemptions array by adding 0x40 to the extraData offset. - // let traitRedemptionsLengthPtr := calldataload(add(0x40, extraData.offset)) - - // // Set the length of the trait redeptions array to the value at the array length pointer. - // traitRedemptions.length := calldataload(traitRedemptionsLengthPtr) - - // // Set the pointer to the start of the trait redemptions array to the word after the length. - // traitRedemptions.offset := add(0x20, traitRedemptionsLengthPtr) - // } - - // // Iterate over the trait redemptions and set traits on the tokens. - // for (uint256 i; i < traitRedemptions.length;) { - // // Get the trait redemption token address and place on the stack. - // address token = traitRedemptions[i].token; - - // uint256 identifier = traitRedemptions[i].identifier; - - // // Revert if the trait redemption token is not this token contract. - // if (token != address(this)) { - // revert InvalidToken(token); - // } - - // // Revert if the trait redemption identifier is not owned by the caller. - // if (ERC721(token).ownerOf(identifier) != msg.sender) { - // revert InvalidCaller(token); - // } - - // // Declare a new block to manage stack depth. - // { - // // Get the substandard and place on the stack. - // uint8 substandard = traitRedemptions[i].substandard; - - // // Get the substandard value and place on the stack. - // bytes32 substandardValue = traitRedemptions[i].substandardValue; - - // // Get the trait key and place on the stack. - // bytes32 traitKey = traitRedemptions[i].traitKey; - - // bytes32 traitValue = traitRedemptions[i].traitValue; - - // // Get the current trait value and place on the stack. - // bytes32 currentTraitValue = getTraitValue(traitKey, identifier); - - // // If substandard is 1, set trait to traitValue. - // if (substandard == 1) { - // // Revert if the current trait value does not match the substandard value. - // if (currentTraitValue != substandardValue) { - // revert InvalidRequiredValue(currentTraitValue, substandardValue); - // } - - // // Set the trait to the trait value. - // _setTrait(traitRedemptions[i].traitKey, identifier, traitValue); - // // If substandard is 2, increment trait by traitValue. - // } else if (substandard == 2) { - // // Revert if the current trait value is greater than the substandard value. - // if (currentTraitValue > substandardValue) { - // revert InvalidRequiredValue(currentTraitValue, substandardValue); - // } - - // // Increment the trait by the trait value. - // uint256 newTraitValue = uint256(currentTraitValue) + uint256(traitValue); - - // _setTrait(traitRedemptions[i].traitKey, identifier, bytes32(newTraitValue)); - // } else if (substandard == 3) { - // // Revert if the current trait value is less than the substandard value. - // if (currentTraitValue < substandardValue) { - // revert InvalidRequiredValue(currentTraitValue, substandardValue); - // } - - // uint256 newTraitValue = uint256(currentTraitValue) - uint256(traitValue); - - // // Decrement the trait by the trait value. - // _setTrait(traitRedemptions[i].traitKey, traitRedemptions[i].identifier, bytes32(newTraitValue)); - // } - // } - // unchecked { - // ++i; - // } - // } + TraitRedemption[] calldata traitRedemptions; + + // calldata array is two vars on stack (length, ptr to start of array) + assembly { + // Get the pointer to the length of the trait redemptions array by adding 0x40 to the extraData offset. + let traitRedemptionsLengthPtr := calldataload(add(0x40, extraData.offset)) + + // Set the length of the trait redeptions array to the value at the array length pointer. + traitRedemptions.length := calldataload(traitRedemptionsLengthPtr) + + // Set the pointer to the start of the trait redemptions array to the word after the length. + traitRedemptions.offset := add(0x20, traitRedemptionsLengthPtr) + } + + // Iterate over the trait redemptions and set traits on the tokens. + for (uint256 i; i < traitRedemptions.length;) { + // Get the trait redemption token address and place on the stack. + address token = traitRedemptions[i].token; + + uint256 identifier = traitRedemptions[i].identifier; + + // Revert if the trait redemption token is not this token contract. + if (token != address(this)) { + revert InvalidToken(token); + } + + // Revert if the trait redemption identifier is not owned by the caller. + if (ERC721(token).ownerOf(identifier) != msg.sender) { + revert InvalidCaller(token); + } + + // Declare a new block to manage stack depth. + { + // Get the substandard and place on the stack. + uint8 substandard = traitRedemptions[i].substandard; + + // Get the substandard value and place on the stack. + bytes32 substandardValue = traitRedemptions[i].substandardValue; + + // Get the trait key and place on the stack. + bytes32 traitKey = traitRedemptions[i].traitKey; + + bytes32 traitValue = traitRedemptions[i].traitValue; + + // Get the current trait value and place on the stack. + bytes32 currentTraitValue = getTraitValue(traitKey, identifier); + + // If substandard is 1, set trait to traitValue. + if (substandard == 1) { + // Revert if the current trait value does not match the substandard value. + if (currentTraitValue != substandardValue) { + revert InvalidRequiredValue(currentTraitValue, substandardValue); + } + + // Set the trait to the trait value. + _setTrait(traitRedemptions[i].traitKey, identifier, traitValue); + // If substandard is 2, increment trait by traitValue. + } else if (substandard == 2) { + // Revert if the current trait value is greater than the substandard value. + if (currentTraitValue > substandardValue) { + revert InvalidRequiredValue(currentTraitValue, substandardValue); + } + + // Increment the trait by the trait value. + uint256 newTraitValue = uint256(currentTraitValue) + uint256(traitValue); + + _setTrait(traitRedemptions[i].traitKey, identifier, bytes32(newTraitValue)); + } else if (substandard == 3) { + // Revert if the current trait value is less than the substandard value. + if (currentTraitValue < substandardValue) { + revert InvalidRequiredValue(currentTraitValue, substandardValue); + } + + uint256 newTraitValue = uint256(currentTraitValue) - uint256(traitValue); + + // Decrement the trait by the trait value. + _setTrait(traitRedemptions[i].traitKey, traitRedemptions[i].identifier, bytes32(newTraitValue)); + } + } + unchecked { + ++i; + } + } // Iterate over the token IDs and check if caller is the owner or approved operator. // Redeem the token if the caller is valid. @@ -174,7 +173,7 @@ contract ERC7498NFTRedeemables is ERC721SeaDrop, IERC7498, RedeemablesErrorsAndE } // Mint the redemption token. - IERC721RedemptionMintable(params.offer[0].token).mintRedemption(recipient, identifier); + IRedemptionMintable(params.offer[0].token).mintRedemption(campaignId, recipient, consideration); unchecked { ++i; @@ -294,7 +293,13 @@ contract ERC7498NFTRedeemables is ERC721SeaDrop, IERC7498, RedeemablesErrorsAndE } } - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721SeaDrop) returns (bool) { + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(AbstractDynamicTraits, ERC721SeaDropContractOfferer) + returns (bool) + { return interfaceId == type(IERC7498).interfaceId || super.supportsInterface(interfaceId); } } diff --git a/src/redeemables/interfaces/IERC721RedemptionMintable.sol b/src/redeemables/interfaces/IERC721RedemptionMintable.sol deleted file mode 100644 index 3e855c9..0000000 --- a/src/redeemables/interfaces/IERC721RedemptionMintable.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import {SpentItem} from "seaport-types/lib/ConsiderationStructs.sol"; - -interface IERC721RedemptionMintable { - function mintRedemption(address to, uint256 tokenId) external returns (uint256); -} - -// import {ConsiderationItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; - -// interface IERC721RedemptionMintable { -// function mintRedemption( -// uint256 campaignId, -// address recipient, -// ConsiderationItem[] memory consideration -// ) external returns (uint256[] memory tokenIds); -// } diff --git a/src/redeemables/interfaces/IRedemptionMintable.sol b/src/redeemables/interfaces/IRedemptionMintable.sol new file mode 100644 index 0000000..1aa984a --- /dev/null +++ b/src/redeemables/interfaces/IRedemptionMintable.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ConsiderationItem} from "seaport-types/lib/ConsiderationStructs.sol"; + +interface IRedemptionMintable { + function mintRedemption(uint256 campaignId, address recipient, ConsiderationItem[] memory consideration) external; +} diff --git a/src/redeemables/lib/RedeemablesErrorsAndEvents.sol b/src/redeemables/lib/RedeemablesErrorsAndEvents.sol index d413a19..6f79c4e 100644 --- a/src/redeemables/lib/RedeemablesErrorsAndEvents.sol +++ b/src/redeemables/lib/RedeemablesErrorsAndEvents.sol @@ -15,7 +15,7 @@ interface RedeemablesErrorsAndEvents { error InvalidCampaignId(); error CampaignAlreadyExists(); error InvalidCaller(address caller); - error NotActive(uint256 currentTimestamp, uint256 startTime, uint256 endTime); + // error NotActive(uint256 currentTimestamp, uint256 startTime, uint256 endTime); error MaxRedemptionsReached(uint256 total, uint256 max); error MaxCampaignRedemptionsReached(uint256 total, uint256 max); error NativeTransferFailed(); @@ -27,7 +27,7 @@ interface RedeemablesErrorsAndEvents { error InvalidNativeOfferItem(); error InvalidOwner(); error InvalidRequiredValue(bytes32 got, bytes32 want); - error InvalidSubstandard(uint256 substandard); + // error InvalidSubstandard(uint256 substandard); error InvalidToken(address token); error InvalidTraitRedemption(); error InvalidTraitRedemptionToken(address token); diff --git a/src/seadrop/ERC721ContractMetadata.sol b/src/seadrop/ERC721ContractMetadata.sol deleted file mode 100644 index 7badba0..0000000 --- a/src/seadrop/ERC721ContractMetadata.sol +++ /dev/null @@ -1,282 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {ISeaDropTokenContractMetadata} from "./interfaces/ISeaDropTokenContractMetadata.sol"; - -import {ERC721A} from "ERC721A/ERC721A.sol"; - -import {Ownable} from "solady/auth/Ownable.sol"; - -import {IERC2981, IERC165} from "openzeppelin-contracts/interfaces/IERC2981.sol"; - -/** - * @title ERC721ContractMetadata - * @author James Wenzel (emo.eth) - * @author Ryan Ghods (ralxz.eth) - * @author Stephan Min (stephanm.eth) - * @notice ERC721ContractMetadata is a token contract that extends ERC721A - * with additional metadata and ownership capabilities. - */ -contract ERC721ContractMetadata is ERC721A, Ownable, ISeaDropTokenContractMetadata { - /// @notice Track the max supply. - uint256 _maxSupply; - - /// @notice Track the base URI for token metadata. - string _tokenBaseURI; - - /// @notice Track the contract URI for contract metadata. - string _contractURI; - - /// @notice Track the provenance hash for guaranteeing metadata order - /// for random reveals. - bytes32 _provenanceHash; - - /// @notice Track the royalty info: address to receive royalties, and - /// royalty basis points. - RoyaltyInfo _royaltyInfo; - - /** - * @dev Reverts if the sender is not the owner or the contract itself. - * This function is inlined instead of being a modifier - * to save contract space from being inlined N times. - */ - function _onlyOwnerOrSelf() internal view { - if (_cast(msg.sender == owner()) | _cast(msg.sender == address(this)) == 0) { - revert Unauthorized(); - } - } - - /** - * @notice Deploy the token contract with its name and symbol. - */ - constructor(string memory name, string memory symbol) ERC721A(name, symbol) {} - - /** - * @notice Sets the base URI for the token metadata and emits an event. - * - * @param newBaseURI The new base URI to set. - */ - function setBaseURI(string calldata newBaseURI) external override { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Set the new base URI. - _tokenBaseURI = newBaseURI; - - // Emit an event with the update. - if (totalSupply() != 0) { - emit BatchMetadataUpdate(1, _nextTokenId() - 1); - } - } - - /** - * @notice Sets the contract URI for contract metadata. - * - * @param newContractURI The new contract URI. - */ - function setContractURI(string calldata newContractURI) external override { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Set the new contract URI. - _contractURI = newContractURI; - - // Emit an event with the update. - emit ContractURIUpdated(newContractURI); - } - - /** - * @notice Emit an event notifying metadata updates for - * a range of token ids, according to EIP-4906. - * - * @param fromTokenId The start token id. - * @param toTokenId The end token id. - */ - function emitBatchMetadataUpdate(uint256 fromTokenId, uint256 toTokenId) external { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Emit an event with the update. - emit BatchMetadataUpdate(fromTokenId, toTokenId); - } - - /** - * @notice Sets the max token supply and emits an event. - * - * @param newMaxSupply The new max supply to set. - */ - function setMaxSupply(uint256 newMaxSupply) external { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the max supply does not exceed the maximum value of uint64. - if (newMaxSupply > 2 ** 64 - 1) { - revert CannotExceedMaxSupplyOfUint64(newMaxSupply); - } - - // Set the new max supply. - _maxSupply = newMaxSupply; - - // Emit an event with the update. - emit MaxSupplyUpdated(newMaxSupply); - } - - /** - * @notice Sets the provenance hash and emits an event. - * - * The provenance hash is used for random reveals, which - * is a hash of the ordered metadata to show it has not been - * modified after mint started. - * - * This function will revert after the first item has been minted. - * - * @param newProvenanceHash The new provenance hash to set. - */ - function setProvenanceHash(bytes32 newProvenanceHash) external { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Revert if any items have been minted. - if (_totalMinted() > 0) { - revert ProvenanceHashCannotBeSetAfterMintStarted(); - } - - // Keep track of the old provenance hash for emitting with the event. - bytes32 oldProvenanceHash = _provenanceHash; - - // Set the new provenance hash. - _provenanceHash = newProvenanceHash; - - // Emit an event with the update. - emit ProvenanceHashUpdated(oldProvenanceHash, newProvenanceHash); - } - - /** - * @notice Sets the address and basis points for royalties. - * - * @param newInfo The struct to configure royalties. - */ - function setRoyaltyInfo(RoyaltyInfo calldata newInfo) external { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Revert if the new royalty address is the zero address. - if (newInfo.royaltyAddress == address(0)) { - revert RoyaltyAddressCannotBeZeroAddress(); - } - - // Revert if the new basis points is greater than 10_000. - if (newInfo.royaltyBps > 10_000) { - revert InvalidRoyaltyBasisPoints(newInfo.royaltyBps); - } - - // Set the new royalty info. - _royaltyInfo = newInfo; - - // Emit an event with the updated params. - emit RoyaltyInfoUpdated(newInfo.royaltyAddress, newInfo.royaltyBps); - } - - /** - * @notice Returns the base URI for token metadata. - */ - function baseURI() external view override returns (string memory) { - return _baseURI(); - } - - /** - * @notice Returns the base URI for the contract, which ERC721A uses - * to return tokenURI. - */ - function _baseURI() internal view virtual override returns (string memory) { - return _tokenBaseURI; - } - - /** - * @notice Returns the contract URI for contract metadata. - */ - function contractURI() external view override returns (string memory) { - return _contractURI; - } - - /** - * @notice Returns the max token supply. - */ - function maxSupply() public view returns (uint256) { - return _maxSupply; - } - - /** - * @notice Returns the provenance hash. - * The provenance hash is used for random reveals, which - * is a hash of the ordered metadata to show it is unmodified - * after mint has started. - */ - function provenanceHash() external view override returns (bytes32) { - return _provenanceHash; - } - - /** - * @notice Returns the address that receives royalties. - */ - function royaltyAddress() external view returns (address) { - return _royaltyInfo.royaltyAddress; - } - - /** - * @notice Returns the royalty basis points out of 10_000. - */ - function royaltyBasisPoints() external view returns (uint256) { - return _royaltyInfo.royaltyBps; - } - - /** - * @notice Called with the sale price to determine how much royalty - * is owed and to whom. - * - * @ param _tokenId The NFT asset queried for royalty information. - * @param _salePrice The sale price of the NFT asset specified by - * _tokenId. - * - * @return receiver Address of who should be sent the royalty payment. - * @return royaltyAmount The royalty payment amount for _salePrice. - */ - function royaltyInfo(uint256, /* _tokenId */ uint256 _salePrice) - external - view - returns (address receiver, uint256 royaltyAmount) - { - // Put the royalty info on the stack for more efficient access. - RoyaltyInfo storage info = _royaltyInfo; - - // Set the royalty amount to the sale price times the royalty basis - // points divided by 10_000. - royaltyAmount = (_salePrice * info.royaltyBps) / 10_000; - - // Set the receiver of the royalty. - receiver = info.royaltyAddress; - } - - /** - * @notice Returns whether the interface is supported. - * - * @param interfaceId The interface id to check against. - */ - function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721A) returns (bool) { - return interfaceId == type(IERC2981).interfaceId || interfaceId == 0x49064906 // ERC-4906 - || super.supportsInterface(interfaceId); - } - - /** - * @dev Internal pure function to cast a `bool` value to a `uint256` value. - * - * @param b The `bool` value to cast. - * - * @return u The `uint256` value. - */ - function _cast(bool b) internal pure returns (uint256 u) { - assembly { - u := b - } - } -} diff --git a/src/seadrop/ERC721PartnerSeaDrop.sol b/src/seadrop/ERC721PartnerSeaDrop.sol deleted file mode 100644 index 1f8aad3..0000000 --- a/src/seadrop/ERC721PartnerSeaDrop.sol +++ /dev/null @@ -1,314 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {ERC721SeaDrop} from "./ERC721SeaDrop.sol"; - -import {ISeaDrop} from "./interfaces/ISeaDrop.sol"; - -import {AllowListData, PublicDrop, TokenGatedDropStage, SignedMintValidationParams} from "./lib/SeaDropStructs.sol"; - -import {Ownable} from "solady/auth/Ownable.sol"; -import {TwoStepAdministered, IOwnable} from "src/TwoStepAdministered.sol"; - -/** - * @title ERC721PartnerSeaDrop - * @author James Wenzel (emo.eth) - * @author Ryan Ghods (ralxz.eth) - * @author Stephan Min (stephanm.eth) - * @notice ERC721PartnerSeaDrop is a token contract that contains methods - * to properly interact with SeaDrop, with additional administrative - * functionality tailored for business requirements around partnered - * mints with off-chain agreements in place between two parties. - * - * The "Owner" should control mint specifics such as price and start. - * The "Administrator" should control fee parameters. - * - * Otherwise, for ease of administration, either Owner or Administrator - * should be able to configure mint parameters. They have the ability - * to override each other's actions in many circumstances, which is - * why the establishment of off-chain trust is important. - * - * Note: An Administrator is not required to interface with SeaDrop. - */ -contract ERC721PartnerSeaDrop is ERC721SeaDrop, TwoStepAdministered { - /// @notice To prevent Owner from overriding fees, Administrator must - /// first initialize with fee. - error AdministratorMustInitializeWithFee(); - - function owner() public view virtual override(TwoStepAdministered, Ownable) returns (address) { - return super.owner(); - } - - /** - * @notice Deploy the token contract with its name, symbol, - * administrator, and allowed SeaDrop addresses. - */ - constructor(string memory name, string memory symbol, address administrator, address[] memory allowedSeaDrop) - ERC721SeaDrop(name, symbol, allowedSeaDrop) - TwoStepAdministered(administrator) - {} - - /** - * @notice Mint tokens, restricted to the SeaDrop contract. - * - * @param minter The address to mint to. - * @param quantity The number of tokens to mint. - */ - function mintSeaDrop(address minter, uint256 quantity) external virtual override { - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(msg.sender); - - // Extra safety check to ensure the max supply is not exceeded. - if (_totalMinted() + quantity > maxSupply()) { - revert MintQuantityExceedsMaxSupply(_totalMinted() + quantity, maxSupply()); - } - - // Mint the quantity of tokens to the minter. - _mint(minter, quantity); - } - - /** - * @notice Update the allowed SeaDrop contracts. - * Only the owner or administrator can use this function. - * - * @param allowedSeaDrop The allowed SeaDrop addresses. - */ - function updateAllowedSeaDrop(address[] calldata allowedSeaDrop) external override onlyOwnerOrAdministrator { - _updateAllowedSeaDrop(allowedSeaDrop); - } - - /** - * @notice Update the public drop data for this nft contract on SeaDrop. - * Only the owner or administrator can use this function. - * - * The administrator can only update `feeBps`. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param publicDrop The public drop data. - */ - function updatePublicDrop(address seaDropImpl, PublicDrop calldata publicDrop) - external - virtual - override - onlyOwnerOrAdministrator - { - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Track the previous public drop data. - PublicDrop memory retrieved = ISeaDrop(seaDropImpl).getPublicDrop(address(this)); - - // Track the newly supplied drop data. - PublicDrop memory supplied = publicDrop; - - // Only the administrator (OpenSea) can set feeBps. - if (msg.sender != administrator) { - // Administrator must first set fee. - if (retrieved.maxTotalMintableByWallet == 0) { - revert AdministratorMustInitializeWithFee(); - } - supplied.feeBps = retrieved.feeBps; - supplied.restrictFeeRecipients = true; - } else { - // Administrator can only initialize - // (maxTotalMintableByWallet > 0) and set - // feeBps/restrictFeeRecipients. - uint16 maxTotalMintableByWallet = retrieved.maxTotalMintableByWallet; - retrieved.maxTotalMintableByWallet = maxTotalMintableByWallet > 0 ? maxTotalMintableByWallet : 1; - retrieved.feeBps = supplied.feeBps; - retrieved.restrictFeeRecipients = true; - supplied = retrieved; - } - - // Update the public drop data on SeaDrop. - ISeaDrop(seaDropImpl).updatePublicDrop(supplied); - } - - /** - * @notice Update the allow list data for this nft contract on SeaDrop. - * Only the owner or administrator can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param allowListData The allow list data. - */ - function updateAllowList(address seaDropImpl, AllowListData calldata allowListData) - external - virtual - override - onlyOwnerOrAdministrator - { - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the allow list on SeaDrop. - ISeaDrop(seaDropImpl).updateAllowList(allowListData); - } - - /** - * @notice Update the token gated drop stage data for this nft contract - * on SeaDrop. - * Only the owner or administrator can use this function. - * - * The administrator must first set `feeBps`. - * - * Note: If two INonFungibleSeaDropToken tokens are doing - * simultaneous token gated drop promotions for each other, - * they can be minted by the same actor until - * `maxTokenSupplyForStage` is reached. Please ensure the - * `allowedNftToken` is not running an active drop during the - * `dropStage` time period. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param allowedNftToken The allowed nft token. - * @param dropStage The token gated drop stage data. - */ - function updateTokenGatedDrop(address seaDropImpl, address allowedNftToken, TokenGatedDropStage calldata dropStage) - external - virtual - override - onlyOwnerOrAdministrator - { - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Track the previous drop stage data. - TokenGatedDropStage memory retrieved = ISeaDrop(seaDropImpl).getTokenGatedDrop(address(this), allowedNftToken); - - // Track the newly supplied drop data. - TokenGatedDropStage memory supplied = dropStage; - - // Only the administrator (OpenSea) can set feeBps on Partner - // contracts. - if (msg.sender != administrator) { - // Administrator must first set fee. - if (retrieved.maxTotalMintableByWallet == 0) { - revert AdministratorMustInitializeWithFee(); - } - supplied.feeBps = retrieved.feeBps; - supplied.restrictFeeRecipients = true; - } else { - // Administrator can only initialize - // (maxTotalMintableByWallet > 0) and set - // feeBps/restrictFeeRecipients. - uint16 maxTotalMintableByWallet = retrieved.maxTotalMintableByWallet; - retrieved.maxTotalMintableByWallet = maxTotalMintableByWallet > 0 ? maxTotalMintableByWallet : 1; - retrieved.feeBps = supplied.feeBps; - retrieved.restrictFeeRecipients = true; - supplied = retrieved; - } - - // Update the token gated drop stage. - ISeaDrop(seaDropImpl).updateTokenGatedDrop(allowedNftToken, supplied); - } - - /** - * @notice Update the drop URI for this nft contract on SeaDrop. - * Only the owner or administrator can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param dropURI The new drop URI. - */ - function updateDropURI(address seaDropImpl, string calldata dropURI) - external - virtual - override - onlyOwnerOrAdministrator - { - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the drop URI. - ISeaDrop(seaDropImpl).updateDropURI(dropURI); - } - - /** - * @notice Update the allowed fee recipient for this nft contract - * on SeaDrop. - * Only the administrator can set the allowed fee recipient. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param feeRecipient The new fee recipient. - * @param allowed If the fee recipient is allowed. - */ - function updateAllowedFeeRecipient(address seaDropImpl, address feeRecipient, bool allowed) - external - override - onlyAdministrator - { - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the allowed fee recipient. - ISeaDrop(seaDropImpl).updateAllowedFeeRecipient(feeRecipient, allowed); - } - - /** - * @notice Update the server-side signers for this nft contract - * on SeaDrop. - * Only the owner or administrator can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param signer The signer to update. - * @param signedMintValidationParams Minimum and maximum parameters to - * enforce for signed mints. - */ - function updateSignedMintValidationParams( - address seaDropImpl, - address signer, - SignedMintValidationParams memory signedMintValidationParams - ) external virtual override onlyOwnerOrAdministrator { - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Track the previous signed mint validation params. - SignedMintValidationParams memory retrieved = - ISeaDrop(seaDropImpl).getSignedMintValidationParams(address(this), signer); - - // Track the newly supplied params. - SignedMintValidationParams memory supplied = signedMintValidationParams; - - // Only the administrator (OpenSea) can set feeBps on Partner - // contracts. - if (msg.sender != administrator) { - // Administrator must first set fee. - if (retrieved.maxMaxTotalMintableByWallet == 0) { - revert AdministratorMustInitializeWithFee(); - } - supplied.minFeeBps = retrieved.minFeeBps; - supplied.maxFeeBps = retrieved.maxFeeBps; - } else { - // Administrator can only initialize - // (maxTotalMintableByWallet > 0) and set - // feeBps/restrictFeeRecipients. - uint24 maxMaxTotalMintableByWallet = retrieved.maxMaxTotalMintableByWallet; - retrieved.maxMaxTotalMintableByWallet = maxMaxTotalMintableByWallet > 0 ? maxMaxTotalMintableByWallet : 1; - retrieved.minFeeBps = supplied.minFeeBps; - retrieved.maxFeeBps = supplied.maxFeeBps; - supplied = retrieved; - } - - // Update the signed mint validation params. - ISeaDrop(seaDropImpl).updateSignedMintValidationParams(signer, supplied); - } - - /** - * @notice Update the allowed payers for this nft contract on SeaDrop. - * Only the owner or administrator can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param payer The payer to update. - * @param allowed Whether the payer is allowed. - */ - function updatePayer(address seaDropImpl, address payer, bool allowed) - external - virtual - override - onlyOwnerOrAdministrator - { - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the payer. - ISeaDrop(seaDropImpl).updatePayer(payer, allowed); - } -} diff --git a/src/seadrop/ERC721SeaDrop.sol b/src/seadrop/ERC721SeaDrop.sol deleted file mode 100644 index a23696a..0000000 --- a/src/seadrop/ERC721SeaDrop.sol +++ /dev/null @@ -1,596 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {ERC721ContractMetadata, ISeaDropTokenContractMetadata} from "./ERC721ContractMetadata.sol"; - -import {INonFungibleSeaDropToken} from "./interfaces/INonFungibleSeaDropToken.sol"; - -import {ISeaDrop} from "./interfaces/ISeaDrop.sol"; - -import {AllowListData, PublicDrop, TokenGatedDropStage, SignedMintValidationParams} from "./lib/SeaDropStructs.sol"; - -import {ERC721SeaDropStructsErrorsAndEvents} from "./lib/ERC721SeaDropStructsErrorsAndEvents.sol"; - -import {ERC721A} from "ERC721A/ERC721A.sol"; - -import {ReentrancyGuard} from "openzeppelin-contracts/security/ReentrancyGuard.sol"; -import {IERC165} from "openzeppelin-contracts/interfaces/IERC165.sol"; -/** - * @title ERC721SeaDrop - * @author James Wenzel (emo.eth) - * @author Ryan Ghods (ralxz.eth) - * @author Stephan Min (stephanm.eth) - * @author Michael Cohen (notmichael.eth) - * @notice ERC721SeaDrop is a token contract that contains methods - * to properly interact with SeaDrop. - */ - -contract ERC721SeaDrop is - ERC721ContractMetadata, - INonFungibleSeaDropToken, - ERC721SeaDropStructsErrorsAndEvents, - ReentrancyGuard -{ - /// @notice Track the allowed SeaDrop addresses. - mapping(address => bool) internal _allowedSeaDrop; - - /// @notice Track the enumerated allowed SeaDrop addresses. - address[] internal _enumeratedAllowedSeaDrop; - - /** - * @dev Reverts if not an allowed SeaDrop contract. - * This function is inlined instead of being a modifier - * to save contract space from being inlined N times. - * - * @param seaDrop The SeaDrop address to check if allowed. - */ - function _onlyAllowedSeaDrop(address seaDrop) internal view { - if (_allowedSeaDrop[seaDrop] != true) { - revert OnlyAllowedSeaDrop(); - } - } - - /** - * @notice Deploy the token contract with its name, symbol, - * and allowed SeaDrop addresses. - */ - constructor(string memory name, string memory symbol, address[] memory allowedSeaDrop) - ERC721ContractMetadata(name, symbol) - { - // Put the length on the stack for more efficient access. - uint256 allowedSeaDropLength = allowedSeaDrop.length; - - // Set the mapping for allowed SeaDrop contracts. - for (uint256 i = 0; i < allowedSeaDropLength;) { - _allowedSeaDrop[allowedSeaDrop[i]] = true; - unchecked { - ++i; - } - } - - // Set the enumeration. - _enumeratedAllowedSeaDrop = allowedSeaDrop; - - // Emit an event noting the contract deployment. - emit SeaDropTokenDeployed(); - } - - /** - * @notice Update the allowed SeaDrop contracts. - * Only the owner or administrator can use this function. - * - * @param allowedSeaDrop The allowed SeaDrop addresses. - */ - function updateAllowedSeaDrop(address[] calldata allowedSeaDrop) external virtual override onlyOwner { - _updateAllowedSeaDrop(allowedSeaDrop); - } - - /** - * @notice Internal function to update the allowed SeaDrop contracts. - * - * @param allowedSeaDrop The allowed SeaDrop addresses. - */ - function _updateAllowedSeaDrop(address[] calldata allowedSeaDrop) internal { - // Put the length on the stack for more efficient access. - uint256 enumeratedAllowedSeaDropLength = _enumeratedAllowedSeaDrop.length; - uint256 allowedSeaDropLength = allowedSeaDrop.length; - - // Reset the old mapping. - for (uint256 i = 0; i < enumeratedAllowedSeaDropLength;) { - _allowedSeaDrop[_enumeratedAllowedSeaDrop[i]] = false; - unchecked { - ++i; - } - } - - // Set the new mapping for allowed SeaDrop contracts. - for (uint256 i = 0; i < allowedSeaDropLength;) { - _allowedSeaDrop[allowedSeaDrop[i]] = true; - unchecked { - ++i; - } - } - - // Set the enumeration. - _enumeratedAllowedSeaDrop = allowedSeaDrop; - - // Emit an event for the update. - emit AllowedSeaDropUpdated(allowedSeaDrop); - } - - /** - * @dev Overrides the `_startTokenId` function from ERC721A - * to start at token id `1`. - * - * This is to avoid future possible problems since `0` is usually - * used to signal values that have not been set or have been removed. - */ - function _startTokenId() internal view virtual override returns (uint256) { - return 1; - } - - /** - * @dev Overrides the `tokenURI()` function from ERC721A - * to return just the base URI if it is implied to not be a directory. - * - * This is to help with ERC721 contracts in which the same token URI - * is desired for each token, such as when the tokenURI is 'unrevealed'. - */ - function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { - if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); - - string memory baseURI = _baseURI(); - - // Exit early if the baseURI is empty. - if (bytes(baseURI).length == 0) { - return ""; - } - - // Check if the last character in baseURI is a slash. - if (bytes(baseURI)[bytes(baseURI).length - 1] != bytes("/")[0]) { - return baseURI; - } - - return string(abi.encodePacked(baseURI, _toString(tokenId))); - } - - /** - * @notice Mint tokens, restricted to the SeaDrop contract. - * - * @dev NOTE: If a token registers itself with multiple SeaDrop - * contracts, the implementation of this function should guard - * against reentrancy. If the implementing token uses - * _safeMint(), or a feeRecipient with a malicious receive() hook - * is specified, the token or fee recipients may be able to execute - * another mint in the same transaction via a separate SeaDrop - * contract. - * This is dangerous if an implementing token does not correctly - * update the minterNumMinted and currentTotalSupply values before - * transferring minted tokens, as SeaDrop references these values - * to enforce token limits on a per-wallet and per-stage basis. - * - * ERC721A tracks these values automatically, but this note and - * nonReentrant modifier are left here to encourage best-practices - * when referencing this contract. - * - * @param minter The address to mint to. - * @param quantity The number of tokens to mint. - */ - function mintSeaDrop(address minter, uint256 quantity) external virtual override nonReentrant { - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(msg.sender); - - // Extra safety check to ensure the max supply is not exceeded. - if (_totalMinted() + quantity > maxSupply()) { - revert MintQuantityExceedsMaxSupply(_totalMinted() + quantity, maxSupply()); - } - - // Mint the quantity of tokens to the minter. - _safeMint(minter, quantity); - } - - /** - * @notice Update the public drop data for this nft contract on SeaDrop. - * Only the owner can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param publicDrop The public drop data. - */ - function updatePublicDrop(address seaDropImpl, PublicDrop calldata publicDrop) external virtual override { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the public drop data on SeaDrop. - ISeaDrop(seaDropImpl).updatePublicDrop(publicDrop); - } - - /** - * @notice Update the allow list data for this nft contract on SeaDrop. - * Only the owner can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param allowListData The allow list data. - */ - function updateAllowList(address seaDropImpl, AllowListData calldata allowListData) external virtual override { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the allow list on SeaDrop. - ISeaDrop(seaDropImpl).updateAllowList(allowListData); - } - - /** - * @notice Update the token gated drop stage data for this nft contract - * on SeaDrop. - * Only the owner can use this function. - * - * Note: If two INonFungibleSeaDropToken tokens are doing - * simultaneous token gated drop promotions for each other, - * they can be minted by the same actor until - * `maxTokenSupplyForStage` is reached. Please ensure the - * `allowedNftToken` is not running an active drop during the - * `dropStage` time period. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param allowedNftToken The allowed nft token. - * @param dropStage The token gated drop stage data. - */ - function updateTokenGatedDrop(address seaDropImpl, address allowedNftToken, TokenGatedDropStage calldata dropStage) - external - virtual - override - { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the token gated drop stage. - ISeaDrop(seaDropImpl).updateTokenGatedDrop(allowedNftToken, dropStage); - } - - /** - * @notice Update the drop URI for this nft contract on SeaDrop. - * Only the owner can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param dropURI The new drop URI. - */ - function updateDropURI(address seaDropImpl, string calldata dropURI) external virtual override { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the drop URI. - ISeaDrop(seaDropImpl).updateDropURI(dropURI); - } - - /** - * @notice Update the creator payout address for this nft contract on - * SeaDrop. - * Only the owner can set the creator payout address. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param payoutAddress The new payout address. - */ - function updateCreatorPayoutAddress(address seaDropImpl, address payoutAddress) external { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the creator payout address. - ISeaDrop(seaDropImpl).updateCreatorPayoutAddress(payoutAddress); - } - - /** - * @notice Update the allowed fee recipient for this nft contract - * on SeaDrop. - * Only the owner can set the allowed fee recipient. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param feeRecipient The new fee recipient. - * @param allowed If the fee recipient is allowed. - */ - function updateAllowedFeeRecipient(address seaDropImpl, address feeRecipient, bool allowed) external virtual { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the allowed fee recipient. - ISeaDrop(seaDropImpl).updateAllowedFeeRecipient(feeRecipient, allowed); - } - - /** - * @notice Update the server-side signers for this nft contract - * on SeaDrop. - * Only the owner can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param signer The signer to update. - * @param signedMintValidationParams Minimum and maximum parameters to - * enforce for signed mints. - */ - function updateSignedMintValidationParams( - address seaDropImpl, - address signer, - SignedMintValidationParams memory signedMintValidationParams - ) external virtual override { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the signer. - ISeaDrop(seaDropImpl).updateSignedMintValidationParams(signer, signedMintValidationParams); - } - - /** - * @notice Update the allowed payers for this nft contract on SeaDrop. - * Only the owner can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param payer The payer to update. - * @param allowed Whether the payer is allowed. - */ - function updatePayer(address seaDropImpl, address payer, bool allowed) external virtual override { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the payer. - ISeaDrop(seaDropImpl).updatePayer(payer, allowed); - } - - /** - * @notice Returns a set of mint stats for the address. - * This assists SeaDrop in enforcing maxSupply, - * maxTotalMintableByWallet, and maxTokenSupplyForStage checks. - * - * @dev NOTE: Implementing contracts should always update these numbers - * before transferring any tokens with _safeMint() to mitigate - * consequences of malicious onERC721Received() hooks. - * - * @param minter The minter address. - */ - function getMintStats(address minter) - external - view - override - returns (uint256 minterNumMinted, uint256 currentTotalSupply, uint256 maxSupply) - { - minterNumMinted = _numberMinted(minter); - currentTotalSupply = _totalMinted(); - maxSupply = _maxSupply; - } - - /** - * @notice Returns whether the interface is supported. - * - * @param interfaceId The interface id to check against. - */ - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(IERC165, ERC721ContractMetadata) - returns (bool) - { - return interfaceId == type(INonFungibleSeaDropToken).interfaceId - || interfaceId == type(ISeaDropTokenContractMetadata).interfaceId - // ERC721ContractMetadata returns supportsInterface true for - // EIP-2981 - // ERC721A returns supportsInterface true for - // ERC165, ERC721, ERC721Metadata - || super.supportsInterface(interfaceId); - } - - /** - * @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. - * - The `operator` must be allowed. - * - * Emits an {ApprovalForAll} event. - */ - function setApprovalForAll(address operator, bool approved) public override { - super.setApprovalForAll(operator, approved); - } - - /** - * @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. - * - The `operator` mut be allowed. - * - * Emits an {Approval} event. - */ - function approve(address operator, uint256 tokenId) public override { - super.approve(operator, tokenId); - } - - /** - * @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}. - * - The operator must be allowed. - * - * Emits a {Transfer} event. - */ - function transferFrom(address from, address to, uint256 tokenId) public override { - super.transferFrom(from, to, tokenId); - } - - /** - * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. - */ - function safeTransferFrom(address from, address to, uint256 tokenId) public override { - super.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. - * - The operator must be allowed. - * - * Emits a {Transfer} event. - */ - function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public override { - super.safeTransferFrom(from, to, tokenId, data); - } - - /** - * @notice Configure multiple properties at a time. - * - * Note: The individual configure methods should be used - * to unset or reset any properties to zero, as this method - * will ignore zero-value properties in the config struct. - * - * @param config The configuration struct. - */ - function multiConfigure(MultiConfigureStruct calldata config) external onlyOwner { - if (config.maxSupply > 0) { - this.setMaxSupply(config.maxSupply); - } - if (bytes(config.baseURI).length != 0) { - this.setBaseURI(config.baseURI); - } - if (bytes(config.contractURI).length != 0) { - this.setContractURI(config.contractURI); - } - if (_cast(config.publicDrop.startTime != 0) | _cast(config.publicDrop.endTime != 0) == 1) { - this.updatePublicDrop(config.seaDropImpl, config.publicDrop); - } - if (bytes(config.dropURI).length != 0) { - this.updateDropURI(config.seaDropImpl, config.dropURI); - } - if (config.allowListData.merkleRoot != bytes32(0)) { - this.updateAllowList(config.seaDropImpl, config.allowListData); - } - if (config.creatorPayoutAddress != address(0)) { - this.updateCreatorPayoutAddress(config.seaDropImpl, config.creatorPayoutAddress); - } - if (config.provenanceHash != bytes32(0)) { - this.setProvenanceHash(config.provenanceHash); - } - if (config.allowedFeeRecipients.length > 0) { - for (uint256 i = 0; i < config.allowedFeeRecipients.length;) { - this.updateAllowedFeeRecipient(config.seaDropImpl, config.allowedFeeRecipients[i], true); - unchecked { - ++i; - } - } - } - if (config.disallowedFeeRecipients.length > 0) { - for (uint256 i = 0; i < config.disallowedFeeRecipients.length;) { - this.updateAllowedFeeRecipient(config.seaDropImpl, config.disallowedFeeRecipients[i], false); - unchecked { - ++i; - } - } - } - if (config.allowedPayers.length > 0) { - for (uint256 i = 0; i < config.allowedPayers.length;) { - this.updatePayer(config.seaDropImpl, config.allowedPayers[i], true); - unchecked { - ++i; - } - } - } - if (config.disallowedPayers.length > 0) { - for (uint256 i = 0; i < config.disallowedPayers.length;) { - this.updatePayer(config.seaDropImpl, config.disallowedPayers[i], false); - unchecked { - ++i; - } - } - } - if (config.tokenGatedDropStages.length > 0) { - if (config.tokenGatedDropStages.length != config.tokenGatedAllowedNftTokens.length) { - revert TokenGatedMismatch(); - } - for (uint256 i = 0; i < config.tokenGatedDropStages.length;) { - this.updateTokenGatedDrop( - config.seaDropImpl, config.tokenGatedAllowedNftTokens[i], config.tokenGatedDropStages[i] - ); - unchecked { - ++i; - } - } - } - if (config.disallowedTokenGatedAllowedNftTokens.length > 0) { - for (uint256 i = 0; i < config.disallowedTokenGatedAllowedNftTokens.length;) { - TokenGatedDropStage memory emptyStage; - this.updateTokenGatedDrop( - config.seaDropImpl, config.disallowedTokenGatedAllowedNftTokens[i], emptyStage - ); - unchecked { - ++i; - } - } - } - if (config.signedMintValidationParams.length > 0) { - if (config.signedMintValidationParams.length != config.signers.length) { - revert SignersMismatch(); - } - for (uint256 i = 0; i < config.signedMintValidationParams.length;) { - this.updateSignedMintValidationParams( - config.seaDropImpl, config.signers[i], config.signedMintValidationParams[i] - ); - unchecked { - ++i; - } - } - } - if (config.disallowedSigners.length > 0) { - for (uint256 i = 0; i < config.disallowedSigners.length;) { - SignedMintValidationParams memory emptyParams; - this.updateSignedMintValidationParams(config.seaDropImpl, config.disallowedSigners[i], emptyParams); - unchecked { - ++i; - } - } - } - } -} diff --git a/src/seadrop/SeaDrop.sol b/src/seadrop/SeaDrop.sol deleted file mode 100644 index 46c3239..0000000 --- a/src/seadrop/SeaDrop.sol +++ /dev/null @@ -1,1154 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {ISeaDrop} from "./interfaces/ISeaDrop.sol"; - -import {INonFungibleSeaDropToken} from "./interfaces/INonFungibleSeaDropToken.sol"; - -import { - AllowListData, - MintParams, - PublicDrop, - TokenGatedDropStage, - TokenGatedMintParams, - SignedMintValidationParams -} from "./lib/SeaDropStructs.sol"; - -import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; - -import {ReentrancyGuard} from "openzeppelin-contracts/security/ReentrancyGuard.sol"; - -import {IERC721} from "openzeppelin-contracts/token/ERC721/IERC721.sol"; - -import {IERC165} from "forge-std/interfaces/IERC165.sol"; - -import {ECDSA} from "openzeppelin-contracts/utils/cryptography/ECDSA.sol"; - -import {MerkleProof} from "openzeppelin-contracts/utils/cryptography/MerkleProof.sol"; - -/** - * @title SeaDrop - * @author James Wenzel (emo.eth) - * @author Ryan Ghods (ralxz.eth) - * @author Stephan Min (stephanm.eth) - * @notice SeaDrop is a contract to help facilitate ERC721 token drops - * with functionality for public, allow list, server-side signed, - * and token-gated drops. - */ -contract SeaDrop is ISeaDrop, ReentrancyGuard { - using ECDSA for bytes32; - - /// @notice Track the public drops. - mapping(address => PublicDrop) private _publicDrops; - - /// @notice Track the creator payout addresses. - mapping(address => address) private _creatorPayoutAddresses; - - /// @notice Track the allow list merkle roots. - mapping(address => bytes32) private _allowListMerkleRoots; - - /// @notice Track the allowed fee recipients. - mapping(address => mapping(address => bool)) private _allowedFeeRecipients; - - /// @notice Track the enumerated allowed fee recipients. - mapping(address => address[]) private _enumeratedFeeRecipients; - - /// @notice Track the parameters for allowed signers for server-side drops. - mapping(address => mapping(address => SignedMintValidationParams)) private _signedMintValidationParams; - - /// @notice Track the signers for each server-side drop. - mapping(address => address[]) private _enumeratedSigners; - - /// @notice Track the used signature digests. - mapping(bytes32 => bool) private _usedDigests; - - /// @notice Track the allowed payers. - mapping(address => mapping(address => bool)) private _allowedPayers; - - /// @notice Track the enumerated allowed payers. - mapping(address => address[]) private _enumeratedPayers; - - /// @notice Track the token gated drop stages. - mapping(address => mapping(address => TokenGatedDropStage)) private _tokenGatedDrops; - - /// @notice Track the tokens for token gated drops. - mapping(address => address[]) private _enumeratedTokenGatedTokens; - - /// @notice Track the redeemed token IDs for token gated drop stages. - mapping(address => mapping(address => mapping(uint256 => bool))) private _tokenGatedRedeemed; - - /// @notice Internal constants for EIP-712: Typed structured - /// data hashing and signing - bytes32 internal constant _SIGNED_MINT_TYPEHASH = - // prettier-ignore - keccak256( - "SignedMint(" "address nftContract," "address minter," "address feeRecipient," "MintParams mintParams," - "uint256 salt" ")" "MintParams(" "uint256 mintPrice," "uint256 maxTotalMintableByWallet," "uint256 startTime," - "uint256 endTime," "uint256 dropStageIndex," "uint256 maxTokenSupplyForStage," "uint256 feeBps," - "bool restrictFeeRecipients" ")" - ); - bytes32 internal constant _MINT_PARAMS_TYPEHASH = - // prettier-ignore - keccak256( - "MintParams(" "uint256 mintPrice," "uint256 maxTotalMintableByWallet," "uint256 startTime," "uint256 endTime," - "uint256 dropStageIndex," "uint256 maxTokenSupplyForStage," "uint256 feeBps," "bool restrictFeeRecipients" ")" - ); - bytes32 internal constant _EIP_712_DOMAIN_TYPEHASH = - // prettier-ignore - keccak256("EIP712Domain(" "string name," "string version," "uint256 chainId," "address verifyingContract" ")"); - bytes32 internal constant _NAME_HASH = keccak256("SeaDrop"); - bytes32 internal constant _VERSION_HASH = keccak256("1.0"); - uint256 internal immutable _CHAIN_ID = block.chainid; - bytes32 internal immutable _DOMAIN_SEPARATOR; - - /// @notice Constant for an unlimited `maxTokenSupplyForStage`. - /// Used in `mintPublic` where no `maxTokenSupplyForStage` - /// is stored in the `PublicDrop` struct. - uint256 internal constant _UNLIMITED_MAX_TOKEN_SUPPLY_FOR_STAGE = type(uint256).max; - - /// @notice Constant for a public mint's `dropStageIndex`. - /// Used in `mintPublic` where no `dropStageIndex` - /// is stored in the `PublicDrop` struct. - uint256 internal constant _PUBLIC_DROP_STAGE_INDEX = 0; - - /** - * @notice Ensure only tokens implementing INonFungibleSeaDropToken can - * call the update methods. - */ - modifier onlyINonFungibleSeaDropToken() virtual { - if (!IERC165(msg.sender).supportsInterface(type(INonFungibleSeaDropToken).interfaceId)) { - revert OnlyINonFungibleSeaDropToken(msg.sender); - } - _; - } - - /** - * @notice Constructor for the contract deployment. - */ - constructor() { - // Derive the domain separator. - _DOMAIN_SEPARATOR = _deriveDomainSeparator(); - } - - /** - * @notice Mint a public drop. - * - * @param nftContract The nft contract to mint. - * @param feeRecipient The fee recipient. - * @param minterIfNotPayer The mint recipient if different than the payer. - * @param quantity The number of tokens to mint. - */ - function mintPublic(address nftContract, address feeRecipient, address minterIfNotPayer, uint256 quantity) - external - payable - override - { - // Get the public drop data. - PublicDrop memory publicDrop = _publicDrops[nftContract]; - - // Ensure that the drop has started. - _checkActive(publicDrop.startTime, publicDrop.endTime); - - // Put the mint price on the stack. - uint256 mintPrice = publicDrop.mintPrice; - - // Validate payment is correct for number minted. - _checkCorrectPayment(quantity, mintPrice); - - // Get the minter address. - address minter = minterIfNotPayer != address(0) ? minterIfNotPayer : msg.sender; - - // Ensure the payer is allowed if not the minter. - if (minter != msg.sender) { - if (!_allowedPayers[nftContract][msg.sender]) { - revert PayerNotAllowed(); - } - } - - // Check that the minter is allowed to mint the desired quantity. - _checkMintQuantity( - nftContract, minter, quantity, publicDrop.maxTotalMintableByWallet, _UNLIMITED_MAX_TOKEN_SUPPLY_FOR_STAGE - ); - - // Check that the fee recipient is allowed if restricted. - _checkFeeRecipientIsAllowed(nftContract, feeRecipient, publicDrop.restrictFeeRecipients); - - // Mint the token(s), split the payout, emit an event. - _mintAndPay(nftContract, minter, quantity, mintPrice, _PUBLIC_DROP_STAGE_INDEX, publicDrop.feeBps, feeRecipient); - } - - /** - * @notice Mint from an allow list. - * - * @param nftContract The nft contract to mint. - * @param feeRecipient The fee recipient. - * @param minterIfNotPayer The mint recipient if different than the payer. - * @param quantity The number of tokens to mint. - * @param mintParams The mint parameters. - * @param proof The proof for the leaf of the allow list. - */ - function mintAllowList( - address nftContract, - address feeRecipient, - address minterIfNotPayer, - uint256 quantity, - MintParams calldata mintParams, - bytes32[] calldata proof - ) external payable override { - // Check that the drop stage is active. - _checkActive(mintParams.startTime, mintParams.endTime); - - // Put the mint price on the stack. - uint256 mintPrice = mintParams.mintPrice; - - // Validate payment is correct for number minted. - _checkCorrectPayment(quantity, mintPrice); - - // Get the minter address. - address minter = minterIfNotPayer != address(0) ? minterIfNotPayer : msg.sender; - - // Ensure the payer is allowed if not the minter. - if (minter != msg.sender) { - if (!_allowedPayers[nftContract][msg.sender]) { - revert PayerNotAllowed(); - } - } - - // Check that the minter is allowed to mint the desired quantity. - _checkMintQuantity( - nftContract, minter, quantity, mintParams.maxTotalMintableByWallet, mintParams.maxTokenSupplyForStage - ); - - // Check that the fee recipient is allowed if restricted. - _checkFeeRecipientIsAllowed(nftContract, feeRecipient, mintParams.restrictFeeRecipients); - - // Verify the proof. - if (!MerkleProof.verify(proof, _allowListMerkleRoots[nftContract], keccak256(abi.encode(minter, mintParams)))) { - revert InvalidProof(); - } - - // Mint the token(s), split the payout, emit an event. - _mintAndPay( - nftContract, minter, quantity, mintPrice, mintParams.dropStageIndex, mintParams.feeBps, feeRecipient - ); - } - - /** - * @notice Mint with a server-side signature. - * Note that a signature can only be used once. - * - * @param nftContract The nft contract to mint. - * @param feeRecipient The fee recipient. - * @param minterIfNotPayer The mint recipient if different than the payer. - * @param quantity The number of tokens to mint. - * @param mintParams The mint parameters. - * @param salt The salt for the signed mint. - * @param signature The server-side signature, must be an allowed - * signer. - */ - function mintSigned( - address nftContract, - address feeRecipient, - address minterIfNotPayer, - uint256 quantity, - MintParams calldata mintParams, - uint256 salt, - bytes calldata signature - ) external payable override { - // Check that the drop stage is active. - _checkActive(mintParams.startTime, mintParams.endTime); - - // Validate payment is correct for number minted. - _checkCorrectPayment(quantity, mintParams.mintPrice); - - // Get the minter address. - address minter = minterIfNotPayer != address(0) ? minterIfNotPayer : msg.sender; - - // Ensure the payer is allowed if not the minter. - if (minter != msg.sender) { - if (!_allowedPayers[nftContract][msg.sender]) { - revert PayerNotAllowed(); - } - } - - // Check that the minter is allowed to mint the desired quantity. - _checkMintQuantity( - nftContract, minter, quantity, mintParams.maxTotalMintableByWallet, mintParams.maxTokenSupplyForStage - ); - - // Check that the fee recipient is allowed if restricted. - _checkFeeRecipientIsAllowed(nftContract, feeRecipient, mintParams.restrictFeeRecipients); - - // Validate the signature in a block scope to avoid "stack too deep". - { - // Get the digest to verify the EIP-712 signature. - bytes32 digest = _getDigest(nftContract, minter, feeRecipient, mintParams, salt); - - // Ensure the digest has not already been used. - if (_usedDigests[digest]) { - revert SignatureAlreadyUsed(); - } - - // Mark the digest as used. - _usedDigests[digest] = true; - - // Use the recover method to see what address was used to create - // the signature on this data. - // Note that if the digest doesn't exactly match what was signed we'll - // get a random recovered address. - address recoveredAddress = digest.recover(signature); - _validateSignerAndParams(nftContract, mintParams, recoveredAddress); - } - - // Mint the token(s), split the payout, emit an event. - _mintAndPay( - nftContract, - minter, - quantity, - mintParams.mintPrice, - mintParams.dropStageIndex, - mintParams.feeBps, - feeRecipient - ); - } - - /** - * @notice Enforce stored parameters for signed mints to mitigate - * the effects of a malicious signer. - */ - function _validateSignerAndParams(address nftContract, MintParams memory mintParams, address signer) - internal - view - { - SignedMintValidationParams memory signedMintValidationParams = _signedMintValidationParams[nftContract][signer]; - - // Check that SignedMintValidationParams have been initialized; if not, - // this is an invalid signer. - if (signedMintValidationParams.maxMaxTotalMintableByWallet == 0) { - revert InvalidSignature(signer); - } - - // Validate individual params. - if (mintParams.mintPrice < signedMintValidationParams.minMintPrice) { - revert InvalidSignedMintPrice(mintParams.mintPrice, signedMintValidationParams.minMintPrice); - } - if (mintParams.maxTotalMintableByWallet > signedMintValidationParams.maxMaxTotalMintableByWallet) { - revert InvalidSignedMaxTotalMintableByWallet( - mintParams.maxTotalMintableByWallet, signedMintValidationParams.maxMaxTotalMintableByWallet - ); - } - if (mintParams.startTime < signedMintValidationParams.minStartTime) { - revert InvalidSignedStartTime(mintParams.startTime, signedMintValidationParams.minStartTime); - } - if (mintParams.endTime > signedMintValidationParams.maxEndTime) { - revert InvalidSignedEndTime(mintParams.endTime, signedMintValidationParams.maxEndTime); - } - if (mintParams.maxTokenSupplyForStage > signedMintValidationParams.maxMaxTokenSupplyForStage) { - revert InvalidSignedMaxTokenSupplyForStage( - mintParams.maxTokenSupplyForStage, signedMintValidationParams.maxMaxTokenSupplyForStage - ); - } - if (mintParams.feeBps > signedMintValidationParams.maxFeeBps) { - revert InvalidSignedFeeBps(mintParams.feeBps, signedMintValidationParams.maxFeeBps); - } - if (mintParams.feeBps < signedMintValidationParams.minFeeBps) { - revert InvalidSignedFeeBps(mintParams.feeBps, signedMintValidationParams.minFeeBps); - } - if (!mintParams.restrictFeeRecipients) { - revert SignedMintsMustRestrictFeeRecipients(); - } - } - - /** - * @notice Mint as an allowed token holder. - * This will mark the token ids as redeemed and will revert if the - * same token id is attempted to be redeemed twice. - * - * @param nftContract The nft contract to mint. - * @param feeRecipient The fee recipient. - * @param minterIfNotPayer The mint recipient if different than the payer. - * @param mintParams The token gated mint params. - */ - function mintAllowedTokenHolder( - address nftContract, - address feeRecipient, - address minterIfNotPayer, - TokenGatedMintParams calldata mintParams - ) external payable override { - // Get the minter address. - address minter = minterIfNotPayer != address(0) ? minterIfNotPayer : msg.sender; - - // Ensure the payer is allowed if not the minter. - if (minter != msg.sender) { - if (!_allowedPayers[nftContract][msg.sender]) { - revert PayerNotAllowed(); - } - } - - // Put the allowedNftToken on the stack for more efficient access. - address allowedNftToken = mintParams.allowedNftToken; - - // Set the dropStage to a variable. - TokenGatedDropStage memory dropStage = _tokenGatedDrops[nftContract][allowedNftToken]; - - // Validate that the dropStage is active. - _checkActive(dropStage.startTime, dropStage.endTime); - - // Check that the fee recipient is allowed if restricted. - _checkFeeRecipientIsAllowed(nftContract, feeRecipient, dropStage.restrictFeeRecipients); - - // Put the mint quantity on the stack for more efficient access. - uint256 mintQuantity = mintParams.allowedNftTokenIds.length; - - // Validate payment is correct for number minted. - _checkCorrectPayment(mintQuantity, dropStage.mintPrice); - - // Check that the minter is allowed to mint the desired quantity. - _checkMintQuantity( - nftContract, minter, mintQuantity, dropStage.maxTotalMintableByWallet, dropStage.maxTokenSupplyForStage - ); - - // Iterate through each allowedNftTokenId - // to ensure it is not already redeemed. - for (uint256 i = 0; i < mintQuantity;) { - // Put the tokenId on the stack. - uint256 tokenId = mintParams.allowedNftTokenIds[i]; - - // Check that the minter is the owner of the allowedNftTokenId. - if (IERC721(allowedNftToken).ownerOf(tokenId) != minter) { - revert TokenGatedNotTokenOwner(nftContract, allowedNftToken, tokenId); - } - - // Cache the storage pointer for cheaper access. - mapping(uint256 => bool) storage redeemedTokenIds = _tokenGatedRedeemed[nftContract][allowedNftToken]; - - // Check that the token id has not already been redeemed. - if (redeemedTokenIds[tokenId]) { - revert TokenGatedTokenIdAlreadyRedeemed(nftContract, allowedNftToken, tokenId); - } - - // Mark the token id as redeemed. - redeemedTokenIds[tokenId] = true; - - unchecked { - ++i; - } - } - - // Mint the token(s), split the payout, emit an event. - _mintAndPay( - nftContract, - minter, - mintQuantity, - dropStage.mintPrice, - dropStage.dropStageIndex, - dropStage.feeBps, - feeRecipient - ); - } - - /** - * @notice Check that the drop stage is active. - * - * @param startTime The drop stage start time. - * @param endTime The drop stage end time. - */ - function _checkActive(uint256 startTime, uint256 endTime) internal view { - if (_cast(block.timestamp < startTime) | _cast(block.timestamp > endTime) == 1) { - // Revert if the drop stage is not active. - revert NotActive(block.timestamp, startTime, endTime); - } - } - - /** - * @notice Check that the fee recipient is allowed. - * - * @param nftContract The nft contract. - * @param feeRecipient The fee recipient. - * @param restrictFeeRecipients If the fee recipients are restricted. - */ - function _checkFeeRecipientIsAllowed(address nftContract, address feeRecipient, bool restrictFeeRecipients) - internal - view - { - // Ensure the fee recipient is not the zero address. - if (feeRecipient == address(0)) { - revert FeeRecipientCannotBeZeroAddress(); - } - - // Revert if the fee recipient is restricted and not allowed. - if (restrictFeeRecipients) { - if (!_allowedFeeRecipients[nftContract][feeRecipient]) { - revert FeeRecipientNotAllowed(); - } - } - } - - /** - * @notice Check that the wallet is allowed to mint the desired quantity. - * - * @param nftContract The nft contract. - * @param minter The mint recipient. - * @param quantity The number of tokens to mint. - * @param maxTotalMintableByWallet The max allowed mints per wallet. - * @param maxTokenSupplyForStage The max token supply for the drop stage. - */ - function _checkMintQuantity( - address nftContract, - address minter, - uint256 quantity, - uint256 maxTotalMintableByWallet, - uint256 maxTokenSupplyForStage - ) internal view { - // Mint quantity of zero is not valid. - if (quantity == 0) { - revert MintQuantityCannotBeZero(); - } - - // Get the mint stats. - (uint256 minterNumMinted, uint256 currentTotalSupply, uint256 maxSupply) = - INonFungibleSeaDropToken(nftContract).getMintStats(minter); - - // Ensure mint quantity doesn't exceed maxTotalMintableByWallet. - if (quantity + minterNumMinted > maxTotalMintableByWallet) { - revert MintQuantityExceedsMaxMintedPerWallet(quantity + minterNumMinted, maxTotalMintableByWallet); - } - - // Ensure mint quantity doesn't exceed maxSupply. - if (quantity + currentTotalSupply > maxSupply) { - revert MintQuantityExceedsMaxSupply(quantity + currentTotalSupply, maxSupply); - } - - // Ensure mint quantity doesn't exceed maxTokenSupplyForStage. - if (quantity + currentTotalSupply > maxTokenSupplyForStage) { - revert MintQuantityExceedsMaxTokenSupplyForStage(quantity + currentTotalSupply, maxTokenSupplyForStage); - } - } - - /** - * @notice Revert if the payment is not the quantity times the mint price. - * - * @param quantity The number of tokens to mint. - * @param mintPrice The mint price per token. - */ - function _checkCorrectPayment(uint256 quantity, uint256 mintPrice) internal view { - // Revert if the tx's value doesn't match the total cost. - if (msg.value != quantity * mintPrice) { - revert IncorrectPayment(msg.value, quantity * mintPrice); - } - } - - /** - * @notice Split the payment payout for the creator and fee recipient. - * - * @param nftContract The nft contract. - * @param feeRecipient The fee recipient. - * @param feeBps The fee basis points. - */ - function _splitPayout(address nftContract, address feeRecipient, uint256 feeBps) internal { - // Revert if the fee basis points is greater than 10_000. - if (feeBps > 10_000) { - revert InvalidFeeBps(feeBps); - } - - // Get the creator payout address. - address creatorPayoutAddress = _creatorPayoutAddresses[nftContract]; - - // Ensure the creator payout address is not the zero address. - if (creatorPayoutAddress == address(0)) { - revert CreatorPayoutAddressCannotBeZeroAddress(); - } - - // msg.value has already been validated by this point, so can use it directly. - - // If the fee is zero, just transfer to the creator and return. - if (feeBps == 0) { - SafeTransferLib.safeTransferETH(creatorPayoutAddress, msg.value); - return; - } - - // Get the fee amount. - // Note that the fee amount is rounded down in favor of the creator. - uint256 feeAmount = (msg.value * feeBps) / 10_000; - - // Get the creator payout amount. Fee amount is <= msg.value per above. - uint256 payoutAmount; - unchecked { - payoutAmount = msg.value - feeAmount; - } - - // Transfer the fee amount to the fee recipient. - if (feeAmount > 0) { - SafeTransferLib.safeTransferETH(feeRecipient, feeAmount); - } - - // Transfer the creator payout amount to the creator. - SafeTransferLib.safeTransferETH(creatorPayoutAddress, payoutAmount); - } - - /** - * @notice Mints a number of tokens, splits the payment, - * and emits an event. - * - * @param nftContract The nft contract. - * @param minter The mint recipient. - * @param quantity The number of tokens to mint. - * @param mintPrice The mint price per token. - * @param dropStageIndex The drop stage index. - * @param feeBps The fee basis points. - * @param feeRecipient The fee recipient. - */ - function _mintAndPay( - address nftContract, - address minter, - uint256 quantity, - uint256 mintPrice, - uint256 dropStageIndex, - uint256 feeBps, - address feeRecipient - ) internal nonReentrant { - // Mint the token(s). - INonFungibleSeaDropToken(nftContract).mintSeaDrop(minter, quantity); - - if (mintPrice != 0) { - // Split the payment between the creator and fee recipient. - _splitPayout(nftContract, feeRecipient, feeBps); - } - - // Emit an event for the mint. - emit SeaDropMint(nftContract, minter, feeRecipient, msg.sender, quantity, mintPrice, feeBps, dropStageIndex); - } - - /** - * @dev Internal view function to get the EIP-712 domain separator. If the - * chainId matches the chainId set on deployment, the cached domain - * separator will be returned; otherwise, it will be derived from - * scratch. - * - * @return The domain separator. - */ - function _domainSeparator() internal view returns (bytes32) { - // prettier-ignore - return block.chainid == _CHAIN_ID ? _DOMAIN_SEPARATOR : _deriveDomainSeparator(); - } - - /** - * @dev Internal view function to derive the EIP-712 domain separator. - * - * @return The derived domain separator. - */ - function _deriveDomainSeparator() internal view returns (bytes32) { - // prettier-ignore - return keccak256(abi.encode(_EIP_712_DOMAIN_TYPEHASH, _NAME_HASH, _VERSION_HASH, block.chainid, address(this))); - } - - /** - * @notice Returns the public drop data for the nft contract. - * - * @param nftContract The nft contract. - */ - function getPublicDrop(address nftContract) external view returns (PublicDrop memory) { - return _publicDrops[nftContract]; - } - - /** - * @notice Returns the creator payout address for the nft contract. - * - * @param nftContract The nft contract. - */ - function getCreatorPayoutAddress(address nftContract) external view returns (address) { - return _creatorPayoutAddresses[nftContract]; - } - - /** - * @notice Returns the allow list merkle root for the nft contract. - * - * @param nftContract The nft contract. - */ - function getAllowListMerkleRoot(address nftContract) external view returns (bytes32) { - return _allowListMerkleRoots[nftContract]; - } - - /** - * @notice Returns if the specified fee recipient is allowed - * for the nft contract. - * - * @param nftContract The nft contract. - */ - function getFeeRecipientIsAllowed(address nftContract, address feeRecipient) external view returns (bool) { - return _allowedFeeRecipients[nftContract][feeRecipient]; - } - - /** - * @notice Returns an enumeration of allowed fee recipients for an - * nft contract when fee recipients are enforced. - * - * @param nftContract The nft contract. - */ - function getAllowedFeeRecipients(address nftContract) external view returns (address[] memory) { - return _enumeratedFeeRecipients[nftContract]; - } - - /** - * @notice Returns the server-side signers for the nft contract. - * - * @param nftContract The nft contract. - */ - function getSigners(address nftContract) external view returns (address[] memory) { - return _enumeratedSigners[nftContract]; - } - - /** - * @notice Returns the struct of SignedMintValidationParams for a signer. - * - * @param nftContract The nft contract. - * @param signer The signer. - */ - function getSignedMintValidationParams(address nftContract, address signer) - external - view - returns (SignedMintValidationParams memory) - { - return _signedMintValidationParams[nftContract][signer]; - } - - /** - * @notice Returns the payers for the nft contract. - * - * @param nftContract The nft contract. - */ - function getPayers(address nftContract) external view returns (address[] memory) { - return _enumeratedPayers[nftContract]; - } - - /** - * @notice Returns if the specified payer is allowed - * for the nft contract. - * - * @param nftContract The nft contract. - * @param payer The payer. - */ - function getPayerIsAllowed(address nftContract, address payer) external view returns (bool) { - return _allowedPayers[nftContract][payer]; - } - - /** - * @notice Returns the allowed token gated drop tokens for the nft contract. - * - * @param nftContract The nft contract. - */ - function getTokenGatedAllowedTokens(address nftContract) external view returns (address[] memory) { - return _enumeratedTokenGatedTokens[nftContract]; - } - - /** - * @notice Returns the token gated drop data for the nft contract - * and token gated nft. - * - * @param nftContract The nft contract. - * @param allowedNftToken The token gated nft token. - */ - function getTokenGatedDrop(address nftContract, address allowedNftToken) - external - view - returns (TokenGatedDropStage memory) - { - return _tokenGatedDrops[nftContract][allowedNftToken]; - } - - /** - * @notice Returns whether the token id for a token gated drop has been - * redeemed. - * - * @param nftContract The nft contract. - * @param allowedNftToken The token gated nft token. - * @param allowedNftTokenId The token gated nft token id to check. - */ - function getAllowedNftTokenIdIsRedeemed(address nftContract, address allowedNftToken, uint256 allowedNftTokenId) - external - view - returns (bool) - { - return _tokenGatedRedeemed[nftContract][allowedNftToken][allowedNftTokenId]; - } - - /** - * @notice Emits an event to notify update of the drop URI. - * - * This method assume msg.sender is an nft contract and its - * ERC165 interface id matches INonFungibleSeaDropToken. - * - * Note: Be sure only authorized users can call this from - * token contracts that implement INonFungibleSeaDropToken. - * - * @param dropURI The new drop URI. - */ - function updateDropURI(string calldata dropURI) external onlyINonFungibleSeaDropToken { - // Emit an event with the update. - emit DropURIUpdated(msg.sender, dropURI); - } - - /** - * @notice Updates the public drop data for the nft contract - * and emits an event. - * - * This method assume msg.sender is an nft contract and its - * ERC165 interface id matches INonFungibleSeaDropToken. - * - * Note: Be sure only authorized users can call this from - * token contracts that implement INonFungibleSeaDropToken. - * - * @param publicDrop The public drop data. - */ - function updatePublicDrop(PublicDrop calldata publicDrop) external override onlyINonFungibleSeaDropToken { - // Revert if the fee basis points is greater than 10_000. - if (publicDrop.feeBps > 10_000) { - revert InvalidFeeBps(publicDrop.feeBps); - } - - // Set the public drop data. - _publicDrops[msg.sender] = publicDrop; - - // Emit an event with the update. - emit PublicDropUpdated(msg.sender, publicDrop); - } - - /** - * @notice Updates the allow list merkle root for the nft contract - * and emits an event. - * - * This method assume msg.sender is an nft contract and its - * ERC165 interface id matches INonFungibleSeaDropToken. - * - * Note: Be sure only authorized users can call this from - * token contracts that implement INonFungibleSeaDropToken. - * - * @param allowListData The allow list data. - */ - function updateAllowList(AllowListData calldata allowListData) external override onlyINonFungibleSeaDropToken { - // Track the previous root. - bytes32 prevRoot = _allowListMerkleRoots[msg.sender]; - - // Update the merkle root. - _allowListMerkleRoots[msg.sender] = allowListData.merkleRoot; - - // Emit an event with the update. - emit AllowListUpdated( - msg.sender, prevRoot, allowListData.merkleRoot, allowListData.publicKeyURIs, allowListData.allowListURI - ); - } - - /** - * @notice Updates the token gated drop stage for the nft contract - * and emits an event. - * - * This method assume msg.sender is an nft contract and its - * ERC165 interface id matches INonFungibleSeaDropToken. - * - * Note: Be sure only authorized users can call this from - * token contracts that implement INonFungibleSeaDropToken. - * - * Note: If two INonFungibleSeaDropToken tokens are doing - * simultaneous token gated drop promotions for each other, - * they can be minted by the same actor until - * `maxTokenSupplyForStage` is reached. Please ensure the - * `allowedNftToken` is not running an active drop during - * the `dropStage` time period. - * - * @param allowedNftToken The token gated nft token. - * @param dropStage The token gated drop stage data. - */ - function updateTokenGatedDrop(address allowedNftToken, TokenGatedDropStage calldata dropStage) - external - override - onlyINonFungibleSeaDropToken - { - // Ensure the allowedNftToken is not the zero address. - if (allowedNftToken == address(0)) { - revert TokenGatedDropAllowedNftTokenCannotBeZeroAddress(); - } - - // Ensure the allowedNftToken cannot be the drop token itself. - if (allowedNftToken == msg.sender) { - revert TokenGatedDropAllowedNftTokenCannotBeDropToken(); - } - - // Revert if the fee basis points is greater than 10_000. - if (dropStage.feeBps > 10_000) { - revert InvalidFeeBps(dropStage.feeBps); - } - - // Use maxTotalMintableByWallet != 0 as a signal that this update should - // add or update the drop stage, otherwise we will be removing. - bool addOrUpdateDropStage = dropStage.maxTotalMintableByWallet != 0; - - // Get pointers to the token gated drop data and enumerated addresses. - TokenGatedDropStage storage existingDropStageData = _tokenGatedDrops[msg.sender][allowedNftToken]; - address[] storage enumeratedTokens = _enumeratedTokenGatedTokens[msg.sender]; - - // Stage struct packs to a single slot, so load it - // as a uint256; if it is 0, it is empty. - bool dropStageDoesNotExist; - assembly { - dropStageDoesNotExist := iszero(sload(existingDropStageData.slot)) - } - - if (addOrUpdateDropStage) { - _tokenGatedDrops[msg.sender][allowedNftToken] = dropStage; - // Add to enumeration if it does not exist already. - if (dropStageDoesNotExist) { - enumeratedTokens.push(allowedNftToken); - } - } else { - // Check we are not deleting a drop stage that does not exist. - if (dropStageDoesNotExist) { - revert TokenGatedDropStageNotPresent(); - } - // Clear storage slot and remove from enumeration. - delete _tokenGatedDrops[msg.sender][allowedNftToken]; - _removeFromEnumeration(allowedNftToken, enumeratedTokens); - } - - // Emit an event with the update. - emit TokenGatedDropStageUpdated(msg.sender, allowedNftToken, dropStage); - } - - /** - * @notice Updates the creator payout address and emits an event. - * - * This method assume msg.sender is an nft contract and its - * ERC165 interface id matches INonFungibleSeaDropToken. - * - * Note: Be sure only authorized users can call this from - * token contracts that implement INonFungibleSeaDropToken. - * - * @param payoutAddress The creator payout address. - */ - function updateCreatorPayoutAddress(address payoutAddress) external onlyINonFungibleSeaDropToken { - if (payoutAddress == address(0)) { - revert CreatorPayoutAddressCannotBeZeroAddress(); - } - // Set the creator payout address. - _creatorPayoutAddresses[msg.sender] = payoutAddress; - - // Emit an event with the update. - emit CreatorPayoutAddressUpdated(msg.sender, payoutAddress); - } - - /** - * @notice Updates the allowed fee recipient and emits an event. - * - * This method assume msg.sender is an nft contract and its - * ERC165 interface id matches INonFungibleSeaDropToken. - * - * Note: Be sure only authorized users can call this from - * token contracts that implement INonFungibleSeaDropToken. - * - * @param feeRecipient The fee recipient. - * @param allowed If the fee recipient is allowed. - */ - function updateAllowedFeeRecipient(address feeRecipient, bool allowed) external onlyINonFungibleSeaDropToken { - if (feeRecipient == address(0)) { - revert FeeRecipientCannotBeZeroAddress(); - } - - // Track the enumerated storage. - address[] storage enumeratedStorage = _enumeratedFeeRecipients[msg.sender]; - mapping(address => bool) storage feeRecipientsMap = _allowedFeeRecipients[msg.sender]; - - if (allowed) { - if (feeRecipientsMap[feeRecipient]) { - revert DuplicateFeeRecipient(); - } - feeRecipientsMap[feeRecipient] = true; - enumeratedStorage.push(feeRecipient); - } else { - if (!feeRecipientsMap[feeRecipient]) { - revert FeeRecipientNotPresent(); - } - delete _allowedFeeRecipients[msg.sender][feeRecipient]; - _removeFromEnumeration(feeRecipient, enumeratedStorage); - } - - // Emit an event with the update. - emit AllowedFeeRecipientUpdated(msg.sender, feeRecipient, allowed); - } - - /** - * @notice Updates the allowed server-side signers and emits an event. - * - * This method assume msg.sender is an nft contract and its - * ERC165 interface id matches INonFungibleSeaDropToken. - * - * Note: Be sure only authorized users can call this from - * token contracts that implement INonFungibleSeaDropToken. - * - * @param signer The signer to update. - * @param signedMintValidationParams Minimum and maximum parameters - * to enforce for signed mints. - */ - function updateSignedMintValidationParams( - address signer, - SignedMintValidationParams calldata signedMintValidationParams - ) external onlyINonFungibleSeaDropToken { - if (signer == address(0)) { - revert SignerCannotBeZeroAddress(); - } - - if (signedMintValidationParams.minFeeBps > 10_000) { - revert InvalidFeeBps(signedMintValidationParams.minFeeBps); - } - if (signedMintValidationParams.maxFeeBps > 10_000) { - revert InvalidFeeBps(signedMintValidationParams.maxFeeBps); - } - - // Track the enumerated storage. - address[] storage enumeratedStorage = _enumeratedSigners[msg.sender]; - mapping(address => SignedMintValidationParams) storage signedMintValidationParamsMap = - _signedMintValidationParams[msg.sender]; - - SignedMintValidationParams storage existingSignedMintValidationParams = signedMintValidationParamsMap[signer]; - - bool signedMintValidationParamsDoNotExist; - assembly { - signedMintValidationParamsDoNotExist := iszero(sload(existingSignedMintValidationParams.slot)) - } - // Use maxMaxTotalMintableByWallet as sentry for add/update or delete. - bool addOrUpdate = signedMintValidationParams.maxMaxTotalMintableByWallet > 0; - - if (addOrUpdate) { - signedMintValidationParamsMap[signer] = signedMintValidationParams; - if (signedMintValidationParamsDoNotExist) { - enumeratedStorage.push(signer); - } - } else { - if (existingSignedMintValidationParams.maxMaxTotalMintableByWallet == 0) { - revert SignerNotPresent(); - } - delete _signedMintValidationParams[msg.sender][signer]; - _removeFromEnumeration(signer, enumeratedStorage); - } - - // Emit an event with the update. - emit SignedMintValidationParamsUpdated(msg.sender, signer, signedMintValidationParams); - } - - /** - * @notice Updates the allowed payer and emits an event. - * - * This method assume msg.sender is an nft contract and its - * ERC165 interface id matches INonFungibleSeaDropToken. - * - * Note: Be sure only authorized users can call this from - * token contracts that implement INonFungibleSeaDropToken. - * - * @param payer The payer to add or remove. - * @param allowed Whether to add or remove the payer. - */ - function updatePayer(address payer, bool allowed) external onlyINonFungibleSeaDropToken { - if (payer == address(0)) { - revert PayerCannotBeZeroAddress(); - } - - // Track the enumerated storage. - address[] storage enumeratedStorage = _enumeratedPayers[msg.sender]; - mapping(address => bool) storage payersMap = _allowedPayers[msg.sender]; - - if (allowed) { - if (payersMap[payer]) { - revert DuplicatePayer(); - } - payersMap[payer] = true; - enumeratedStorage.push(payer); - } else { - if (!payersMap[payer]) { - revert PayerNotPresent(); - } - delete _allowedPayers[msg.sender][payer]; - _removeFromEnumeration(payer, enumeratedStorage); - } - - // Emit an event with the update. - emit PayerUpdated(msg.sender, payer, allowed); - } - - /** - * @notice Remove an address from a supplied enumeration. - * - * @param toRemove The address to remove. - * @param enumeration The enumerated addresses to parse. - */ - function _removeFromEnumeration(address toRemove, address[] storage enumeration) internal { - // Cache the length. - uint256 enumerationLength = enumeration.length; - for (uint256 i = 0; i < enumerationLength;) { - // Check if the enumerated element is the one we are deleting. - if (enumeration[i] == toRemove) { - // Swap with the last element. - enumeration[i] = enumeration[enumerationLength - 1]; - // Delete the (now duplicated) last element. - enumeration.pop(); - // Exit the loop. - break; - } - unchecked { - ++i; - } - } - } - - /** - * @notice Verify an EIP-712 signature by recreating the data structure - * that we signed on the client side, and then using that to recover - * the address that signed the signature for this data. - * - * @param nftContract The nft contract. - * @param minter The mint recipient. - * @param feeRecipient The fee recipient. - * @param mintParams The mint params. - * @param salt The salt for the signed mint. - */ - function _getDigest( - address nftContract, - address minter, - address feeRecipient, - MintParams memory mintParams, - uint256 salt - ) internal view returns (bytes32 digest) { - bytes32 mintParamsHashStruct = keccak256( - abi.encode( - _MINT_PARAMS_TYPEHASH, - mintParams.mintPrice, - mintParams.maxTotalMintableByWallet, - mintParams.startTime, - mintParams.endTime, - mintParams.dropStageIndex, - mintParams.maxTokenSupplyForStage, - mintParams.feeBps, - mintParams.restrictFeeRecipients - ) - ); - digest = keccak256( - bytes.concat( - bytes2(0x1901), - _domainSeparator(), - keccak256( - abi.encode(_SIGNED_MINT_TYPEHASH, nftContract, minter, feeRecipient, mintParamsHashStruct, salt) - ) - ) - ); - } - - /** - * @dev Internal pure function to cast a `bool` value to a `uint256` value. - * - * @param b The `bool` value to cast. - * - * @return u The `uint256` value. - */ - function _cast(bool b) internal pure returns (uint256 u) { - assembly { - u := b - } - } -} diff --git a/src/seadrop/clones/ERC721ACloneable.sol b/src/seadrop/clones/ERC721ACloneable.sol deleted file mode 100644 index 7633867..0000000 --- a/src/seadrop/clones/ERC721ACloneable.sol +++ /dev/null @@ -1,1237 +0,0 @@ -// SPDX-License-Identifier: MIT -// ERC721A Contracts v4.2.2 -// Creator: Chiru Labs - -pragma solidity ^0.8.4; - -import { IERC721A } from "ERC721A/IERC721A.sol"; - -import { - Initializable -} from "openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; - -/** - * @dev Interface of ERC721 token receiver. - */ -interface ERC721A__IERC721Receiver { - 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 ERC721ACloneable is IERC721A, Initializable { - // Bypass for a `--via-ir` bug (https://github.com/chiru-labs/ERC721A/pull/364). - struct TokenApprovalRef { - address value; - } - - // ============================================================= - // 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; - - // ============================================================= - // STORAGE - // ============================================================= - - // The next token ID to be minted. - uint256 private _currentIndex; - - // The number of tokens burned. - uint256 private _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 {_packedOwnershipOf} implementation for details. - // - // Bits Layout: - // - [0..159] `addr` - // - [160..223] `startTimestamp` - // - [224] `burned` - // - [225] `nextInitialized` - // - [232..255] `extraData` - mapping(uint256 => uint256) private _packedOwnerships; - - // Mapping owner address to address data. - // - // Bits Layout: - // - [0..63] `balance` - // - [64..127] `numberMinted` - // - [128..191] `numberBurned` - // - [192..255] `aux` - mapping(address => uint256) private _packedAddressData; - - // Mapping from token ID to approved address. - mapping(uint256 => TokenApprovalRef) private _tokenApprovals; - - // Mapping from owner to operator approvals - mapping(address => mapping(address => bool)) private _operatorApprovals; - - // ============================================================= - // CONSTRUCTOR - // ============================================================= - - function __ERC721ACloneable__init( - string memory name_, - string memory symbol_ - ) internal onlyInitializing { - _name = name_; - _symbol = symbol_; - _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 _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 _currentIndex - _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 _currentIndex - _startTokenId(); - } - } - - /** - * @dev Returns the total number of tokens burned. - */ - function _totalBurned() internal view virtual returns (uint256) { - return _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(); - return _packedAddressData[owner] & _BITMASK_ADDRESS_DATA_ENTRY; - } - - /** - * Returns the number of tokens minted by `owner`. - */ - function _numberMinted(address owner) internal view returns (uint256) { - return - (_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 - (_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(_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 = _packedAddressData[owner]; - uint256 auxCasted; - // Cast `aux` with assembly to avoid redundant masking. - assembly { - auxCasted := aux - } - packed = - (packed & _BITMASK_AUX_COMPLEMENT) | - (auxCasted << _BITPOS_AUX); - _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 _name; - } - - /** - * @dev Returns the token collection symbol. - */ - function symbol() public view virtual override returns (string memory) { - return _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(); - - 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(_packedOwnerships[index]); - } - - /** - * @dev Initializes the ownership slot minted at `index` for efficiency purposes. - */ - function _initializeOwnershipAt(uint256 index) internal virtual { - if (_packedOwnerships[index] == 0) { - _packedOwnerships[index] = _packedOwnershipOf(index); - } - } - - /** - * Returns the packed ownership data of `tokenId`. - */ - function _packedOwnershipOf(uint256 tokenId) - private - view - returns (uint256) - { - uint256 curr = tokenId; - - unchecked { - if (_startTokenId() <= curr) { - if (curr < _currentIndex) { - uint256 packed = _packedOwnerships[curr]; - // If not burned. - if (packed & _BITMASK_BURNED == 0) { - // 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, `curr` will not underflow. - // - // We can directly compare the packed value. - // If the address is zero, packed will be zero. - while (packed == 0) { - packed = _packedOwnerships[--curr]; - } - return packed; - } - } - } - } - revert OwnerQueryForNonexistentToken(); - } - - /** - * @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. - * 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) public virtual override { - address owner = ownerOf(tokenId); - - if (_msgSenderERC721A() != owner) { - if (!isApprovedForAll(owner, _msgSenderERC721A())) { - revert ApprovalCallerNotOwnerNorApproved(); - } - } - - _tokenApprovals[tokenId].value = to; - emit Approval(owner, to, tokenId); - } - - /** - * @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(); - - return _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 - { - _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 _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) { - return - _startTokenId() <= tokenId && - tokenId < _currentIndex && // If within bounds, - _packedOwnerships[tokenId] & _BITMASK_BURNED == 0; // and not burned. - } - - /** - * @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) - { - TokenApprovalRef storage tokenApproval = _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 virtual override { - uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); - - if (address(uint160(prevOwnershipPacked)) != from) - revert TransferFromIncorrectOwner(); - - ( - 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(); - } - - if (to == address(0)) revert TransferToZeroAddress(); - - _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. - --_packedAddressData[from]; // Updates: `balance -= 1`. - ++_packedAddressData[to]; // Updates: `balance += 1`. - - // Updates: - // - `address` to the next owner. - // - `startTimestamp` to the timestamp of transfering. - // - `burned` to `false`. - // - `nextInitialized` to `true`. - _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 (_packedOwnerships[nextTokenId] == 0) { - // If the next slot is within bounds. - if (nextTokenId != _currentIndex) { - // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. - _packedOwnerships[nextTokenId] = prevOwnershipPacked; - } - } - } - } - - emit Transfer(from, to, tokenId); - _afterTokenTransfers(from, to, tokenId, 1); - } - - /** - * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. - */ - function safeTransferFrom( - address from, - address to, - uint256 tokenId - ) public 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 virtual override { - transferFrom(from, to, tokenId); - if (to.code.length != 0) { - if (!_checkContractOnERC721Received(from, to, tokenId, _data)) { - revert TransferToNonERC721ReceiverImplementer(); - } - } - } - - /** - * @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__IERC721Receiver(to).onERC721Received( - _msgSenderERC721A(), - from, - tokenId, - _data - ) - returns (bytes4 retval) { - return - retval == - ERC721A__IERC721Receiver(to).onERC721Received.selector; - } catch (bytes memory reason) { - if (reason.length == 0) { - revert TransferToNonERC721ReceiverImplementer(); - } else { - 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 = _currentIndex; - if (quantity == 0) revert MintZeroQuantity(); - - _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: - // - `balance += quantity`. - // - `numberMinted += quantity`. - // - // We can directly add to the `balance` and `numberMinted`. - _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`. - _packedOwnerships[startTokenId] = _packOwnershipData( - to, - _nextInitializedFlag(quantity) | - _nextExtraData(address(0), to, 0) - ); - - uint256 toMasked; - uint256 end = startTokenId + quantity; - - // Use assembly to loop and emit the `Transfer` event for gas savings. - // The duplicated `log4` removes an extra check and reduces stack juggling. - // The assembly, together with the surrounding Solidity code, have been - // delicately arranged to nudge the compiler into producing optimized opcodes. - assembly { - // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean. - toMasked := and(to, _BITMASK_ADDRESS) - // 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`. - startTokenId // `tokenId`. - ) - - // The `iszero(eq(,))` check ensures that large values of `quantity` - // that overflows uint256 will make the loop run out of gas. - // The compiler will optimize the `iszero` away for performance. - for { - let tokenId := add(startTokenId, 1) - } iszero(eq(tokenId, end)) { - tokenId := add(tokenId, 1) - } { - // Emit the `Transfer` event. Similar to above. - log4(0, 0, _TRANSFER_EVENT_SIGNATURE, 0, toMasked, tokenId) - } - } - if (toMasked == 0) revert MintToZeroAddress(); - - _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 = _currentIndex; - if (to == address(0)) revert MintToZeroAddress(); - if (quantity == 0) revert MintZeroQuantity(); - if (quantity > _MAX_MINT_ERC2309_QUANTITY_LIMIT) - revert MintERC2309QuantityExceedsLimit(); - - _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`. - _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`. - _packedOwnerships[startTokenId] = _packOwnershipData( - to, - _nextInitializedFlag(quantity) | - _nextExtraData(address(0), to, 0) - ); - - emit ConsecutiveTransfer( - startTokenId, - startTokenId + quantity - 1, - address(0), - to - ); - - _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 = _currentIndex; - uint256 index = end - quantity; - do { - if ( - !_checkContractOnERC721Received( - address(0), - to, - index++, - _data - ) - ) { - revert TransferToNonERC721ReceiverImplementer(); - } - } while (index < end); - // Reentrancy protection. - if (_currentIndex != end) revert(); - } - } - } - - /** - * @dev Equivalent to `_safeMint(to, quantity, '')`. - */ - function _safeMint(address to, uint256 quantity) internal virtual { - _safeMint(to, quantity, ""); - } - - // ============================================================= - // 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(); - } - } - - _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;`. - _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`. - _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 (_packedOwnerships[nextTokenId] == 0) { - // If the next slot is within bounds. - if (nextTokenId != _currentIndex) { - // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. - _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 { - _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 = _packedOwnerships[index]; - if (packed == 0) revert OwnershipNotInitializedForExtraData(); - uint256 extraDataCasted; - // Cast `extraData` with assembly to avoid redundant masking. - assembly { - extraDataCasted := extraData - } - packed = - (packed & _BITMASK_EXTRA_DATA_COMPLEMENT) | - (extraDataCasted << _BITPOS_EXTRA_DATA); - _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) - } - } -} diff --git a/src/seadrop/clones/ERC721ContractMetadataCloneable.sol b/src/seadrop/clones/ERC721ContractMetadataCloneable.sol deleted file mode 100644 index e3a87c0..0000000 --- a/src/seadrop/clones/ERC721ContractMetadataCloneable.sol +++ /dev/null @@ -1,285 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {ISeaDropTokenContractMetadata} from "../interfaces/ISeaDropTokenContractMetadata.sol"; - -import {ERC721ACloneable} from "./ERC721ACloneable.sol"; - -import {Ownable} from "solady/auth/Ownable.sol"; - -import {IERC2981} from "openzeppelin-contracts/interfaces/IERC2981.sol"; - -import {IERC165} from "openzeppelin-contracts/interfaces/IERC165.sol"; - -/** - * @title ERC721ContractMetadataCloneable - * @author James Wenzel (emo.eth) - * @author Ryan Ghods (ralxz.eth) - * @author Stephan Min (stephanm.eth) - * @notice ERC721ContractMetadata is a token contract that extends ERC721A - * with additional metadata and ownership capabilities. - */ -contract ERC721ContractMetadataCloneable is ERC721ACloneable, Ownable, ISeaDropTokenContractMetadata { - /// @notice Track the max supply. - uint256 _maxSupply; - - /// @notice Track the base URI for token metadata. - string _tokenBaseURI; - - /// @notice Track the contract URI for contract metadata. - string _contractURI; - - /// @notice Track the provenance hash for guaranteeing metadata order - /// for random reveals. - bytes32 _provenanceHash; - - /// @notice Track the royalty info: address to receive royalties, and - /// royalty basis points. - RoyaltyInfo _royaltyInfo; - - /** - * @dev Reverts if the sender is not the owner or the contract itself. - * This function is inlined instead of being a modifier - * to save contract space from being inlined N times. - */ - function _onlyOwnerOrSelf() internal view { - if (_cast(msg.sender == owner()) | _cast(msg.sender == address(this)) == 0) { - revert Unauthorized(); - } - } - - /** - * @notice Sets the base URI for the token metadata and emits an event. - * - * @param newBaseURI The new base URI to set. - */ - function setBaseURI(string calldata newBaseURI) external override { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Set the new base URI. - _tokenBaseURI = newBaseURI; - - // Emit an event with the update. - if (totalSupply() != 0) { - emit BatchMetadataUpdate(1, _nextTokenId() - 1); - } - } - - /** - * @notice Sets the contract URI for contract metadata. - * - * @param newContractURI The new contract URI. - */ - function setContractURI(string calldata newContractURI) external override { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Set the new contract URI. - _contractURI = newContractURI; - - // Emit an event with the update. - emit ContractURIUpdated(newContractURI); - } - - /** - * @notice Emit an event notifying metadata updates for - * a range of token ids, according to EIP-4906. - * - * @param fromTokenId The start token id. - * @param toTokenId The end token id. - */ - function emitBatchMetadataUpdate(uint256 fromTokenId, uint256 toTokenId) external { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Emit an event with the update. - emit BatchMetadataUpdate(fromTokenId, toTokenId); - } - - /** - * @notice Sets the max token supply and emits an event. - * - * @param newMaxSupply The new max supply to set. - */ - function setMaxSupply(uint256 newMaxSupply) external { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the max supply does not exceed the maximum value of uint64. - if (newMaxSupply > 2 ** 64 - 1) { - revert CannotExceedMaxSupplyOfUint64(newMaxSupply); - } - - // Set the new max supply. - _maxSupply = newMaxSupply; - - // Emit an event with the update. - emit MaxSupplyUpdated(newMaxSupply); - } - - /** - * @notice Sets the provenance hash and emits an event. - * - * The provenance hash is used for random reveals, which - * is a hash of the ordered metadata to show it has not been - * modified after mint started. - * - * This function will revert after the first item has been minted. - * - * @param newProvenanceHash The new provenance hash to set. - */ - function setProvenanceHash(bytes32 newProvenanceHash) external { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Revert if any items have been minted. - if (_totalMinted() > 0) { - revert ProvenanceHashCannotBeSetAfterMintStarted(); - } - - // Keep track of the old provenance hash for emitting with the event. - bytes32 oldProvenanceHash = _provenanceHash; - - // Set the new provenance hash. - _provenanceHash = newProvenanceHash; - - // Emit an event with the update. - emit ProvenanceHashUpdated(oldProvenanceHash, newProvenanceHash); - } - - /** - * @notice Sets the address and basis points for royalties. - * - * @param newInfo The struct to configure royalties. - */ - function setRoyaltyInfo(RoyaltyInfo calldata newInfo) external { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Revert if the new royalty address is the zero address. - if (newInfo.royaltyAddress == address(0)) { - revert RoyaltyAddressCannotBeZeroAddress(); - } - - // Revert if the new basis points is greater than 10_000. - if (newInfo.royaltyBps > 10_000) { - revert InvalidRoyaltyBasisPoints(newInfo.royaltyBps); - } - - // Set the new royalty info. - _royaltyInfo = newInfo; - - // Emit an event with the updated params. - emit RoyaltyInfoUpdated(newInfo.royaltyAddress, newInfo.royaltyBps); - } - - /** - * @notice Returns the base URI for token metadata. - */ - function baseURI() external view override returns (string memory) { - return _baseURI(); - } - - /** - * @notice Returns the base URI for the contract, which ERC721A uses - * to return tokenURI. - */ - function _baseURI() internal view virtual override returns (string memory) { - return _tokenBaseURI; - } - - /** - * @notice Returns the contract URI for contract metadata. - */ - function contractURI() external view override returns (string memory) { - return _contractURI; - } - - /** - * @notice Returns the max token supply. - */ - function maxSupply() public view returns (uint256) { - return _maxSupply; - } - - /** - * @notice Returns the provenance hash. - * The provenance hash is used for random reveals, which - * is a hash of the ordered metadata to show it is unmodified - * after mint has started. - */ - function provenanceHash() external view override returns (bytes32) { - return _provenanceHash; - } - - /** - * @notice Returns the address that receives royalties. - */ - function royaltyAddress() external view returns (address) { - return _royaltyInfo.royaltyAddress; - } - - /** - * @notice Returns the royalty basis points out of 10_000. - */ - function royaltyBasisPoints() external view returns (uint256) { - return _royaltyInfo.royaltyBps; - } - - /** - * @notice Called with the sale price to determine how much royalty - * is owed and to whom. - * - * @ param _tokenId The NFT asset queried for royalty information. - * @param _salePrice The sale price of the NFT asset specified by - * _tokenId. - * - * @return receiver Address of who should be sent the royalty payment. - * @return royaltyAmount The royalty payment amount for _salePrice. - */ - function royaltyInfo( - uint256, - /* _tokenId */ - uint256 _salePrice - ) external view returns (address receiver, uint256 royaltyAmount) { - // Put the royalty info on the stack for more efficient access. - RoyaltyInfo storage info = _royaltyInfo; - - // Set the royalty amount to the sale price times the royalty basis - // points divided by 10_000. - royaltyAmount = (_salePrice * info.royaltyBps) / 10_000; - - // Set the receiver of the royalty. - receiver = info.royaltyAddress; - } - - /** - * @notice Returns whether the interface is supported. - * - * @param interfaceId The interface id to check against. - */ - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(IERC165, ERC721ACloneable) - returns (bool) - { - return interfaceId == type(IERC2981).interfaceId || interfaceId == 0x49064906 // ERC-4906 - || super.supportsInterface(interfaceId); - } - - /** - * @dev Internal pure function to cast a `bool` value to a `uint256` value. - * - * @param b The `bool` value to cast. - * - * @return u The `uint256` value. - */ - function _cast(bool b) internal pure returns (uint256 u) { - assembly { - u := b - } - } -} diff --git a/src/seadrop/clones/ERC721SeaDropCloneFactory.sol b/src/seadrop/clones/ERC721SeaDropCloneFactory.sol deleted file mode 100644 index 750ff1d..0000000 --- a/src/seadrop/clones/ERC721SeaDropCloneFactory.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {ERC721SeaDropCloneable} from "./ERC721SeaDropCloneable.sol"; - -import {Clones} from "openzeppelin-contracts/proxy/Clones.sol"; - -contract ERC721SeaDropCloneFactory { - address public immutable seaDropCloneableUpgradeableImplementation; - address public constant DEFAULT_SEADROP = 0x00005EA00Ac477B1030CE78506496e8C2dE24bf5; - - constructor() { - ERC721SeaDropCloneable impl = new ERC721SeaDropCloneable(); - impl.initialize("", "", new address[](0), address(this)); - seaDropCloneableUpgradeableImplementation = address(impl); - } - - function createClone(string memory name, string memory symbol, bytes32 salt) external returns (address) { - // Derive a pseudo-random salt, so clone addresses don't collide - // across chains. - bytes32 cloneSalt = keccak256(abi.encodePacked(salt, blockhash(block.number))); - - address instance = Clones.cloneDeterministic(seaDropCloneableUpgradeableImplementation, cloneSalt); - address[] memory allowedSeaDrop = new address[](1); - allowedSeaDrop[0] = DEFAULT_SEADROP; - ERC721SeaDropCloneable(instance).initialize(name, symbol, allowedSeaDrop, msg.sender); - return instance; - } -} diff --git a/src/seadrop/clones/ERC721SeaDropCloneable.sol b/src/seadrop/clones/ERC721SeaDropCloneable.sol deleted file mode 100644 index 145b58e..0000000 --- a/src/seadrop/clones/ERC721SeaDropCloneable.sol +++ /dev/null @@ -1,588 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {INonFungibleSeaDropToken} from "../interfaces/INonFungibleSeaDropToken.sol"; - -import {ISeaDrop} from "../interfaces/ISeaDrop.sol"; - -import {AllowListData, PublicDrop, TokenGatedDropStage, SignedMintValidationParams} from "../lib/SeaDropStructs.sol"; - -import {ERC721SeaDropStructsErrorsAndEvents} from "../lib/ERC721SeaDropStructsErrorsAndEvents.sol"; - -import {ERC721ACloneable} from "./ERC721ACloneable.sol"; - -import {ReentrancyGuardUpgradeable} from "openzeppelin-contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; - -import {IERC165} from "openzeppelin-contracts/interfaces/IERC165.sol"; -import {ERC721ContractMetadataCloneable} from "./ERC721ContractMetadataCloneable.sol"; -import {ISeaDropTokenContractMetadata} from "../interfaces/ISeaDropTokenContractMetadata.sol"; -/** - * @title ERC721SeaDrop - * @author James Wenzel (emo.eth) - * @author Ryan Ghods (ralxz.eth) - * @author Stephan Min (stephanm.eth) - * @notice ERC721SeaDrop is a token contract that contains methods - * to properly interact with SeaDrop. - */ - -contract ERC721SeaDropCloneable is - ERC721ContractMetadataCloneable, - INonFungibleSeaDropToken, - ERC721SeaDropStructsErrorsAndEvents, - ReentrancyGuardUpgradeable -{ - /// @notice Track the allowed SeaDrop addresses. - mapping(address => bool) internal _allowedSeaDrop; - - /// @notice Track the enumerated allowed SeaDrop addresses. - address[] internal _enumeratedAllowedSeaDrop; - - /** - * @dev Reverts if not an allowed SeaDrop contract. - * This function is inlined instead of being a modifier - * to save contract space from being inlined N times. - * - * @param seaDrop The SeaDrop address to check if allowed. - */ - function _onlyAllowedSeaDrop(address seaDrop) internal view { - if (_allowedSeaDrop[seaDrop] != true) { - revert OnlyAllowedSeaDrop(); - } - } - - /** - * @notice Deploy the token contract with its name, symbol, - * and allowed SeaDrop addresses. - */ - function initialize( - string calldata __name, - string calldata __symbol, - address[] calldata allowedSeaDrop, - address initialOwner - ) public initializer { - __ERC721ACloneable__init(__name, __symbol); - __ReentrancyGuard_init(); - _updateAllowedSeaDrop(allowedSeaDrop); - _initializeOwner(initialOwner); - emit SeaDropTokenDeployed(); - } - - /** - * @notice Update the allowed SeaDrop contracts. - * Only the owner or administrator can use this function. - * - * @param allowedSeaDrop The allowed SeaDrop addresses. - */ - function updateAllowedSeaDrop(address[] calldata allowedSeaDrop) external virtual override onlyOwner { - _updateAllowedSeaDrop(allowedSeaDrop); - } - - /** - * @notice Internal function to update the allowed SeaDrop contracts. - * - * @param allowedSeaDrop The allowed SeaDrop addresses. - */ - function _updateAllowedSeaDrop(address[] calldata allowedSeaDrop) internal { - // Put the length on the stack for more efficient access. - uint256 enumeratedAllowedSeaDropLength = _enumeratedAllowedSeaDrop.length; - uint256 allowedSeaDropLength = allowedSeaDrop.length; - - // Reset the old mapping. - for (uint256 i = 0; i < enumeratedAllowedSeaDropLength;) { - _allowedSeaDrop[_enumeratedAllowedSeaDrop[i]] = false; - unchecked { - ++i; - } - } - - // Set the new mapping for allowed SeaDrop contracts. - for (uint256 i = 0; i < allowedSeaDropLength;) { - _allowedSeaDrop[allowedSeaDrop[i]] = true; - unchecked { - ++i; - } - } - - // Set the enumeration. - _enumeratedAllowedSeaDrop = allowedSeaDrop; - - // Emit an event for the update. - emit AllowedSeaDropUpdated(allowedSeaDrop); - } - - /** - * @dev Overrides the `_startTokenId` function from ERC721A - * to start at token id `1`. - * - * This is to avoid future possible problems since `0` is usually - * used to signal values that have not been set or have been removed. - */ - function _startTokenId() internal view virtual override returns (uint256) { - return 1; - } - - /** - * @dev Overrides the `tokenURI()` function from ERC721A - * to return just the base URI if it is implied to not be a directory. - * - * This is to help with ERC721 contracts in which the same token URI - * is desired for each token, such as when the tokenURI is 'unrevealed'. - */ - function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { - if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); - - string memory baseURI = _baseURI(); - - // Exit early if the baseURI is empty. - if (bytes(baseURI).length == 0) { - return ""; - } - - // Check if the last character in baseURI is a slash. - if (bytes(baseURI)[bytes(baseURI).length - 1] != bytes("/")[0]) { - return baseURI; - } - - return string(abi.encodePacked(baseURI, _toString(tokenId))); - } - - /** - * @notice Mint tokens, restricted to the SeaDrop contract. - * - * @dev NOTE: If a token registers itself with multiple SeaDrop - * contracts, the implementation of this function should guard - * against reentrancy. If the implementing token uses - * _safeMint(), or a feeRecipient with a malicious receive() hook - * is specified, the token or fee recipients may be able to execute - * another mint in the same transaction via a separate SeaDrop - * contract. - * This is dangerous if an implementing token does not correctly - * update the minterNumMinted and currentTotalSupply values before - * transferring minted tokens, as SeaDrop references these values - * to enforce token limits on a per-wallet and per-stage basis. - * - * ERC721A tracks these values automatically, but this note and - * nonReentrant modifier are left here to encourage best-practices - * when referencing this contract. - * - * @param minter The address to mint to. - * @param quantity The number of tokens to mint. - */ - function mintSeaDrop(address minter, uint256 quantity) external virtual override nonReentrant { - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(msg.sender); - - // Extra safety check to ensure the max supply is not exceeded. - if (_totalMinted() + quantity > maxSupply()) { - revert MintQuantityExceedsMaxSupply(_totalMinted() + quantity, maxSupply()); - } - - // Mint the quantity of tokens to the minter. - _safeMint(minter, quantity); - } - - /** - * @notice Update the public drop data for this nft contract on SeaDrop. - * Only the owner can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param publicDrop The public drop data. - */ - function updatePublicDrop(address seaDropImpl, PublicDrop calldata publicDrop) external virtual override { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the public drop data on SeaDrop. - ISeaDrop(seaDropImpl).updatePublicDrop(publicDrop); - } - - /** - * @notice Update the allow list data for this nft contract on SeaDrop. - * Only the owner can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param allowListData The allow list data. - */ - function updateAllowList(address seaDropImpl, AllowListData calldata allowListData) external virtual override { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the allow list on SeaDrop. - ISeaDrop(seaDropImpl).updateAllowList(allowListData); - } - - /** - * @notice Update the token gated drop stage data for this nft contract - * on SeaDrop. - * Only the owner can use this function. - * - * Note: If two INonFungibleSeaDropToken tokens are doing - * simultaneous token gated drop promotions for each other, - * they can be minted by the same actor until - * `maxTokenSupplyForStage` is reached. Please ensure the - * `allowedNftToken` is not running an active drop during the - * `dropStage` time period. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param allowedNftToken The allowed nft token. - * @param dropStage The token gated drop stage data. - */ - function updateTokenGatedDrop(address seaDropImpl, address allowedNftToken, TokenGatedDropStage calldata dropStage) - external - virtual - override - { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the token gated drop stage. - ISeaDrop(seaDropImpl).updateTokenGatedDrop(allowedNftToken, dropStage); - } - - /** - * @notice Update the drop URI for this nft contract on SeaDrop. - * Only the owner can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param dropURI The new drop URI. - */ - function updateDropURI(address seaDropImpl, string calldata dropURI) external virtual override { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the drop URI. - ISeaDrop(seaDropImpl).updateDropURI(dropURI); - } - - /** - * @notice Update the creator payout address for this nft contract on - * SeaDrop. - * Only the owner can set the creator payout address. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param payoutAddress The new payout address. - */ - function updateCreatorPayoutAddress(address seaDropImpl, address payoutAddress) external { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the creator payout address. - ISeaDrop(seaDropImpl).updateCreatorPayoutAddress(payoutAddress); - } - - /** - * @notice Update the allowed fee recipient for this nft contract - * on SeaDrop. - * Only the owner can set the allowed fee recipient. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param feeRecipient The new fee recipient. - * @param allowed If the fee recipient is allowed. - */ - function updateAllowedFeeRecipient(address seaDropImpl, address feeRecipient, bool allowed) external virtual { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the allowed fee recipient. - ISeaDrop(seaDropImpl).updateAllowedFeeRecipient(feeRecipient, allowed); - } - - /** - * @notice Update the server-side signers for this nft contract - * on SeaDrop. - * Only the owner can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param signer The signer to update. - * @param signedMintValidationParams Minimum and maximum parameters to - * enforce for signed mints. - */ - function updateSignedMintValidationParams( - address seaDropImpl, - address signer, - SignedMintValidationParams memory signedMintValidationParams - ) external virtual override { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the signer. - ISeaDrop(seaDropImpl).updateSignedMintValidationParams(signer, signedMintValidationParams); - } - - /** - * @notice Update the allowed payers for this nft contract on SeaDrop. - * Only the owner can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param payer The payer to update. - * @param allowed Whether the payer is allowed. - */ - function updatePayer(address seaDropImpl, address payer, bool allowed) external virtual override { - // Ensure the sender is only the owner or contract itself. - _onlyOwnerOrSelf(); - - // Ensure the SeaDrop is allowed. - _onlyAllowedSeaDrop(seaDropImpl); - - // Update the payer. - ISeaDrop(seaDropImpl).updatePayer(payer, allowed); - } - - /** - * @notice Returns a set of mint stats for the address. - * This assists SeaDrop in enforcing maxSupply, - * maxTotalMintableByWallet, and maxTokenSupplyForStage checks. - * - * @dev NOTE: Implementing contracts should always update these numbers - * before transferring any tokens with _safeMint() to mitigate - * consequences of malicious onERC721Received() hooks. - * - * @param minter The minter address. - */ - function getMintStats(address minter) - external - view - override - returns (uint256 minterNumMinted, uint256 currentTotalSupply, uint256 maxSupply) - { - minterNumMinted = _numberMinted(minter); - currentTotalSupply = _totalMinted(); - maxSupply = _maxSupply; - } - - /** - * @notice Returns whether the interface is supported. - * - * @param interfaceId The interface id to check against. - */ - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(IERC165, ERC721ContractMetadataCloneable) - returns (bool) - { - return interfaceId == type(INonFungibleSeaDropToken).interfaceId - || interfaceId == type(ISeaDropTokenContractMetadata).interfaceId - // ERC721ContractMetadata returns supportsInterface true for - // EIP-2981 - // ERC721A returns supportsInterface true for - // ERC165, ERC721, ERC721Metadata - || super.supportsInterface(interfaceId); - } - - /** - * @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. - * - The `operator` must be allowed. - * - * Emits an {ApprovalForAll} event. - */ - function setApprovalForAll(address operator, bool approved) public override { - super.setApprovalForAll(operator, approved); - } - - /** - * @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. - * - The `operator` mut be allowed. - * - * Emits an {Approval} event. - */ - function approve(address operator, uint256 tokenId) public override { - super.approve(operator, tokenId); - } - - /** - * @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}. - * - The operator must be allowed. - * - * Emits a {Transfer} event. - */ - function transferFrom(address from, address to, uint256 tokenId) public override { - super.transferFrom(from, to, tokenId); - } - - /** - * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. - */ - function safeTransferFrom(address from, address to, uint256 tokenId) public override { - super.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. - * - The operator must be allowed. - * - * Emits a {Transfer} event. - */ - function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public override { - super.safeTransferFrom(from, to, tokenId, data); - } - - /** - * @notice Configure multiple properties at a time. - * - * Note: The individual configure methods should be used - * to unset or reset any properties to zero, as this method - * will ignore zero-value properties in the config struct. - * - * @param config The configuration struct. - */ - function multiConfigure(MultiConfigureStruct calldata config) external onlyOwner { - if (config.maxSupply > 0) { - this.setMaxSupply(config.maxSupply); - } - if (bytes(config.baseURI).length != 0) { - this.setBaseURI(config.baseURI); - } - if (bytes(config.contractURI).length != 0) { - this.setContractURI(config.contractURI); - } - if (_cast(config.publicDrop.startTime != 0) | _cast(config.publicDrop.endTime != 0) == 1) { - this.updatePublicDrop(config.seaDropImpl, config.publicDrop); - } - if (bytes(config.dropURI).length != 0) { - this.updateDropURI(config.seaDropImpl, config.dropURI); - } - if (config.allowListData.merkleRoot != bytes32(0)) { - this.updateAllowList(config.seaDropImpl, config.allowListData); - } - if (config.creatorPayoutAddress != address(0)) { - this.updateCreatorPayoutAddress(config.seaDropImpl, config.creatorPayoutAddress); - } - if (config.provenanceHash != bytes32(0)) { - this.setProvenanceHash(config.provenanceHash); - } - if (config.allowedFeeRecipients.length > 0) { - for (uint256 i = 0; i < config.allowedFeeRecipients.length;) { - this.updateAllowedFeeRecipient(config.seaDropImpl, config.allowedFeeRecipients[i], true); - unchecked { - ++i; - } - } - } - if (config.disallowedFeeRecipients.length > 0) { - for (uint256 i = 0; i < config.disallowedFeeRecipients.length;) { - this.updateAllowedFeeRecipient(config.seaDropImpl, config.disallowedFeeRecipients[i], false); - unchecked { - ++i; - } - } - } - if (config.allowedPayers.length > 0) { - for (uint256 i = 0; i < config.allowedPayers.length;) { - this.updatePayer(config.seaDropImpl, config.allowedPayers[i], true); - unchecked { - ++i; - } - } - } - if (config.disallowedPayers.length > 0) { - for (uint256 i = 0; i < config.disallowedPayers.length;) { - this.updatePayer(config.seaDropImpl, config.disallowedPayers[i], false); - unchecked { - ++i; - } - } - } - if (config.tokenGatedDropStages.length > 0) { - if (config.tokenGatedDropStages.length != config.tokenGatedAllowedNftTokens.length) { - revert TokenGatedMismatch(); - } - for (uint256 i = 0; i < config.tokenGatedDropStages.length;) { - this.updateTokenGatedDrop( - config.seaDropImpl, config.tokenGatedAllowedNftTokens[i], config.tokenGatedDropStages[i] - ); - unchecked { - ++i; - } - } - } - if (config.disallowedTokenGatedAllowedNftTokens.length > 0) { - for (uint256 i = 0; i < config.disallowedTokenGatedAllowedNftTokens.length;) { - TokenGatedDropStage memory emptyStage; - this.updateTokenGatedDrop( - config.seaDropImpl, config.disallowedTokenGatedAllowedNftTokens[i], emptyStage - ); - unchecked { - ++i; - } - } - } - if (config.signedMintValidationParams.length > 0) { - if (config.signedMintValidationParams.length != config.signers.length) { - revert SignersMismatch(); - } - for (uint256 i = 0; i < config.signedMintValidationParams.length;) { - this.updateSignedMintValidationParams( - config.seaDropImpl, config.signers[i], config.signedMintValidationParams[i] - ); - unchecked { - ++i; - } - } - } - if (config.disallowedSigners.length > 0) { - for (uint256 i = 0; i < config.disallowedSigners.length;) { - SignedMintValidationParams memory emptyParams; - this.updateSignedMintValidationParams(config.seaDropImpl, config.disallowedSigners[i], emptyParams); - unchecked { - ++i; - } - } - } - } -} diff --git a/src/seadrop/extensions/ERC721PartnerSeaDropBurnable.sol b/src/seadrop/extensions/ERC721PartnerSeaDropBurnable.sol deleted file mode 100644 index 7ade010..0000000 --- a/src/seadrop/extensions/ERC721PartnerSeaDropBurnable.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {ERC721PartnerSeaDrop} from "../ERC721PartnerSeaDrop.sol"; -/** - * @title ERC721PartnerSeaDropBurnable - * @author James Wenzel (emo.eth) - * @author Ryan Ghods (ralxz.eth) - * @author Stephan Min (stephanm.eth) - * @notice ERC721PartnerSeaDropBurnable is a token contract that extends - * ERC721PartnerSeaDrop to additionally provide a burn function. - */ - -contract ERC721PartnerSeaDropBurnable is ERC721PartnerSeaDrop { - /** - * @notice Deploy the token contract with its name, symbol, - * administrator, and allowed SeaDrop addresses. - */ - constructor(string memory name, string memory symbol, address administrator, address[] memory allowedSeaDrop) - ERC721PartnerSeaDrop(name, symbol, administrator, allowedSeaDrop) - {} - - /** - * @notice Burns `tokenId`. The caller must own `tokenId` or be an - * approved operator. - * - * @param tokenId The token id to burn. - */ - // solhint-disable-next-line comprehensive-interface - function burn(uint256 tokenId) external { - _burn(tokenId, true); - } -} diff --git a/src/seadrop/extensions/ERC721PartnerSeaDropRandomOffset.sol b/src/seadrop/extensions/ERC721PartnerSeaDropRandomOffset.sol deleted file mode 100644 index 16d2dd3..0000000 --- a/src/seadrop/extensions/ERC721PartnerSeaDropRandomOffset.sol +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {ERC721PartnerSeaDrop} from "../ERC721PartnerSeaDrop.sol"; - -/** - * @title ERC721PartnerSeaDropRandomOffset - * @author James Wenzel (emo.eth) - * @author Ryan Ghods (ralxz.eth) - * @author Stephan Min (stephanm.eth) - * @notice ERC721PartnerSeaDropRandomOffset is a token contract that extends - * ERC721PartnerSeaDrop to apply a randomOffset to the tokenURI, - * to enable fair metadata reveals. - */ -contract ERC721PartnerSeaDropRandomOffset is ERC721PartnerSeaDrop { - /// @notice The random offset, between 1 and the MAX_SUPPLY at the time of - /// being set. - uint256 public randomOffset; - - /// @notice If the collection has been revealed and the randomOffset has - /// been set. 1=False, 2=True. - uint256 public revealed = _REVEALED_FALSE; - - /// @dev For gas efficiency, uint is used instead of bool for revealed. - uint256 private constant _REVEALED_FALSE = 1; - uint256 private constant _REVEALED_TRUE = 2; - - /// @notice Revert when setting the randomOffset if already set. - error AlreadyRevealed(); - - /// @notice Revert when setting the randomOffset if the collection is - /// not yet fully minted. - error NotFullyMinted(); - - /** - * @notice Deploy the token contract with its name, symbol, - * administrator, and allowed SeaDrop addresses. - */ - constructor(string memory name, string memory symbol, address administrator, address[] memory allowedSeaDrop) - ERC721PartnerSeaDrop(name, symbol, administrator, allowedSeaDrop) - {} - - /** - * @notice Set the random offset, for a fair metadata reveal. Only callable - * by the owner one time when the total number of minted tokens - * equals the max supply. Should be called immediately before - * reveal. - */ - // solhint-disable-next-line comprehensive-interface - function setRandomOffset() external onlyOwner { - // Revert setting the offset if already revealed. - if (revealed == _REVEALED_TRUE) { - revert AlreadyRevealed(); - } - - // Put maxSupply on the stack, since reading a state variable - // costs more gas than reading a local variable. - uint256 maxSupply = _maxSupply; - - // Revert if the collection is not yet fully minted. - if (_totalMinted() != maxSupply) { - revert NotFullyMinted(); - } - - // block.prevrandao returns PREVRANDAO on Ethereum post-merge - // NOTE: do not use this on other chains - // randomOffset returns between 1 and MAX_SUPPLY - randomOffset = (uint256(keccak256(abi.encode(block.prevrandao))) % (maxSupply - 1)) + 1; - - // Set revealed to true. - revealed = _REVEALED_TRUE; - } - - /** - * @notice The token URI, offset by randomOffset, to enable fair metadata - * reveals. - * - * @param tokenId The token id - */ - function tokenURI(uint256 tokenId) public view override returns (string memory) { - if (!_exists(tokenId)) { - revert URIQueryForNonexistentToken(); - } - - string memory base = _baseURI(); - if (bytes(base).length == 0) { - // If there is no baseURI set, return an empty string. - return ""; - } else if (revealed == _REVEALED_FALSE) { - // If the baseURI is set but the collection is not revealed yet, - // return just the baseURI. - return base; - } else { - // If the baseURI is set and the collection is revealed, - // return the tokenURI offset by the randomOffset. - return string.concat(base, _toString(((tokenId + randomOffset) % _maxSupply) + _startTokenId())); - } - } -} diff --git a/src/seadrop/extensions/ERC721SeaDropBurnable.sol b/src/seadrop/extensions/ERC721SeaDropBurnable.sol deleted file mode 100644 index 39828ca..0000000 --- a/src/seadrop/extensions/ERC721SeaDropBurnable.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {ERC721SeaDrop} from "../ERC721SeaDrop.sol"; - -/** - * @title ERC721SeaDropBurnable - * @author James Wenzel (emo.eth) - * @author Ryan Ghods (ralxz.eth) - * @author Stephan Min (stephanm.eth) - * @author Michael Cohen (notmichael.eth) - * @notice ERC721SeaDropBurnable is a token contract that extends - * ERC721SeaDrop to additionally provide a burn function. - */ -contract ERC721SeaDropBurnable is ERC721SeaDrop { - /** - * @notice Deploy the token contract with its name, symbol, - * and allowed SeaDrop addresses. - */ - constructor(string memory name, string memory symbol, address[] memory allowedSeaDrop) - ERC721SeaDrop(name, symbol, allowedSeaDrop) - {} - - /** - * @notice Burns `tokenId`. The caller must own `tokenId` or be an - * approved operator. - * - * @param tokenId The token id to burn. - */ - // solhint-disable-next-line comprehensive-interface - function burn(uint256 tokenId) external { - _burn(tokenId, true); - } -} diff --git a/src/seadrop/extensions/ERC721SeaDropRandomOffset.sol b/src/seadrop/extensions/ERC721SeaDropRandomOffset.sol deleted file mode 100644 index 3eff13d..0000000 --- a/src/seadrop/extensions/ERC721SeaDropRandomOffset.sol +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {ERC721SeaDrop} from "../ERC721SeaDrop.sol"; - -/** - * @title ERC721SeaDropRandomOffset - * @author James Wenzel (emo.eth) - * @author Ryan Ghods (ralxz.eth) - * @author Stephan Min (stephanm.eth) - * @author Michael Cohen (notmichael.eth) - * @notice ERC721SeaDropRandomOffset is a token contract that extends - * ERC721SeaDrop to apply a randomOffset to the tokenURI, - * to enable fair metadata reveals. - */ -contract ERC721SeaDropRandomOffset is ERC721SeaDrop { - /// @notice The random offset, between 1 and the MAX_SUPPLY at the time of - /// being set. - uint256 public randomOffset; - - /// @notice If the collection has been revealed and the randomOffset has - /// been set. 1=False, 2=True. - uint256 public revealed = _REVEALED_FALSE; - - /// @dev For gas efficiency, uint is used instead of bool for revealed. - uint256 private constant _REVEALED_FALSE = 1; - uint256 private constant _REVEALED_TRUE = 2; - - /// @notice Revert when setting the randomOffset if already set. - error AlreadyRevealed(); - - /// @notice Revert when setting the randomOffset if the collection is - /// not yet fully minted. - error NotFullyMinted(); - - /** - * @notice Deploy the token contract with its name, symbol, - * and allowed SeaDrop addresses. - */ - constructor(string memory name, string memory symbol, address[] memory allowedSeaDrop) - ERC721SeaDrop(name, symbol, allowedSeaDrop) - {} - - /** - * @notice Set the random offset, for a fair metadata reveal. Only callable - * by the owner one time when the total number of minted tokens - * equals the max supply. Should be called immediately before - * reveal. - */ - // solhint-disable-next-line comprehensive-interface - function setRandomOffset() external onlyOwner { - // Revert setting the offset if already revealed. - if (revealed == _REVEALED_TRUE) { - revert AlreadyRevealed(); - } - - // Put maxSupply on the stack, since reading a state variable - // costs more gas than reading a local variable. - uint256 maxSupply = _maxSupply; - - // Revert if the collection is not yet fully minted. - if (_totalMinted() != maxSupply) { - revert NotFullyMinted(); - } - - // block.prevrandao returns PREVRANDAO on Ethereum post-merge - // NOTE: do not use this on other chains - // randomOffset returns between 1 and MAX_SUPPLY - randomOffset = (uint256(keccak256(abi.encode(block.prevrandao))) % (maxSupply - 1)) + 1; - - // Set revealed to true. - revealed = _REVEALED_TRUE; - } - - /** - * @notice The token URI, offset by randomOffset, to enable fair metadata - * reveals. - * - * @param tokenId The token id - */ - function tokenURI(uint256 tokenId) public view override returns (string memory) { - if (!_exists(tokenId)) { - revert URIQueryForNonexistentToken(); - } - - string memory base = _baseURI(); - if (bytes(base).length == 0) { - // If there is no baseURI set, return an empty string. - return ""; - } else if (revealed == _REVEALED_FALSE) { - // If the baseURI is set but the collection is not revealed yet, - // return just the baseURI. - return base; - } else { - // If the baseURI is set and the collection is revealed, - // return the tokenURI offset by the randomOffset. - return string.concat(base, _toString(((tokenId + randomOffset) % _maxSupply) + _startTokenId())); - } - } -} diff --git a/src/seadrop/extensions/ERC721SeaDropRedeemable.sol b/src/seadrop/extensions/ERC721SeaDropRedeemable.sol deleted file mode 100644 index 782fa20..0000000 --- a/src/seadrop/extensions/ERC721SeaDropRedeemable.sol +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {ERC721SeaDrop} from "../ERC721SeaDrop.sol"; - -import {OfferItem, ConsiderationItem} from "seaport-types/lib/ConsiderationStructs.sol"; - -interface IERC721RedemptionMintable { - enum ItemType { - NATIVE, - ERC20, - ERC721, - ERC1155, - ERC721_WITH_CRITERIA, - ERC1155_WITH_CRITERIA - } - - struct SpentItem { - ItemType itemType; - address token; - uint256 identifier; - uint256 amount; - } - - function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256 tokenId); -} - -struct CampaignParams { - uint32 startTime; - uint32 endTime; - uint32 maxCampaignRedemptions; - address manager; - address signer; - OfferItem[] offer; - ConsiderationItem[] consideration; -} - -/** - * @title ERC721SeaDropRedemptionMintable - * @author James Wenzel (emo.eth) - * @author Ryan Ghods (ralxz.eth) - * @author Stephan Min (stephanm.eth) - * @author Michael Cohen (notmichael.eth) - * @notice ERC721SeaDropRedemptionMintable is a token contract that extends - * ERC721SeaDrop to additionally add a mintRedemption function. - */ -contract ERC721SeaDropRedeemable is ERC721SeaDrop, IERC721RedemptionMintable { - address internal immutable _REDEEMABLE_CONTRACT_OFFERER; - address internal immutable _REDEEM_TOKEN; - - mapping(uint256 => uint256) public tokenURINumbers; - - /// @dev Revert if the sender of mintRedemption is not the redeemable contract offerer. - error InvalidSender(); - - /// @dev Revert if the redemption spent is not the required token. - error InvalidRedemption(); - - /** - * @notice Deploy the token contract with its name, symbol, - * and allowed SeaDrop addresses. - */ - constructor( - string memory name, - string memory symbol, - address[] memory allowedSeaDrop, - address redeemableContractOfferer, - address redeemToken - ) ERC721SeaDrop(name, symbol, allowedSeaDrop) { - _REDEEMABLE_CONTRACT_OFFERER = redeemableContractOfferer; - _REDEEM_TOKEN = redeemToken; - } - - /** - * @notice Only callable by the Redeemable Contract Offerer. - */ - function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256 tokenId) { - if (msg.sender != _REDEEMABLE_CONTRACT_OFFERER) revert InvalidSender(); - - SpentItem memory spentItem = spent[0]; - if (spentItem.token != _REDEEM_TOKEN) revert InvalidRedemption(); - - // Mint the same token ID redeemed. - _mint(to, 1); - - return _nextTokenId() - 1; - } - - /** - * @notice Hook to set tokenURINumber on mint. - */ - function _beforeTokenTransfers(address from, address, /* to */ uint256 startTokenId, uint256 quantity) - internal - virtual - override - { - // Set tokenURINumbers on mint. - if (from == address(0)) { - for (uint256 i = 0; i < quantity; i++) { - // 60% chance of tokenURI 1 - // 30% chance of tokenURI 2 - // 10% chance of tokenURI 3 - - // block.prevrandao returns PREVRANDAO on Ethereum post-merge - // NOTE: do not use this on other chains - uint256 randomness = (uint256(keccak256(abi.encode(block.prevrandao))) % 100) + 1; - - uint256 tokenURINumber = 1; - if (randomness >= 60 && randomness < 90) { - tokenURINumber = 2; - } else if (randomness >= 90) { - tokenURINumber = 3; - } - - tokenURINumbers[startTokenId + i] = tokenURINumber; - } - } - } - - /* - * @notice Overrides the `tokenURI()` function to return baseURI + 1, 2, or 3 - */ - function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { - if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); - - string memory baseURI = _baseURI(); - uint256 tokenURINumber = tokenURINumbers[tokenId]; - - return string(abi.encodePacked(baseURI, _toString(tokenURINumber))); - } -} diff --git a/src/seadrop/extensions/ERC721SeaDropRedemptionMintable.sol b/src/seadrop/extensions/ERC721SeaDropRedemptionMintable.sol deleted file mode 100644 index 2aacff0..0000000 --- a/src/seadrop/extensions/ERC721SeaDropRedemptionMintable.sol +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {ERC721SeaDrop} from "../ERC721SeaDrop.sol"; - -interface IERC721RedemptionMintable { - enum ItemType { - NATIVE, - ERC20, - ERC721, - ERC1155, - ERC721_WITH_CRITERIA, - ERC1155_WITH_CRITERIA - } - - struct SpentItem { - ItemType itemType; - address token; - uint256 identifier; - uint256 amount; - } - - function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256 tokenId); -} - -/** - * @title ERC721SeaDropRedemptionMintable - * @author James Wenzel (emo.eth) - * @author Ryan Ghods (ralxz.eth) - * @author Stephan Min (stephanm.eth) - * @author Michael Cohen (notmichael.eth) - * @notice ERC721SeaDropRedemptionMintable is a token contract that extends - * ERC721SeaDrop to additionally add a mintRedemption function. - */ -contract ERC721SeaDropRedemptionMintable is ERC721SeaDrop, IERC721RedemptionMintable { - address internal immutable _REDEEMABLE_CONTRACT_OFFERER; - address internal immutable _REDEEM_TOKEN; - - mapping(uint256 => uint256) public tokenURINumbers; - - /// @dev Revert if the sender of mintRedemption is not the redeemable contract offerer. - error InvalidSender(); - - /// @dev Revert if the redemption spent is not the required token. - error InvalidRedemption(); - - /** - * @notice Deploy the token contract with its name, symbol, - * and allowed SeaDrop addresses. - */ - constructor( - string memory name, - string memory symbol, - address[] memory allowedSeaDrop, - address redeemableContractOfferer, - address redeemToken - ) ERC721SeaDrop(name, symbol, allowedSeaDrop) { - _REDEEMABLE_CONTRACT_OFFERER = redeemableContractOfferer; - _REDEEM_TOKEN = redeemToken; - } - - /** - * @notice Only callable by the Redeemable Contract Offerer. - */ - function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256 tokenId) { - if (msg.sender != _REDEEMABLE_CONTRACT_OFFERER) revert InvalidSender(); - - SpentItem memory spentItem = spent[0]; - if (spentItem.token != _REDEEM_TOKEN) revert InvalidRedemption(); - - // Mint the same token ID redeemed. - _mint(to, 1); - - return _nextTokenId() - 1; - } - - /** - * @notice Hook to set tokenURINumber on mint. - */ - function _beforeTokenTransfers(address from, address, /* to */ uint256 startTokenId, uint256 quantity) - internal - virtual - override - { - // Set tokenURINumbers on mint. - if (from == address(0)) { - for (uint256 i = 0; i < quantity; i++) { - // 60% chance of tokenURI 1 - // 30% chance of tokenURI 2 - // 10% chance of tokenURI 3 - - // block.prevrandao returns PREVRANDAO on Ethereum post-merge - // NOTE: do not use this on other chains - uint256 randomness = (uint256(keccak256(abi.encode(block.prevrandao))) % 100) + 1; - - uint256 tokenURINumber = 1; - if (randomness >= 60 && randomness < 90) { - tokenURINumber = 2; - } else if (randomness >= 90) { - tokenURINumber = 3; - } - - tokenURINumbers[startTokenId + i] = tokenURINumber; - } - } - } - - /* - * @notice Overrides the `tokenURI()` function to return baseURI + 1, 2, or 3 - */ - function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { - if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); - - string memory baseURI = _baseURI(); - uint256 tokenURINumber = tokenURINumbers[tokenId]; - - return string(abi.encodePacked(baseURI, _toString(tokenURINumber))); - } -} diff --git a/src/seadrop/interfaces/INonFungibleSeaDropToken.sol b/src/seadrop/interfaces/INonFungibleSeaDropToken.sol deleted file mode 100644 index a5f8c70..0000000 --- a/src/seadrop/interfaces/INonFungibleSeaDropToken.sol +++ /dev/null @@ -1,160 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {ISeaDropTokenContractMetadata} from "./ISeaDropTokenContractMetadata.sol"; - -import {AllowListData, PublicDrop, TokenGatedDropStage, SignedMintValidationParams} from "../lib/SeaDropStructs.sol"; - -interface INonFungibleSeaDropToken is ISeaDropTokenContractMetadata { - /** - * @dev Revert with an error if a contract is not an allowed - * SeaDrop address. - */ - error OnlyAllowedSeaDrop(); - - /** - * @dev Emit an event when allowed SeaDrop contracts are updated. - */ - event AllowedSeaDropUpdated(address[] allowedSeaDrop); - - /** - * @notice Update the allowed SeaDrop contracts. - * Only the owner or administrator can use this function. - * - * @param allowedSeaDrop The allowed SeaDrop addresses. - */ - function updateAllowedSeaDrop(address[] calldata allowedSeaDrop) external; - - /** - * @notice Mint tokens, restricted to the SeaDrop contract. - * - * @dev NOTE: If a token registers itself with multiple SeaDrop - * contracts, the implementation of this function should guard - * against reentrancy. If the implementing token uses - * _safeMint(), or a feeRecipient with a malicious receive() hook - * is specified, the token or fee recipients may be able to execute - * another mint in the same transaction via a separate SeaDrop - * contract. - * This is dangerous if an implementing token does not correctly - * update the minterNumMinted and currentTotalSupply values before - * transferring minted tokens, as SeaDrop references these values - * to enforce token limits on a per-wallet and per-stage basis. - * - * @param minter The address to mint to. - * @param quantity The number of tokens to mint. - */ - function mintSeaDrop(address minter, uint256 quantity) external; - - /** - * @notice Returns a set of mint stats for the address. - * This assists SeaDrop in enforcing maxSupply, - * maxTotalMintableByWallet, and maxTokenSupplyForStage checks. - * - * @dev NOTE: Implementing contracts should always update these numbers - * before transferring any tokens with _safeMint() to mitigate - * consequences of malicious onERC721Received() hooks. - * - * @param minter The minter address. - */ - function getMintStats(address minter) - external - view - returns (uint256 minterNumMinted, uint256 currentTotalSupply, uint256 maxSupply); - - /** - * @notice Update the public drop data for this nft contract on SeaDrop. - * Only the owner or administrator can use this function. - * - * The administrator can only update `feeBps`. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param publicDrop The public drop data. - */ - function updatePublicDrop(address seaDropImpl, PublicDrop calldata publicDrop) external; - - /** - * @notice Update the allow list data for this nft contract on SeaDrop. - * Only the owner or administrator can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param allowListData The allow list data. - */ - function updateAllowList(address seaDropImpl, AllowListData calldata allowListData) external; - - /** - * @notice Update the token gated drop stage data for this nft contract - * on SeaDrop. - * Only the owner or administrator can use this function. - * - * The administrator, when present, must first set `feeBps`. - * - * Note: If two INonFungibleSeaDropToken tokens are doing - * simultaneous token gated drop promotions for each other, - * they can be minted by the same actor until - * `maxTokenSupplyForStage` is reached. Please ensure the - * `allowedNftToken` is not running an active drop during the - * `dropStage` time period. - * - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param allowedNftToken The allowed nft token. - * @param dropStage The token gated drop stage data. - */ - function updateTokenGatedDrop(address seaDropImpl, address allowedNftToken, TokenGatedDropStage calldata dropStage) - external; - - /** - * @notice Update the drop URI for this nft contract on SeaDrop. - * Only the owner or administrator can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param dropURI The new drop URI. - */ - function updateDropURI(address seaDropImpl, string calldata dropURI) external; - - /** - * @notice Update the creator payout address for this nft contract on - * SeaDrop. - * Only the owner can set the creator payout address. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param payoutAddress The new payout address. - */ - function updateCreatorPayoutAddress(address seaDropImpl, address payoutAddress) external; - - /** - * @notice Update the allowed fee recipient for this nft contract - * on SeaDrop. - * Only the administrator can set the allowed fee recipient. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param feeRecipient The new fee recipient. - */ - function updateAllowedFeeRecipient(address seaDropImpl, address feeRecipient, bool allowed) external; - - /** - * @notice Update the server-side signers for this nft contract - * on SeaDrop. - * Only the owner or administrator can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param signer The signer to update. - * @param signedMintValidationParams Minimum and maximum parameters - * to enforce for signed mints. - */ - function updateSignedMintValidationParams( - address seaDropImpl, - address signer, - SignedMintValidationParams memory signedMintValidationParams - ) external; - - /** - * @notice Update the allowed payers for this nft contract on SeaDrop. - * Only the owner or administrator can use this function. - * - * @param seaDropImpl The allowed SeaDrop contract. - * @param payer The payer to update. - * @param allowed Whether the payer is allowed. - */ - function updatePayer(address seaDropImpl, address payer, bool allowed) external; -} diff --git a/src/seadrop/interfaces/ISeaDrop.sol b/src/seadrop/interfaces/ISeaDrop.sol deleted file mode 100644 index 8e2e7c6..0000000 --- a/src/seadrop/interfaces/ISeaDrop.sol +++ /dev/null @@ -1,312 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import { - AllowListData, - MintParams, - PublicDrop, - TokenGatedDropStage, - TokenGatedMintParams, - SignedMintValidationParams -} from "../lib/SeaDropStructs.sol"; - -import {SeaDropErrorsAndEvents} from "../lib/SeaDropErrorsAndEvents.sol"; - -interface ISeaDrop is SeaDropErrorsAndEvents { - /** - * @notice Mint a public drop. - * - * @param nftContract The nft contract to mint. - * @param feeRecipient The fee recipient. - * @param minterIfNotPayer The mint recipient if different than the payer. - * @param quantity The number of tokens to mint. - */ - function mintPublic(address nftContract, address feeRecipient, address minterIfNotPayer, uint256 quantity) - external - payable; - - /** - * @notice Mint from an allow list. - * - * @param nftContract The nft contract to mint. - * @param feeRecipient The fee recipient. - * @param minterIfNotPayer The mint recipient if different than the payer. - * @param quantity The number of tokens to mint. - * @param mintParams The mint parameters. - * @param proof The proof for the leaf of the allow list. - */ - function mintAllowList( - address nftContract, - address feeRecipient, - address minterIfNotPayer, - uint256 quantity, - MintParams calldata mintParams, - bytes32[] calldata proof - ) external payable; - - /** - * @notice Mint with a server-side signature. - * Note that a signature can only be used once. - * - * @param nftContract The nft contract to mint. - * @param feeRecipient The fee recipient. - * @param minterIfNotPayer The mint recipient if different than the payer. - * @param quantity The number of tokens to mint. - * @param mintParams The mint parameters. - * @param salt The sale for the signed mint. - * @param signature The server-side signature, must be an allowed - * signer. - */ - function mintSigned( - address nftContract, - address feeRecipient, - address minterIfNotPayer, - uint256 quantity, - MintParams calldata mintParams, - uint256 salt, - bytes calldata signature - ) external payable; - - /** - * @notice Mint as an allowed token holder. - * This will mark the token id as redeemed and will revert if the - * same token id is attempted to be redeemed twice. - * - * @param nftContract The nft contract to mint. - * @param feeRecipient The fee recipient. - * @param minterIfNotPayer The mint recipient if different than the payer. - * @param mintParams The token gated mint params. - */ - function mintAllowedTokenHolder( - address nftContract, - address feeRecipient, - address minterIfNotPayer, - TokenGatedMintParams calldata mintParams - ) external payable; - - /** - * @notice Emits an event to notify update of the drop URI. - * - * This method assume msg.sender is an nft contract and its - * ERC165 interface id matches INonFungibleSeaDropToken. - * - * Note: Be sure only authorized users can call this from - * token contracts that implement INonFungibleSeaDropToken. - * - * @param dropURI The new drop URI. - */ - function updateDropURI(string calldata dropURI) external; - - /** - * @notice Updates the public drop data for the nft contract - * and emits an event. - * - * This method assume msg.sender is an nft contract and its - * ERC165 interface id matches INonFungibleSeaDropToken. - * - * Note: Be sure only authorized users can call this from - * token contracts that implement INonFungibleSeaDropToken. - * - * @param publicDrop The public drop data. - */ - function updatePublicDrop(PublicDrop calldata publicDrop) external; - - /** - * @notice Updates the allow list merkle root for the nft contract - * and emits an event. - * - * This method assume msg.sender is an nft contract and its - * ERC165 interface id matches INonFungibleSeaDropToken. - * - * Note: Be sure only authorized users can call this from - * token contracts that implement INonFungibleSeaDropToken. - * - * @param allowListData The allow list data. - */ - function updateAllowList(AllowListData calldata allowListData) external; - - /** - * @notice Updates the token gated drop stage for the nft contract - * and emits an event. - * - * This method assume msg.sender is an nft contract and its - * ERC165 interface id matches INonFungibleSeaDropToken. - * - * Note: Be sure only authorized users can call this from - * token contracts that implement INonFungibleSeaDropToken. - * - * Note: If two INonFungibleSeaDropToken tokens are doing - * simultaneous token gated drop promotions for each other, - * they can be minted by the same actor until - * `maxTokenSupplyForStage` is reached. Please ensure the - * `allowedNftToken` is not running an active drop during - * the `dropStage` time period. - * - * @param allowedNftToken The token gated nft token. - * @param dropStage The token gated drop stage data. - */ - function updateTokenGatedDrop(address allowedNftToken, TokenGatedDropStage calldata dropStage) external; - - /** - * @notice Updates the creator payout address and emits an event. - * - * This method assume msg.sender is an nft contract and its - * ERC165 interface id matches INonFungibleSeaDropToken. - * - * Note: Be sure only authorized users can call this from - * token contracts that implement INonFungibleSeaDropToken. - * - * @param payoutAddress The creator payout address. - */ - function updateCreatorPayoutAddress(address payoutAddress) external; - - /** - * @notice Updates the allowed fee recipient and emits an event. - * - * This method assume msg.sender is an nft contract and its - * ERC165 interface id matches INonFungibleSeaDropToken. - * - * Note: Be sure only authorized users can call this from - * token contracts that implement INonFungibleSeaDropToken. - * - * @param feeRecipient The fee recipient. - * @param allowed If the fee recipient is allowed. - */ - function updateAllowedFeeRecipient(address feeRecipient, bool allowed) external; - - /** - * @notice Updates the allowed server-side signers and emits an event. - * - * This method assume msg.sender is an nft contract and its - * ERC165 interface id matches INonFungibleSeaDropToken. - * - * Note: Be sure only authorized users can call this from - * token contracts that implement INonFungibleSeaDropToken. - * - * @param signer The signer to update. - * @param signedMintValidationParams Minimum and maximum parameters - * to enforce for signed mints. - */ - function updateSignedMintValidationParams( - address signer, - SignedMintValidationParams calldata signedMintValidationParams - ) external; - - /** - * @notice Updates the allowed payer and emits an event. - * - * This method assume msg.sender is an nft contract and its - * ERC165 interface id matches INonFungibleSeaDropToken. - * - * Note: Be sure only authorized users can call this from - * token contracts that implement INonFungibleSeaDropToken. - * - * @param payer The payer to add or remove. - * @param allowed Whether to add or remove the payer. - */ - function updatePayer(address payer, bool allowed) external; - - /** - * @notice Returns the public drop data for the nft contract. - * - * @param nftContract The nft contract. - */ - function getPublicDrop(address nftContract) external view returns (PublicDrop memory); - - /** - * @notice Returns the creator payout address for the nft contract. - * - * @param nftContract The nft contract. - */ - function getCreatorPayoutAddress(address nftContract) external view returns (address); - - /** - * @notice Returns the allow list merkle root for the nft contract. - * - * @param nftContract The nft contract. - */ - function getAllowListMerkleRoot(address nftContract) external view returns (bytes32); - - /** - * @notice Returns if the specified fee recipient is allowed - * for the nft contract. - * - * @param nftContract The nft contract. - * @param feeRecipient The fee recipient. - */ - function getFeeRecipientIsAllowed(address nftContract, address feeRecipient) external view returns (bool); - - /** - * @notice Returns an enumeration of allowed fee recipients for an - * nft contract when fee recipients are enforced - * - * @param nftContract The nft contract. - */ - function getAllowedFeeRecipients(address nftContract) external view returns (address[] memory); - - /** - * @notice Returns the server-side signers for the nft contract. - * - * @param nftContract The nft contract. - */ - function getSigners(address nftContract) external view returns (address[] memory); - - /** - * @notice Returns the struct of SignedMintValidationParams for a signer. - * - * @param nftContract The nft contract. - * @param signer The signer. - */ - function getSignedMintValidationParams(address nftContract, address signer) - external - view - returns (SignedMintValidationParams memory); - - /** - * @notice Returns the payers for the nft contract. - * - * @param nftContract The nft contract. - */ - function getPayers(address nftContract) external view returns (address[] memory); - - /** - * @notice Returns if the specified payer is allowed - * for the nft contract. - * - * @param nftContract The nft contract. - * @param payer The payer. - */ - function getPayerIsAllowed(address nftContract, address payer) external view returns (bool); - - /** - * @notice Returns the allowed token gated drop tokens for the nft contract. - * - * @param nftContract The nft contract. - */ - function getTokenGatedAllowedTokens(address nftContract) external view returns (address[] memory); - - /** - * @notice Returns the token gated drop data for the nft contract - * and token gated nft. - * - * @param nftContract The nft contract. - * @param allowedNftToken The token gated nft token. - */ - function getTokenGatedDrop(address nftContract, address allowedNftToken) - external - view - returns (TokenGatedDropStage memory); - - /** - * @notice Returns whether the token id for a token gated drop has been - * redeemed. - * - * @param nftContract The nft contract. - * @param allowedNftToken The token gated nft token. - * @param allowedNftTokenId The token gated nft token id to check. - */ - function getAllowedNftTokenIdIsRedeemed(address nftContract, address allowedNftToken, uint256 allowedNftTokenId) - external - view - returns (bool); -} diff --git a/src/seadrop/interfaces/ISeaDropTokenContractMetadata.sol b/src/seadrop/interfaces/ISeaDropTokenContractMetadata.sol deleted file mode 100644 index ce7faa0..0000000 --- a/src/seadrop/interfaces/ISeaDropTokenContractMetadata.sol +++ /dev/null @@ -1,141 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {IERC2981} from "openzeppelin-contracts/interfaces/IERC2981.sol"; - -interface ISeaDropTokenContractMetadata is IERC2981 { - /** - * @notice Throw if the max supply exceeds uint64, a limit - * due to the storage of bit-packed variables in ERC721A. - */ - error CannotExceedMaxSupplyOfUint64(uint256 newMaxSupply); - - /** - * @dev Revert with an error when attempting to set the provenance - * hash after the mint has started. - */ - error ProvenanceHashCannotBeSetAfterMintStarted(); - - /** - * @dev Revert if the royalty basis points is greater than 10_000. - */ - error InvalidRoyaltyBasisPoints(uint256 basisPoints); - - /** - * @dev Revert if the royalty address is being set to the zero address. - */ - error RoyaltyAddressCannotBeZeroAddress(); - - /** - * @dev Emit an event for token metadata reveals/updates, - * according to EIP-4906. - * - * @param _fromTokenId The start token id. - * @param _toTokenId The end token id. - */ - event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); - - /** - * @dev Emit an event when the URI for the collection-level metadata - * is updated. - */ - event ContractURIUpdated(string newContractURI); - - /** - * @dev Emit an event when the max token supply is updated. - */ - event MaxSupplyUpdated(uint256 newMaxSupply); - - /** - * @dev Emit an event with the previous and new provenance hash after - * being updated. - */ - event ProvenanceHashUpdated(bytes32 previousHash, bytes32 newHash); - - /** - * @dev Emit an event when the royalties info is updated. - */ - event RoyaltyInfoUpdated(address receiver, uint256 bps); - - /** - * @notice A struct defining royalty info for the contract. - */ - struct RoyaltyInfo { - address royaltyAddress; - uint96 royaltyBps; - } - - /** - * @notice Sets the base URI for the token metadata and emits an event. - * - * @param tokenURI The new base URI to set. - */ - function setBaseURI(string calldata tokenURI) external; - - /** - * @notice Sets the contract URI for contract metadata. - * - * @param newContractURI The new contract URI. - */ - function setContractURI(string calldata newContractURI) external; - - /** - * @notice Sets the max supply and emits an event. - * - * @param newMaxSupply The new max supply to set. - */ - function setMaxSupply(uint256 newMaxSupply) external; - - /** - * @notice Sets the provenance hash and emits an event. - * - * The provenance hash is used for random reveals, which - * is a hash of the ordered metadata to show it has not been - * modified after mint started. - * - * This function will revert after the first item has been minted. - * - * @param newProvenanceHash The new provenance hash to set. - */ - function setProvenanceHash(bytes32 newProvenanceHash) external; - - /** - * @notice Sets the address and basis points for royalties. - * - * @param newInfo The struct to configure royalties. - */ - function setRoyaltyInfo(RoyaltyInfo calldata newInfo) external; - - /** - * @notice Returns the base URI for token metadata. - */ - function baseURI() external view returns (string memory); - - /** - * @notice Returns the contract URI. - */ - function contractURI() external view returns (string memory); - - /** - * @notice Returns the max token supply. - */ - function maxSupply() external view returns (uint256); - - /** - * @notice Returns the provenance hash. - * The provenance hash is used for random reveals, which - * is a hash of the ordered metadata to show it is unmodified - * after mint has started. - */ - function provenanceHash() external view returns (bytes32); - - /** - * @notice Returns the address that receives royalties. - */ - function royaltyAddress() external view returns (address); - - /** - * @notice Returns the royalty basis points out of 10_000. - */ - function royaltyBasisPoints() external view returns (uint256); -} diff --git a/src/seadrop/lib/ERC721SeaDropStructsErrorsAndEvents.sol b/src/seadrop/lib/ERC721SeaDropStructsErrorsAndEvents.sol deleted file mode 100644 index 4b63654..0000000 --- a/src/seadrop/lib/ERC721SeaDropStructsErrorsAndEvents.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {AllowListData, PublicDrop, SignedMintValidationParams, TokenGatedDropStage} from "./SeaDropStructs.sol"; - -interface ERC721SeaDropStructsErrorsAndEvents { - /** - * @notice Revert with an error if mint exceeds the max supply. - */ - error MintQuantityExceedsMaxSupply(uint256 total, uint256 maxSupply); - - /** - * @notice Revert with an error if the number of token gated - * allowedNftTokens doesn't match the length of supplied - * drop stages. - */ - error TokenGatedMismatch(); - - /** - * @notice Revert with an error if the number of signers doesn't match - * the length of supplied signedMintValidationParams - */ - error SignersMismatch(); - - /** - * @notice An event to signify that a SeaDrop token contract was deployed. - */ - event SeaDropTokenDeployed(); - - /** - * @notice A struct to configure multiple contract options at a time. - */ - struct MultiConfigureStruct { - uint256 maxSupply; - string baseURI; - string contractURI; - address seaDropImpl; - PublicDrop publicDrop; - string dropURI; - AllowListData allowListData; - address creatorPayoutAddress; - bytes32 provenanceHash; - address[] allowedFeeRecipients; - address[] disallowedFeeRecipients; - address[] allowedPayers; - address[] disallowedPayers; - // Token-gated - address[] tokenGatedAllowedNftTokens; - TokenGatedDropStage[] tokenGatedDropStages; - address[] disallowedTokenGatedAllowedNftTokens; - // Server-signed - address[] signers; - SignedMintValidationParams[] signedMintValidationParams; - address[] disallowedSigners; - } -} diff --git a/src/seadrop/lib/SeaDropErrorsAndEvents.sol b/src/seadrop/lib/SeaDropErrorsAndEvents.sol deleted file mode 100644 index 2fe2375..0000000 --- a/src/seadrop/lib/SeaDropErrorsAndEvents.sol +++ /dev/null @@ -1,288 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {PublicDrop, TokenGatedDropStage, SignedMintValidationParams} from "./SeaDropStructs.sol"; - -interface SeaDropErrorsAndEvents { - /** - * @dev Revert with an error if the drop stage is not active. - */ - error NotActive(uint256 currentTimestamp, uint256 startTimestamp, uint256 endTimestamp); - - /** - * @dev Revert with an error if the mint quantity is zero. - */ - error MintQuantityCannotBeZero(); - - /** - * @dev Revert with an error if the mint quantity exceeds the max allowed - * to be minted per wallet. - */ - error MintQuantityExceedsMaxMintedPerWallet(uint256 total, uint256 allowed); - - /** - * @dev Revert with an error if the mint quantity exceeds the max token - * supply. - */ - error MintQuantityExceedsMaxSupply(uint256 total, uint256 maxSupply); - - /** - * @dev Revert with an error if the mint quantity exceeds the max token - * supply for the stage. - * Note: The `maxTokenSupplyForStage` for public mint is - * always `type(uint).max`. - */ - error MintQuantityExceedsMaxTokenSupplyForStage(uint256 total, uint256 maxTokenSupplyForStage); - - /** - * @dev Revert if the fee recipient is the zero address. - */ - error FeeRecipientCannotBeZeroAddress(); - - /** - * @dev Revert if the fee recipient is not already included. - */ - error FeeRecipientNotPresent(); - - /** - * @dev Revert if the fee basis points is greater than 10_000. - */ - error InvalidFeeBps(uint256 feeBps); - - /** - * @dev Revert if the fee recipient is already included. - */ - error DuplicateFeeRecipient(); - - /** - * @dev Revert if the fee recipient is restricted and not allowed. - */ - error FeeRecipientNotAllowed(); - - /** - * @dev Revert if the creator payout address is the zero address. - */ - error CreatorPayoutAddressCannotBeZeroAddress(); - - /** - * @dev Revert with an error if the received payment is incorrect. - */ - error IncorrectPayment(uint256 got, uint256 want); - - /** - * @dev Revert with an error if the allow list proof is invalid. - */ - error InvalidProof(); - - /** - * @dev Revert if a supplied signer address is the zero address. - */ - error SignerCannotBeZeroAddress(); - - /** - * @dev Revert with an error if signer's signature is invalid. - */ - error InvalidSignature(address recoveredSigner); - - /** - * @dev Revert with an error if a signer is not included in - * the enumeration when removing. - */ - error SignerNotPresent(); - - /** - * @dev Revert with an error if a payer is not included in - * the enumeration when removing. - */ - error PayerNotPresent(); - - /** - * @dev Revert with an error if a payer is already included in mapping - * when adding. - * Note: only applies when adding a single payer, as duplicates in - * enumeration can be removed with updatePayer. - */ - error DuplicatePayer(); - - /** - * @dev Revert with an error if the payer is not allowed. The minter must - * pay for their own mint. - */ - error PayerNotAllowed(); - - /** - * @dev Revert if a supplied payer address is the zero address. - */ - error PayerCannotBeZeroAddress(); - - /** - * @dev Revert with an error if the sender does not - * match the INonFungibleSeaDropToken interface. - */ - error OnlyINonFungibleSeaDropToken(address sender); - - /** - * @dev Revert with an error if the sender of a token gated supplied - * drop stage redeem is not the owner of the token. - */ - error TokenGatedNotTokenOwner(address nftContract, address allowedNftToken, uint256 allowedNftTokenId); - - /** - * @dev Revert with an error if the token id has already been used to - * redeem a token gated drop stage. - */ - error TokenGatedTokenIdAlreadyRedeemed(address nftContract, address allowedNftToken, uint256 allowedNftTokenId); - - /** - * @dev Revert with an error if an empty TokenGatedDropStage is provided - * for an already-empty TokenGatedDropStage. - */ - error TokenGatedDropStageNotPresent(); - - /** - * @dev Revert with an error if an allowedNftToken is set to - * the zero address. - */ - error TokenGatedDropAllowedNftTokenCannotBeZeroAddress(); - - /** - * @dev Revert with an error if an allowedNftToken is set to - * the drop token itself. - */ - error TokenGatedDropAllowedNftTokenCannotBeDropToken(); - - /** - * @dev Revert with an error if supplied signed mint price is less than - * the minimum specified. - */ - error InvalidSignedMintPrice(uint256 got, uint256 minimum); - - /** - * @dev Revert with an error if supplied signed maxTotalMintableByWallet - * is greater than the maximum specified. - */ - error InvalidSignedMaxTotalMintableByWallet(uint256 got, uint256 maximum); - - /** - * @dev Revert with an error if supplied signed start time is less than - * the minimum specified. - */ - error InvalidSignedStartTime(uint256 got, uint256 minimum); - - /** - * @dev Revert with an error if supplied signed end time is greater than - * the maximum specified. - */ - error InvalidSignedEndTime(uint256 got, uint256 maximum); - - /** - * @dev Revert with an error if supplied signed maxTokenSupplyForStage - * is greater than the maximum specified. - */ - error InvalidSignedMaxTokenSupplyForStage(uint256 got, uint256 maximum); - - /** - * @dev Revert with an error if supplied signed feeBps is greater than - * the maximum specified, or less than the minimum. - */ - error InvalidSignedFeeBps(uint256 got, uint256 minimumOrMaximum); - - /** - * @dev Revert with an error if signed mint did not specify to restrict - * fee recipients. - */ - error SignedMintsMustRestrictFeeRecipients(); - - /** - * @dev Revert with an error if a signature for a signed mint has already - * been used. - */ - error SignatureAlreadyUsed(); - - /** - * @dev An event with details of a SeaDrop mint, for analytical purposes. - * - * @param nftContract The nft contract. - * @param minter The mint recipient. - * @param feeRecipient The fee recipient. - * @param payer The address who payed for the tx. - * @param quantityMinted The number of tokens minted. - * @param unitMintPrice The amount paid for each token. - * @param feeBps The fee out of 10_000 basis points collected. - * @param dropStageIndex The drop stage index. Items minted - * through mintPublic() have - * dropStageIndex of 0. - */ - event SeaDropMint( - address indexed nftContract, - address indexed minter, - address indexed feeRecipient, - address payer, - uint256 quantityMinted, - uint256 unitMintPrice, - uint256 feeBps, - uint256 dropStageIndex - ); - - /** - * @dev An event with updated public drop data for an nft contract. - */ - event PublicDropUpdated(address indexed nftContract, PublicDrop publicDrop); - - /** - * @dev An event with updated token gated drop stage data - * for an nft contract. - */ - event TokenGatedDropStageUpdated( - address indexed nftContract, address indexed allowedNftToken, TokenGatedDropStage dropStage - ); - - /** - * @dev An event with updated allow list data for an nft contract. - * - * @param nftContract The nft contract. - * @param previousMerkleRoot The previous allow list merkle root. - * @param newMerkleRoot The new allow list merkle root. - * @param publicKeyURI If the allow list is encrypted, the public key - * URIs that can decrypt the list. - * Empty if unencrypted. - * @param allowListURI The URI for the allow list. - */ - event AllowListUpdated( - address indexed nftContract, - bytes32 indexed previousMerkleRoot, - bytes32 indexed newMerkleRoot, - string[] publicKeyURI, - string allowListURI - ); - - /** - * @dev An event with updated drop URI for an nft contract. - */ - event DropURIUpdated(address indexed nftContract, string newDropURI); - - /** - * @dev An event with the updated creator payout address for an nft - * contract. - */ - event CreatorPayoutAddressUpdated(address indexed nftContract, address indexed newPayoutAddress); - - /** - * @dev An event with the updated allowed fee recipient for an nft - * contract. - */ - event AllowedFeeRecipientUpdated(address indexed nftContract, address indexed feeRecipient, bool indexed allowed); - - /** - * @dev An event with the updated validation parameters for server-side - * signers. - */ - event SignedMintValidationParamsUpdated( - address indexed nftContract, address indexed signer, SignedMintValidationParams signedMintValidationParams - ); - - /** - * @dev An event with the updated payer for an nft contract. - */ - event PayerUpdated(address indexed nftContract, address indexed payer, bool indexed allowed); -} diff --git a/src/seadrop/lib/SeaDropStructs.sol b/src/seadrop/lib/SeaDropStructs.sol deleted file mode 100644 index 88b3f7d..0000000 --- a/src/seadrop/lib/SeaDropStructs.sol +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -/** - * @notice A struct defining public drop data. - * Designed to fit efficiently in one storage slot. - * - * @param mintPrice The mint price per token. (Up to 1.2m - * of native token, e.g. ETH, MATIC) - * @param startTime The start time, ensure this is not zero. - * @param endTIme The end time, ensure this is not zero. - * @param maxTotalMintableByWallet Maximum total number of mints a user is - * allowed. (The limit for this field is - * 2^16 - 1) - * @param feeBps Fee out of 10_000 basis points to be - * collected. - * @param restrictFeeRecipients If false, allow any fee recipient; - * if true, check fee recipient is allowed. - */ -struct PublicDrop { - uint80 mintPrice; // 80/256 bits - uint48 startTime; // 128/256 bits - uint48 endTime; // 176/256 bits - uint16 maxTotalMintableByWallet; // 224/256 bits - uint16 feeBps; // 240/256 bits - bool restrictFeeRecipients; // 248/256 bits -} - -/** - * @notice A struct defining token gated drop stage data. - * Designed to fit efficiently in one storage slot. - * - * @param mintPrice The mint price per token. (Up to 1.2m - * of native token, e.g.: ETH, MATIC) - * @param maxTotalMintableByWallet Maximum total number of mints a user is - * allowed. (The limit for this field is - * 2^16 - 1) - * @param startTime The start time, ensure this is not zero. - * @param endTime The end time, ensure this is not zero. - * @param dropStageIndex The drop stage index to emit with the event - * for analytical purposes. This should be - * non-zero since the public mint emits - * with index zero. - * @param maxTokenSupplyForStage The limit of token supply this stage can - * mint within. (The limit for this field is - * 2^16 - 1) - * @param feeBps Fee out of 10_000 basis points to be - * collected. - * @param restrictFeeRecipients If false, allow any fee recipient; - * if true, check fee recipient is allowed. - */ -struct TokenGatedDropStage { - uint80 mintPrice; // 80/256 bits - uint16 maxTotalMintableByWallet; // 96/256 bits - uint48 startTime; // 144/256 bits - uint48 endTime; // 192/256 bits - uint8 dropStageIndex; // non-zero. 200/256 bits - uint32 maxTokenSupplyForStage; // 232/256 bits - uint16 feeBps; // 248/256 bits - bool restrictFeeRecipients; // 256/256 bits -} - -/** - * @notice A struct defining mint params for an allow list. - * An allow list leaf will be composed of `msg.sender` and - * the following params. - * - * Note: Since feeBps is encoded in the leaf, backend should ensure - * that feeBps is acceptable before generating a proof. - * - * @param mintPrice The mint price per token. - * @param maxTotalMintableByWallet Maximum total number of mints a user is - * allowed. - * @param startTime The start time, ensure this is not zero. - * @param endTime The end time, ensure this is not zero. - * @param dropStageIndex The drop stage index to emit with the event - * for analytical purposes. This should be - * non-zero since the public mint emits with - * index zero. - * @param maxTokenSupplyForStage The limit of token supply this stage can - * mint within. - * @param feeBps Fee out of 10_000 basis points to be - * collected. - * @param restrictFeeRecipients If false, allow any fee recipient; - * if true, check fee recipient is allowed. - */ -struct MintParams { - uint256 mintPrice; - uint256 maxTotalMintableByWallet; - uint256 startTime; - uint256 endTime; - uint256 dropStageIndex; // non-zero - uint256 maxTokenSupplyForStage; - uint256 feeBps; - bool restrictFeeRecipients; -} - -/** - * @notice A struct defining token gated mint params. - * - * @param allowedNftToken The allowed nft token contract address. - * @param allowedNftTokenIds The token ids to redeem. - */ -struct TokenGatedMintParams { - address allowedNftToken; - uint256[] allowedNftTokenIds; -} - -/** - * @notice A struct defining allow list data (for minting an allow list). - * - * @param merkleRoot The merkle root for the allow list. - * @param publicKeyURIs If the allowListURI is encrypted, a list of URIs - * pointing to the public keys. Empty if unencrypted. - * @param allowListURI The URI for the allow list. - */ -struct AllowListData { - bytes32 merkleRoot; - string[] publicKeyURIs; - string allowListURI; -} - -/** - * @notice A struct defining minimum and maximum parameters to validate for - * signed mints, to minimize negative effects of a compromised signer. - * - * @param minMintPrice The minimum mint price allowed. - * @param maxMaxTotalMintableByWallet The maximum total number of mints allowed - * by a wallet. - * @param minStartTime The minimum start time allowed. - * @param maxEndTime The maximum end time allowed. - * @param maxMaxTokenSupplyForStage The maximum token supply allowed. - * @param minFeeBps The minimum fee allowed. - * @param maxFeeBps The maximum fee allowed. - */ -struct SignedMintValidationParams { - uint80 minMintPrice; // 80/256 bits - uint24 maxMaxTotalMintableByWallet; // 104/256 bits - uint40 minStartTime; // 144/256 bits - uint40 maxEndTime; // 184/256 bits - uint40 maxMaxTokenSupplyForStage; // 224/256 bits - uint16 minFeeBps; // 240/256 bits - uint16 maxFeeBps; // 256/256 bits -} diff --git a/src/seadrop/shim/Shim.sol b/src/seadrop/shim/Shim.sol deleted file mode 100644 index 1d3ba37..0000000 --- a/src/seadrop/shim/Shim.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -/** - * @dev HardHat doesn't support multiple source folders; so import everything - * extra that hardhat tests rely on so they get compiled. Allows for faster - * feedback than running an extra yarn build. - */ -import {TestERC721} from "../test/TestERC721.sol"; -import {MaliciousRecipient} from "../test/MaliciousRecipient.sol"; diff --git a/src/seadrop/test/MaliciousRecipient.sol b/src/seadrop/test/MaliciousRecipient.sol deleted file mode 100644 index b558859..0000000 --- a/src/seadrop/test/MaliciousRecipient.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {SeaDrop} from "../SeaDrop.sol"; - -contract MaliciousRecipient { - bool public startAttack; - address public token; - SeaDrop public seaDrop; - - receive() external payable { - if (startAttack) { - startAttack = false; - seaDrop.mintPublic{value: 1 ether}({ - nftContract: token, - feeRecipient: address(this), - minterIfNotPayer: address(this), - quantity: 1 - }); - } - } - - // Also receive some eth in the process - function setStartAttack() public payable { - startAttack = true; - } - - function attack(SeaDrop _seaDrop, address _token) external payable { - token = _token; - seaDrop = _seaDrop; - - _seaDrop.mintPublic{value: 1 ether}({ - nftContract: _token, - feeRecipient: address(this), - minterIfNotPayer: address(this), - quantity: 1 - }); - - token = address(0); - seaDrop = SeaDrop(address(0)); - } -} diff --git a/src/seadrop/test/TestERC721.sol b/src/seadrop/test/TestERC721.sol deleted file mode 100644 index b627634..0000000 --- a/src/seadrop/test/TestERC721.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: Unlicense -pragma solidity ^0.8.17; - -import {ERC721} from "solady/tokens/ERC721.sol"; - -// Used for minting test ERC721s in our tests -contract TestERC721 is ERC721 { - function name() public pure override returns (string memory) { - return "TestERC721"; - } - - function symbol() public pure override returns (string memory) { - return "TEST"; - } - - function mint(address to, uint256 tokenId) public returns (bool) { - _mint(to, tokenId); - return true; - } - - function tokenURI(uint256) public pure override returns (string memory) { - return "tokenURI"; - } -}