├── .github ├── pull_request_template.md └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── foundry.toml ├── script ├── DeployAndConfigure1155Receive.s.sol ├── DeployAndConfigureExampleCampaign.s.sol.txt ├── DeployAndRedeemTokens-CampaignOnReceiveToken.s.sol ├── DeployAndRedeemTokens.s.sol ├── DeployAndRedeemTrait.s.sol ├── DeployERC721ReceiveTokenWithPredeployedSeadropRedeemToken.s.sol └── RedeemTokens.s.sol ├── src ├── ERC1155SeaDropRedeemable.sol ├── ERC1155ShipyardRedeemable.sol ├── ERC721SeaDropRedeemable.sol ├── ERC721ShipyardRedeemable.sol ├── RedeemableContractOfferer.sol.txt ├── extensions │ ├── ERC1155ShipyardRedeemableMintable.sol │ └── ERC721ShipyardRedeemableMintable.sol ├── interfaces │ ├── IERC1155Receiver.sol │ ├── IERC7498.sol │ ├── IRedeemableContractOfferer.sol │ ├── IRedemptionMintable.sol │ └── IShipyardContractMetadata.sol ├── lib │ ├── ERC1155ShipyardContractMetadata.sol │ ├── ERC721ShipyardContractMetadata.sol │ ├── ERC7498NFTRedeemables.sol │ ├── RedeemablesConstants.sol │ ├── RedeemablesErrors.sol │ ├── RedeemablesStructs.sol │ ├── SignedRedeem.sol.txt │ ├── SignedRedeemContractOfferer.sol.txt │ └── SignedRedeemErrorsAndEvents.sol └── test │ ├── ERC1155SeaDropRedeemableOwnerMintable.sol │ ├── ERC1155ShipyardRedeemableOwnerMintable.sol │ ├── ERC721SeaDropRedeemableOwnerMintable.sol │ ├── ERC721ShipyardRedeemableOwnerMintable.sol │ ├── ERC721ShipyardRedeemableOwnerMintableWithoutInternalBurn.sol │ └── ERC721ShipyardRedeemableTraitSetters.sol └── test ├── ERC1155ShipyardRedeemable.t.sol ├── ERC721ShipyardRedeemable.t.sol ├── ERC7498-GetAndUpdateCampaign.t.sol ├── ERC7498-MultiRedeem.t.sol ├── ERC7498-RedemptionMintable.t.sol ├── ERC7498-Revert.t.sol ├── ERC7498-SimpleRedeem.t.sol ├── ERC7498-TraitRedemption.t.sol ├── ERC7498.t.sol ├── RedeemableContractOfferer-1155.t.sol.txt ├── RedeemableContractOfferer-721.t.sol.txt ├── RedeemableContractOfferer-Revert.t.sol.txt ├── ShipyardContractMetadata.t.sol └── utils ├── ArithmeticUtil.sol ├── BaseOrderTest.sol ├── BaseRedeemablesTest.sol ├── BaseSeaportTest.sol ├── DifferentialTest.sol └── mocks ├── ERC1155Recipient.sol ├── ERC721Recipient.sol ├── MockERC1271Wallet.sol ├── MockERC721DynamicTraits.sol ├── TestERC1155.sol ├── TestERC20.sol └── TestERC721.sol /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | ## Motivation 11 | 12 | 19 | 20 | ## Solution 21 | 22 | 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | tags: ["*"] 7 | pull_request: 8 | types: [opened, reopened, synchronize] 9 | 10 | env: 11 | FOUNDRY_PROFILE: ci 12 | 13 | jobs: 14 | check: 15 | strategy: 16 | fail-fast: true 17 | 18 | name: Foundry project 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | submodules: recursive 24 | 25 | - name: Install Foundry 26 | uses: foundry-rs/foundry-toolchain@v1 27 | with: 28 | version: nightly 29 | 30 | - name: Run Forge build 31 | run: | 32 | forge --version 33 | forge build 34 | id: build 35 | 36 | - name: Run Forge tests 37 | run: | 38 | forge test -vvv 39 | id: test 40 | 41 | - name: Run Forge format 42 | run: | 43 | forge fmt 44 | [ -z "`git status --porcelain`" ] && echo "No diff for format" || { echo "Diff exists for format"; exit 1; } 45 | id: lint 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | coverage/ 5 | lcov.info 6 | 7 | # Ignores broadcast logs 8 | /broadcast 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | 16 | .vscode 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/solady"] 5 | path = lib/solady 6 | url = git@github.com:Vectorized/solady.git 7 | [submodule "lib/solarray"] 8 | path = lib/solarray 9 | url = https://github.com/evmcheb/solarray 10 | [submodule "lib/seaport-types"] 11 | path = lib/seaport-types 12 | url = https://github.com/ProjectOpenSea/seaport-types 13 | [submodule "lib/seaport-sol"] 14 | path = lib/seaport-sol 15 | url = https://github.com/ProjectOpenSea/seaport-sol 16 | [submodule "lib/seaport-core"] 17 | path = lib/seaport-core 18 | url = https://github.com/ProjectOpenSea/seaport-core 19 | [submodule "lib/murky"] 20 | path = lib/murky 21 | url = https://github.com/dmfxyz/murky.git 22 | [submodule "shipyard-core"] 23 | path = shipyard-core 24 | url = https://github.com/ProjectOpenSea/shipyard-core.git 25 | [submodule "lib/seadrop"] 26 | path = lib/seadrop 27 | url = https://github.com/ProjectOpenSea/seadrop 28 | branch = v2 29 | [submodule "lib/ds-test"] 30 | path = lib/ds-test 31 | url = https://github.com/dapphub/ds-test 32 | [submodule "lib/shipyard-core"] 33 | path = lib/shipyard-core 34 | url = https://github.com/ProjectOpenSea/shipyard-core.git 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ozone Networks, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redeemables 2 | 3 | EVM smart contracts for redeemables and dynamic traits. 4 | 5 | ## Foundry 6 | 7 | To install Foundry (assuming a Linux or macOS system): 8 | 9 | ```bash 10 | curl -L https://foundry.paradigm.xyz | bash 11 | ``` 12 | 13 | This will download foundryup. To start Foundry, run: 14 | 15 | ```bash 16 | foundryup 17 | ``` 18 | 19 | To install dependencies: 20 | 21 | ``` 22 | forge install 23 | ``` 24 | 25 | To run tests: 26 | 27 | ``` 28 | forge test 29 | ``` 30 | 31 | To run format: 32 | 33 | ``` 34 | forge fmt 35 | ``` 36 | 37 | 44 | 45 | The following modifiers are also available: 46 | 47 | - Level 2 (-vv): Logs emitted during tests are also displayed. 48 | - Level 3 (-vvv): Stack traces for failing tests are also displayed. 49 | - Level 4 (-vvvv): Stack traces for all tests are displayed, and setup traces for failing tests are displayed. 50 | - Level 5 (-vvvvv): Stack traces and setup traces are always displayed. 51 | 52 | ```bash 53 | forge test -vv 54 | ``` 55 | 56 | For more information on foundry testing and use, see [Foundry Book installation instructions](https://book.getfoundry.sh/getting-started/installation). 57 | 58 | ## Contributing 59 | 60 | Contributions are welcome by anyone interested in writing more tests, improving readability, optimizing for gas efficiency, or extending the protocol with new features. 61 | 62 | When making a pull request, ensure that: 63 | 64 | - All tests pass. 65 | - Code coverage remains at 100% (coverage tests must currently be written in hardhat). 66 | - All new code adheres to the style guide: 67 | - All lint checks pass. 68 | - Code is thoroughly commented with natspec where relevant. 69 | - If making a change to the contracts: 70 | - Gas snapshots are provided and demonstrate an improvement (or an acceptable deficit given other improvements). 71 | - Reference contracts are modified correspondingly if relevant. 72 | - New tests (ideally via foundry) are included for all new features or code paths. 73 | - If making a modification to third-party dependencies, `yarn audit` passes. 74 | - A descriptive summary of the PR has been provided. 75 | 76 | ## License 77 | 78 | [MIT](LICENSE) Copyright 2023 Ozone Networks, Inc. 79 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | bytecode_hash = 'none' 6 | #auto_detect_remappings = false 7 | remappings = [ 8 | 'ds-test/=lib/forge-std/lib/ds-test/src/', 9 | 'solady/=lib/solady/', 10 | 'solarray/=lib/solarray/src/', 11 | 'seaport-core/=lib/seaport-core/', 12 | 'seaport-sol/=lib/seaport-sol/', 13 | 'seaport-types/=lib/seaport-types/', 14 | 'shipyard-core/=lib/shipyard-core/', 15 | 'seadrop/=lib/seadrop/', 16 | 'openzeppelin-contracts/=lib/shipyard-core/lib/openzeppelin-contracts', 17 | 'ERC721A/=lib/seadrop/lib/ERC721A/contracts/' 18 | ] 19 | 20 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config -------------------------------------------------------------------------------- /script/DeployAndConfigure1155Receive.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {Test} from "forge-std/Test.sol"; 6 | import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; 7 | import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 8 | import {Campaign, CampaignParams, CampaignRequirements} from "../src/lib/RedeemablesStructs.sol"; 9 | import {BURN_ADDRESS} from "../src/lib/RedeemablesConstants.sol"; 10 | import {ERC721ShipyardRedeemableMintable} from "../src/extensions/ERC721ShipyardRedeemableMintable.sol"; 11 | import {ERC1155ShipyardRedeemableMintable} from "../src/extensions/ERC1155ShipyardRedeemableMintable.sol"; 12 | 13 | contract DeployAndConfigure1155Receive is Script, Test { 14 | function run() external { 15 | vm.startBroadcast(); 16 | 17 | address redeemToken = 0x1eCC76De3f9E4e9f8378f6ade61A02A10f976c45; 18 | ERC1155ShipyardRedeemableMintable receiveToken = 19 | new ERC1155ShipyardRedeemableMintable("TestRedeemablesReceive1155SequentialIds", "TEST"); 20 | 21 | // Configure the campaign. 22 | OfferItem[] memory offer = new OfferItem[](3); 23 | offer[0] = OfferItem({ 24 | itemType: ItemType.ERC1155_WITH_CRITERIA, 25 | token: address(receiveToken), 26 | identifierOrCriteria: 0, 27 | startAmount: 1, 28 | endAmount: 1 29 | }); 30 | offer[1] = OfferItem({ 31 | itemType: ItemType.ERC1155_WITH_CRITERIA, 32 | token: address(receiveToken), 33 | identifierOrCriteria: 0, 34 | startAmount: 1, 35 | endAmount: 1 36 | }); 37 | offer[2] = OfferItem({ 38 | itemType: ItemType.ERC1155_WITH_CRITERIA, 39 | token: address(receiveToken), 40 | identifierOrCriteria: 0, 41 | startAmount: 1, 42 | endAmount: 1 43 | }); 44 | 45 | ConsiderationItem[] memory consideration = new ConsiderationItem[](1); 46 | consideration[0] = ConsiderationItem({ 47 | itemType: ItemType.ERC721_WITH_CRITERIA, 48 | token: address(redeemToken), 49 | identifierOrCriteria: 0, 50 | startAmount: 1, 51 | endAmount: 1, 52 | recipient: payable(BURN_ADDRESS) 53 | }); 54 | 55 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 56 | requirements[0].offer = offer; 57 | requirements[0].consideration = consideration; 58 | 59 | CampaignParams memory params = CampaignParams({ 60 | startTime: 0, 61 | endTime: 0, 62 | maxCampaignRedemptions: 1_000, 63 | manager: msg.sender, 64 | signer: address(0) 65 | }); 66 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 67 | receiveToken.createCampaign(campaign, "ipfs://QmQjubc6guHReNW5Es5ZrgDtJRwXk2Aia7BkVoLJGaCRqP"); 68 | 69 | // To test updateCampaign, update to proper start/end times. 70 | campaign.params.startTime = uint32(block.timestamp); 71 | campaign.params.endTime = uint32(block.timestamp + 1_000_000); 72 | receiveToken.updateCampaign(1, campaign, ""); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /script/DeployAndConfigureExampleCampaign.s.sol.txt: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; 7 | import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 8 | import {RedeemableContractOfferer} from "../src/RedeemableContractOfferer.sol"; 9 | import {CampaignParams} from "../src/lib/RedeemablesStructs.sol"; 10 | import {BURN_ADDRESS} from "../src/lib/RedeemablesConstants.sol"; 11 | import {ERC721ShipyardRedeemableMintable} from "../src/extensions/ERC721ShipyardRedeemableMintable.sol"; 12 | import {TestERC721} from "../test/utils/mocks/TestERC721.sol"; 13 | 14 | contract DeployAndConfigureExampleCampaign is Script { 15 | // Addresses: Seaport 16 | address seaport = 0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC; 17 | address conduit = 0x1E0049783F008A0085193E00003D00cd54003c71; 18 | bytes32 conduitKey = 0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000; 19 | 20 | function run() external { 21 | vm.startBroadcast(); 22 | 23 | RedeemableContractOfferer offerer = new RedeemableContractOfferer( 24 | conduit, 25 | conduitKey, 26 | seaport 27 | ); 28 | TestERC721 redeemableToken = new TestERC721(); 29 | ERC721ShipyardRedeemableMintable redemptionToken = new ERC721ShipyardRedeemableMintable( 30 | address(offerer), 31 | address(redeemableToken) 32 | ); 33 | 34 | // Configure the campaign. 35 | OfferItem[] memory offer = new OfferItem[](1); 36 | offer[0] = OfferItem({ 37 | itemType: ItemType.ERC721_WITH_CRITERIA, 38 | token: address(redemptionToken), 39 | identifierOrCriteria: 0, 40 | startAmount: 1, 41 | endAmount: 1 42 | }); 43 | 44 | ConsiderationItem[] memory consideration = new ConsiderationItem[](1); 45 | consideration[0] = ConsiderationItem({ 46 | itemType: ItemType.ERC721_WITH_CRITERIA, 47 | token: address(redeemableToken), 48 | identifierOrCriteria: 0, 49 | startAmount: 1, 50 | endAmount: 1, 51 | recipient: payable(BURN_ADDRESS) 52 | }); 53 | 54 | CampaignParams memory params = CampaignParams({ 55 | offer: offer, 56 | consideration: consideration, 57 | signer: address(0), 58 | startTime: uint32(block.timestamp), 59 | endTime: uint32(block.timestamp + 1_000_000), 60 | maxCampaignRedemptions: 1_000, 61 | manager: msg.sender 62 | }); 63 | offerer.createCampaign(params, "ipfs://QmdChMVnMSq4U6oVKhud7wUSEZGnwuMuTY5rUQx57Ayp6H"); 64 | 65 | // Mint tokens 1 and 5 to redeem for tokens 1 and 5. 66 | redeemableToken.mint(msg.sender, 1); 67 | redeemableToken.mint(msg.sender, 5); 68 | 69 | // Let's redeem them! 70 | uint256 campaignId = 1; 71 | bytes32 redemptionHash = bytes32(0); 72 | bytes memory data = abi.encode(campaignId, redemptionHash); 73 | redeemableToken.safeTransferFrom(msg.sender, address(offerer), 1, data); 74 | redeemableToken.safeTransferFrom(msg.sender, address(offerer), 5, data); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /script/DeployAndRedeemTokens-CampaignOnReceiveToken.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {Test} from "forge-std/Test.sol"; 6 | import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; 7 | import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 8 | import {Campaign, CampaignParams, CampaignRequirements} from "../src/lib/RedeemablesStructs.sol"; 9 | import {BURN_ADDRESS} from "../src/lib/RedeemablesConstants.sol"; 10 | import {ERC721ShipyardRedeemableMintable} from "../src/extensions/ERC721ShipyardRedeemableMintable.sol"; 11 | import {ERC721ShipyardRedeemableOwnerMintable} from "../src/test/ERC721ShipyardRedeemableOwnerMintable.sol"; 12 | 13 | contract DeployAndRedeemTokens_CampaignOnReceiveToken is Script, Test { 14 | function run() external { 15 | vm.startBroadcast(); 16 | 17 | ERC721ShipyardRedeemableOwnerMintable redeemToken = 18 | new ERC721ShipyardRedeemableOwnerMintable("TestRedeemablesRedeemToken", "TEST-RDM"); 19 | ERC721ShipyardRedeemableMintable receiveToken = 20 | new ERC721ShipyardRedeemableMintable("TestRedeemablesReceiveToken", "TEST-RCV"); 21 | 22 | // Configure the campaign. 23 | OfferItem[] memory offer = new OfferItem[](1); 24 | offer[0] = OfferItem({ 25 | itemType: ItemType.ERC721_WITH_CRITERIA, 26 | token: address(receiveToken), 27 | identifierOrCriteria: 0, 28 | startAmount: 1, 29 | endAmount: 1 30 | }); 31 | 32 | ConsiderationItem[] memory consideration = new ConsiderationItem[](1); 33 | consideration[0] = ConsiderationItem({ 34 | itemType: ItemType.ERC721_WITH_CRITERIA, 35 | token: address(redeemToken), 36 | identifierOrCriteria: 0, 37 | startAmount: 1, 38 | endAmount: 1, 39 | recipient: payable(BURN_ADDRESS) 40 | }); 41 | 42 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 43 | requirements[0].offer = offer; 44 | requirements[0].consideration = consideration; 45 | 46 | CampaignParams memory params = CampaignParams({ 47 | startTime: uint32(block.timestamp), 48 | endTime: uint32(block.timestamp + 1_000_000), 49 | maxCampaignRedemptions: 1_000, 50 | manager: msg.sender, 51 | signer: address(0) 52 | }); 53 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 54 | uint256 campaignId = 55 | receiveToken.createCampaign(campaign, "ipfs://Qmd1svWLxdjRUCxDCv6i6MFZtcU6SY56mD6JM8Ds1ZrXPB"); 56 | 57 | // Mint token 1 to redeem for token 1. 58 | redeemToken.mint(msg.sender, 1); 59 | redeemToken.mint(msg.sender, 2); 60 | redeemToken.mint(msg.sender, 3); 61 | redeemToken.mint(msg.sender, 4); 62 | 63 | // Let's redeem them! 64 | uint256[] memory traitRedemptionTokenIds; 65 | bytes memory data = abi.encode( 66 | campaignId, 67 | 0, // requirementsIndex 68 | bytes32(0), // redemptionHash 69 | traitRedemptionTokenIds, 70 | uint256(0), // salt 71 | bytes("") // signature 72 | ); 73 | 74 | uint256[] memory tokenIds = new uint256[](1); 75 | tokenIds[0] = 1; 76 | 77 | // Individual user approvals not needed when setting the burn address. 78 | // redeemToken.setBurnAddress(address(receiveToken)); 79 | redeemToken.setApprovalForAll(address(receiveToken), true); 80 | 81 | receiveToken.redeem(tokenIds, msg.sender, data); 82 | 83 | // Assert redeemable token is burned and redemption token is minted. 84 | assertEq(redeemToken.balanceOf(msg.sender), 0); 85 | assertEq(receiveToken.ownerOf(1), msg.sender); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /script/DeployAndRedeemTokens.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {Test} from "forge-std/Test.sol"; 6 | import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; 7 | import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 8 | import {Campaign, CampaignParams, CampaignRequirements} from "../src/lib/RedeemablesStructs.sol"; 9 | import {BURN_ADDRESS} from "../src/lib/RedeemablesConstants.sol"; 10 | import {ERC721ShipyardRedeemableMintable} from "../src/extensions/ERC721ShipyardRedeemableMintable.sol"; 11 | import {ERC721ShipyardRedeemableOwnerMintable} from "../src/test/ERC721ShipyardRedeemableOwnerMintable.sol"; 12 | 13 | contract DeployAndRedeemTokens is Script, Test { 14 | function run() external { 15 | vm.startBroadcast(); 16 | 17 | ERC721ShipyardRedeemableOwnerMintable redeemToken = 18 | new ERC721ShipyardRedeemableOwnerMintable("TestRedeemablesRedeemToken", "TEST"); 19 | address[] memory redeemTokens = new address[](1); 20 | redeemTokens[0] = address(redeemToken); 21 | ERC721ShipyardRedeemableMintable receiveToken = 22 | new ERC721ShipyardRedeemableMintable("TestRedeemablesRecieveToken", "TEST"); 23 | receiveToken.setRedeemablesContracts(redeemTokens); 24 | 25 | // Configure the campaign. 26 | OfferItem[] memory offer = new OfferItem[](1); 27 | offer[0] = OfferItem({ 28 | itemType: ItemType.ERC721_WITH_CRITERIA, 29 | token: address(receiveToken), 30 | identifierOrCriteria: 0, 31 | startAmount: 1, 32 | endAmount: 1 33 | }); 34 | 35 | ConsiderationItem[] memory consideration = new ConsiderationItem[](1); 36 | consideration[0] = ConsiderationItem({ 37 | itemType: ItemType.ERC721_WITH_CRITERIA, 38 | token: address(redeemToken), 39 | identifierOrCriteria: 0, 40 | startAmount: 1, 41 | endAmount: 1, 42 | recipient: payable(BURN_ADDRESS) 43 | }); 44 | 45 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 46 | requirements[0].offer = offer; 47 | requirements[0].consideration = consideration; 48 | 49 | CampaignParams memory params = CampaignParams({ 50 | startTime: uint32(block.timestamp), 51 | endTime: uint32(block.timestamp + 1_000_000), 52 | maxCampaignRedemptions: 1_000, 53 | manager: msg.sender, 54 | signer: address(0) 55 | }); 56 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 57 | redeemToken.createCampaign(campaign, ""); 58 | 59 | // Mint token 1 to redeem for token 1. 60 | redeemToken.mint(msg.sender, 1); 61 | 62 | // Let's redeem them! 63 | uint256[] memory traitRedemptionTokenIds; 64 | bytes memory data = abi.encode( 65 | 1, // campaignId 66 | 0, // requirementsIndex 67 | bytes32(0), // redemptionHash 68 | traitRedemptionTokenIds, 69 | uint256(0), // salt 70 | bytes("") // signature 71 | ); 72 | 73 | uint256[] memory tokenIds = new uint256[](1); 74 | tokenIds[0] = 1; 75 | 76 | redeemToken.redeem(tokenIds, msg.sender, data); 77 | 78 | // Assert redeemable token is burned and redemption token is minted. 79 | assertEq(redeemToken.balanceOf(msg.sender), 0); 80 | assertEq(receiveToken.ownerOf(1), msg.sender); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /script/DeployAndRedeemTrait.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {Test} from "forge-std/Test.sol"; 6 | import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; 7 | import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 8 | import {IERC7496} from "shipyard-core/src/dynamic-traits/interfaces/IERC7496.sol"; 9 | import {Campaign, CampaignParams, CampaignRequirements, TraitRedemption} from "../src/lib/RedeemablesStructs.sol"; 10 | import {ERC721ShipyardRedeemableMintable} from "../src/extensions/ERC721ShipyardRedeemableMintable.sol"; 11 | import {ERC721ShipyardRedeemableTraitSetters} from "../src/test/ERC721ShipyardRedeemableTraitSetters.sol"; 12 | 13 | contract DeployAndRedeemTrait is Script, Test { 14 | function run() external { 15 | vm.startBroadcast(); 16 | 17 | // deploy the receive token first 18 | ERC721ShipyardRedeemableMintable receiveToken = 19 | new ERC721ShipyardRedeemableMintable("TestRedeemablesRecieveToken", "TEST"); 20 | 21 | // add the receive token address to allowed trait setters array 22 | address[] memory allowedTraitSetters = new address[](1); 23 | allowedTraitSetters[0] = address(receiveToken); 24 | 25 | // deploy the redeem token 26 | ERC721ShipyardRedeemableTraitSetters redeemToken = 27 | new ERC721ShipyardRedeemableTraitSetters("DynamicTraitsRedeemToken", "TEST"); 28 | // set the receive token as an allowed trait setter 29 | redeemToken.setAllowedTraitSetters(allowedTraitSetters); 30 | 31 | // configure the campaign. 32 | OfferItem[] memory offer = new OfferItem[](1); 33 | 34 | // offer is receive token 35 | offer[0] = OfferItem({ 36 | itemType: ItemType.ERC721_WITH_CRITERIA, 37 | token: address(receiveToken), 38 | identifierOrCriteria: 0, 39 | startAmount: 1, 40 | endAmount: 1 41 | }); 42 | 43 | // consideration is empty 44 | ConsiderationItem[] memory consideration = new ConsiderationItem[](0); 45 | 46 | TraitRedemption[] memory traitRedemptions = new TraitRedemption[](1); 47 | 48 | // trait key is "hasRedeemed" 49 | bytes32 traitKey = bytes32("hasRedeemed"); 50 | 51 | // previous trait value (`substandardValue`) should be 0 52 | bytes32 substandardValue = bytes32(uint256(0)); 53 | 54 | // new trait value should be 1 55 | bytes32 traitValue = bytes32(uint256(1)); 56 | 57 | traitRedemptions[0] = TraitRedemption({ 58 | substandard: 1, 59 | token: address(redeemToken), 60 | traitKey: traitKey, 61 | traitValue: traitValue, 62 | substandardValue: substandardValue 63 | }); 64 | 65 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 66 | requirements[0].offer = offer; 67 | requirements[0].consideration = consideration; 68 | requirements[0].traitRedemptions = traitRedemptions; 69 | 70 | CampaignParams memory params = CampaignParams({ 71 | startTime: uint32(block.timestamp), 72 | endTime: uint32(block.timestamp + 1_000_000), 73 | maxCampaignRedemptions: 1_000, 74 | manager: msg.sender, 75 | signer: address(0) 76 | }); 77 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 78 | receiveToken.createCampaign(campaign, ""); 79 | 80 | // Mint token 1 to redeem for token 1. 81 | redeemToken.mint(msg.sender, 1); 82 | 83 | // Let's redeem them! 84 | uint256[] memory traitRedemptionTokenIds = new uint256[](1); 85 | traitRedemptionTokenIds[0] = 1; 86 | 87 | bytes memory data = abi.encode( 88 | 1, // campaignId 89 | 0, // requirementsIndex 90 | bytes32(0), // redemptionHash 91 | traitRedemptionTokenIds, 92 | uint256(0), // salt 93 | bytes("") // signature 94 | ); 95 | 96 | receiveToken.redeem(new uint256[](0), msg.sender, data); 97 | 98 | // Assert new trait has been set and redemption token is minted. 99 | bytes32 actualTraitValue = IERC7496(address(redeemToken)).getTraitValue(1, traitKey); 100 | // "hasRedeemed" should be 1 (true) 101 | assertEq(bytes32(uint256(1)), actualTraitValue); 102 | assertEq(receiveToken.ownerOf(1), msg.sender); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /script/DeployERC721ReceiveTokenWithPredeployedSeadropRedeemToken.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {Test} from "forge-std/Test.sol"; 6 | import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; 7 | import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 8 | import {Campaign, CampaignParams, CampaignRequirements} from "../src/lib/RedeemablesStructs.sol"; 9 | import {BURN_ADDRESS} from "../src/lib/RedeemablesConstants.sol"; 10 | import {ERC721ShipyardRedeemableMintable} from "../src/extensions/ERC721ShipyardRedeemableMintable.sol"; 11 | import {ERC721ShipyardRedeemableOwnerMintable} from "../src/test/ERC721ShipyardRedeemableOwnerMintable.sol"; 12 | 13 | contract DeployERC721ReceiveTokenWithPredeployedSeaDropRedeemToken is Script, Test { 14 | function run() external { 15 | vm.startBroadcast(); 16 | 17 | ERC721ShipyardRedeemableMintable redeemToken = 18 | ERC721ShipyardRedeemableMintable(0xa1783E74857736b2AEE610A36b537B31CC333048); 19 | ERC721ShipyardRedeemableMintable receiveToken = 20 | ERC721ShipyardRedeemableMintable(0x343B9aEC7fAB02d07c6747Bace112920822334B4); 21 | 22 | // Configure the campaign. 23 | OfferItem[] memory offer = new OfferItem[](1); 24 | offer[0] = OfferItem({ 25 | itemType: ItemType.ERC721_WITH_CRITERIA, 26 | token: address(receiveToken), 27 | identifierOrCriteria: 0, 28 | startAmount: 1, 29 | endAmount: 1 30 | }); 31 | 32 | ConsiderationItem[] memory consideration = new ConsiderationItem[](1); 33 | consideration[0] = ConsiderationItem({ 34 | itemType: ItemType.ERC721_WITH_CRITERIA, 35 | token: address(redeemToken), 36 | identifierOrCriteria: 0, 37 | startAmount: 1, 38 | endAmount: 1, 39 | recipient: payable(BURN_ADDRESS) 40 | }); 41 | 42 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 43 | requirements[0].offer = offer; 44 | requirements[0].consideration = consideration; 45 | 46 | CampaignParams memory params = CampaignParams({ 47 | startTime: uint32(block.timestamp), 48 | endTime: uint32(block.timestamp + 1_000_000), 49 | maxCampaignRedemptions: 1_000, 50 | manager: msg.sender, 51 | signer: address(0) 52 | }); 53 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 54 | uint256 campaignId = 55 | receiveToken.createCampaign(campaign, "ipfs://QmQKc93y2Ev5k9Kz54mCw48ZM487bwGDktZYPLtrjJ3r1d"); 56 | 57 | // redeemToken.setBaseURI( 58 | // "ipfs://QmYTSupCtriDLBHgPBBhZ98wYdp6N9S8jTL5sKSZwbASeT" 59 | // ); 60 | 61 | // receiveToken.setBaseURI( 62 | // "ipfs://QmWxgnz8T9wsMBmpCY4Cvanj3RR1obFD2hqDKPZhKN5Tsq/" 63 | // ); 64 | 65 | // Let's redeem them! 66 | uint256 requirementsIndex = 0; 67 | bytes32 redemptionHash; 68 | uint256 salt; 69 | bytes memory signature; 70 | bytes memory data = abi.encode(campaignId, requirementsIndex, redemptionHash, salt, signature); 71 | 72 | uint256[] memory tokenIds = new uint256[](1); 73 | tokenIds[0] = 1; 74 | 75 | redeemToken.setApprovalForAll(address(receiveToken), true); 76 | 77 | receiveToken.redeem(tokenIds, msg.sender, data); 78 | 79 | // Assert redeemable token is burned and redemption token is minted. 80 | assertEq(redeemToken.balanceOf(msg.sender), 2); 81 | assertEq(receiveToken.ownerOf(1), msg.sender); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /script/RedeemTokens.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {Test} from "forge-std/Test.sol"; 6 | import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; 7 | import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 8 | import {CampaignParams, CampaignRequirements} from "../src/lib/RedeemablesStructs.sol"; 9 | import {ERC721ShipyardRedeemableMintable} from "../src/extensions/ERC721ShipyardRedeemableMintable.sol"; 10 | import {ERC721ShipyardRedeemableOwnerMintable} from "../src/test/ERC721ShipyardRedeemableOwnerMintable.sol"; 11 | import {ERC1155ShipyardRedeemableMintable} from "../src/extensions/ERC1155ShipyardRedeemableMintable.sol"; 12 | 13 | contract RedeemTokens is Script, Test { 14 | function run() external { 15 | vm.startBroadcast(); 16 | 17 | // address redeemToken = 0x1eCC76De3f9E4e9f8378f6ade61A02A10f976c45; 18 | ERC1155ShipyardRedeemableMintable receiveToken = 19 | ERC1155ShipyardRedeemableMintable(0x3D0fa2a8D07dfe357905a4cB4ed51b0Aea8385B9); 20 | 21 | // Let's redeem them! 22 | uint256[] memory traitRedemptionTokenIds; 23 | bytes memory data = abi.encode( 24 | 1, // campaignId 25 | 0, // requirementsIndex 26 | bytes32(0), // redemptionHash 27 | traitRedemptionTokenIds, 28 | uint256(0), // salt 29 | bytes("") // signature 30 | ); 31 | 32 | uint256[] memory redeemTokenIds = new uint256[](1); 33 | redeemTokenIds[0] = 1; 34 | 35 | // Individual user approvals not needed if preapproved. 36 | // redeemToken.setApprovalForAll(address(receiveToken), true); 37 | 38 | receiveToken.redeem(redeemTokenIds, msg.sender, data); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ERC1155SeaDropRedeemable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC1155SeaDrop} from "seadrop/src/ERC1155SeaDrop.sol"; 5 | import {ERC1155SeaDropContractOfferer} from "seadrop/src/lib/ERC1155SeaDropContractOfferer.sol"; 6 | import {IERC7498} from "./interfaces/IERC7498.sol"; 7 | import {ERC7498NFTRedeemables} from "./lib/ERC7498NFTRedeemables.sol"; 8 | import {DynamicTraits} from "shipyard-core/src/dynamic-traits/DynamicTraits.sol"; 9 | import {Campaign} from "./lib/RedeemablesStructs.sol"; 10 | 11 | contract ERC1155SeaDropRedeemable is ERC1155SeaDrop, ERC7498NFTRedeemables { 12 | constructor(address allowedConfigurer, address allowedSeaport, string memory _name, string memory _symbol) 13 | ERC1155SeaDrop(allowedConfigurer, allowedSeaport, _name, _symbol) 14 | {} 15 | 16 | function createCampaign(Campaign calldata campaign, string calldata metadataURI) 17 | public 18 | override 19 | onlyOwner 20 | returns (uint256 campaignId) 21 | { 22 | campaignId = ERC7498NFTRedeemables.createCampaign(campaign, metadataURI); 23 | } 24 | 25 | function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) public virtual override onlyOwner { 26 | DynamicTraits.setTrait(tokenId, traitKey, value); 27 | } 28 | 29 | function _useInternalBurn() internal pure virtual override returns (bool) { 30 | return true; 31 | } 32 | 33 | function _internalBurn(address from, uint256 id, uint256 amount) internal virtual override { 34 | _burn(from, id, amount); 35 | } 36 | 37 | function supportsInterface(bytes4 interfaceId) 38 | public 39 | view 40 | virtual 41 | override(ERC1155SeaDropContractOfferer, ERC7498NFTRedeemables) 42 | returns (bool) 43 | { 44 | return ERC1155SeaDropContractOfferer.supportsInterface(interfaceId) 45 | || ERC7498NFTRedeemables.supportsInterface(interfaceId); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ERC1155ShipyardRedeemable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC1155ShipyardContractMetadata} from "./lib/ERC1155ShipyardContractMetadata.sol"; 5 | import {Ownable} from "solady/src/auth/Ownable.sol"; 6 | import {ERC7498NFTRedeemables} from "./lib/ERC7498NFTRedeemables.sol"; 7 | import {DynamicTraits} from "shipyard-core/src/dynamic-traits/DynamicTraits.sol"; 8 | import {Campaign} from "./lib/RedeemablesStructs.sol"; 9 | 10 | contract ERC1155ShipyardRedeemable is ERC1155ShipyardContractMetadata, ERC7498NFTRedeemables { 11 | constructor(string memory name_, string memory symbol_) ERC1155ShipyardContractMetadata(name_, symbol_) {} 12 | 13 | function createCampaign(Campaign calldata campaign, string calldata metadataURI) 14 | public 15 | override 16 | onlyOwner 17 | returns (uint256 campaignId) 18 | { 19 | campaignId = ERC7498NFTRedeemables.createCampaign(campaign, metadataURI); 20 | } 21 | 22 | function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) public virtual override onlyOwner { 23 | DynamicTraits.setTrait(tokenId, traitKey, value); 24 | } 25 | 26 | function burn(address from, uint256 id, uint256 amount) public { 27 | _burn(msg.sender, from, id, amount); 28 | } 29 | 30 | function batchBurn(address from, uint256[] calldata ids, uint256[] calldata amounts) public { 31 | _batchBurn(msg.sender, from, ids, amounts); 32 | } 33 | 34 | function _useInternalBurn() internal pure virtual override returns (bool) { 35 | return true; 36 | } 37 | 38 | function _internalBurn(address from, uint256 id, uint256 amount) internal virtual override { 39 | _burn(from, id, amount); 40 | } 41 | 42 | function supportsInterface(bytes4 interfaceId) 43 | public 44 | view 45 | virtual 46 | override(ERC1155ShipyardContractMetadata, ERC7498NFTRedeemables) 47 | returns (bool) 48 | { 49 | return ERC1155ShipyardContractMetadata.supportsInterface(interfaceId) 50 | || ERC7498NFTRedeemables.supportsInterface(interfaceId); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ERC721SeaDropRedeemable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC721SeaDrop} from "seadrop/src/ERC721SeaDrop.sol"; 5 | import {ERC721SeaDropContractOfferer} from "seadrop/src/lib/ERC721SeaDropContractOfferer.sol"; 6 | import {IERC7498} from "./interfaces/IERC7498.sol"; 7 | import {ERC7498NFTRedeemables} from "./lib/ERC7498NFTRedeemables.sol"; 8 | import {DynamicTraits} from "shipyard-core/src/dynamic-traits/DynamicTraits.sol"; 9 | import {Campaign} from "./lib/RedeemablesStructs.sol"; 10 | 11 | contract ERC721SeaDropRedeemable is ERC721SeaDrop, ERC7498NFTRedeemables { 12 | /// @dev Revert if the token does not exist. 13 | error TokenDoesNotExist(); 14 | 15 | constructor(address allowedConfigurer, address allowedSeaport, string memory _name, string memory _symbol) 16 | ERC721SeaDrop(allowedConfigurer, allowedSeaport, _name, _symbol) 17 | {} 18 | 19 | function createCampaign(Campaign calldata campaign, string calldata metadataURI) 20 | public 21 | override 22 | onlyOwner 23 | returns (uint256 campaignId) 24 | { 25 | campaignId = ERC7498NFTRedeemables.createCampaign(campaign, metadataURI); 26 | } 27 | 28 | function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) public virtual override onlyOwner { 29 | if (!_exists(tokenId)) revert TokenDoesNotExist(); 30 | 31 | DynamicTraits.setTrait(tokenId, traitKey, value); 32 | } 33 | 34 | function getTraitValue(uint256 tokenId, bytes32 traitKey) 35 | public 36 | view 37 | virtual 38 | override 39 | returns (bytes32 traitValue) 40 | { 41 | if (!_exists(tokenId)) revert TokenDoesNotExist(); 42 | 43 | traitValue = DynamicTraits.getTraitValue(tokenId, traitKey); 44 | } 45 | 46 | function getTraitValues(uint256 tokenId, bytes32[] calldata traitKeys) 47 | public 48 | view 49 | virtual 50 | override 51 | returns (bytes32[] memory traitValues) 52 | { 53 | if (!_exists(tokenId)) revert TokenDoesNotExist(); 54 | 55 | traitValues = DynamicTraits.getTraitValues(tokenId, traitKeys); 56 | } 57 | 58 | function _useInternalBurn() internal pure virtual override returns (bool) { 59 | return true; 60 | } 61 | 62 | function _internalBurn(address, /* from */ uint256 id, uint256 /* amount */ ) internal virtual override { 63 | _burn(id); 64 | } 65 | 66 | function supportsInterface(bytes4 interfaceId) 67 | public 68 | view 69 | virtual 70 | override(ERC721SeaDropContractOfferer, ERC7498NFTRedeemables) 71 | returns (bool) 72 | { 73 | return ERC721SeaDropContractOfferer.supportsInterface(interfaceId) 74 | || ERC7498NFTRedeemables.supportsInterface(interfaceId); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/ERC721ShipyardRedeemable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC721ShipyardContractMetadata} from "./lib/ERC721ShipyardContractMetadata.sol"; 5 | import {ERC7498NFTRedeemables} from "./lib/ERC7498NFTRedeemables.sol"; 6 | import {DynamicTraits} from "shipyard-core/src/dynamic-traits/DynamicTraits.sol"; 7 | import {Campaign} from "./lib/RedeemablesStructs.sol"; 8 | 9 | contract ERC721ShipyardRedeemable is ERC721ShipyardContractMetadata, ERC7498NFTRedeemables { 10 | constructor(string memory name_, string memory symbol_) ERC721ShipyardContractMetadata(name_, symbol_) {} 11 | 12 | function createCampaign(Campaign calldata campaign, string calldata metadataURI) 13 | public 14 | override 15 | onlyOwner 16 | returns (uint256 campaignId) 17 | { 18 | campaignId = ERC7498NFTRedeemables.createCampaign(campaign, metadataURI); 19 | } 20 | 21 | function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) public virtual override onlyOwner { 22 | if (!_exists(tokenId)) revert TokenDoesNotExist(); 23 | 24 | DynamicTraits.setTrait(tokenId, traitKey, value); 25 | } 26 | 27 | function getTraitValue(uint256 tokenId, bytes32 traitKey) 28 | public 29 | view 30 | virtual 31 | override 32 | returns (bytes32 traitValue) 33 | { 34 | if (!_exists(tokenId)) revert TokenDoesNotExist(); 35 | 36 | traitValue = DynamicTraits.getTraitValue(tokenId, traitKey); 37 | } 38 | 39 | function getTraitValues(uint256 tokenId, bytes32[] calldata traitKeys) 40 | public 41 | view 42 | virtual 43 | override 44 | returns (bytes32[] memory traitValues) 45 | { 46 | if (!_exists(tokenId)) revert TokenDoesNotExist(); 47 | 48 | traitValues = DynamicTraits.getTraitValues(tokenId, traitKeys); 49 | } 50 | 51 | function burn(uint256 tokenId) public { 52 | _burn(msg.sender, tokenId); 53 | } 54 | 55 | function _useInternalBurn() internal pure virtual override returns (bool) { 56 | return true; 57 | } 58 | 59 | function _internalBurn( 60 | address, 61 | /* from */ 62 | uint256 id, 63 | uint256 /* amount */ 64 | ) internal virtual override { 65 | _burn(id); 66 | } 67 | 68 | function supportsInterface(bytes4 interfaceId) 69 | public 70 | view 71 | virtual 72 | override(ERC721ShipyardContractMetadata, ERC7498NFTRedeemables) 73 | returns (bool) 74 | { 75 | return ERC721ShipyardContractMetadata.supportsInterface(interfaceId) 76 | || ERC7498NFTRedeemables.supportsInterface(interfaceId); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/extensions/ERC1155ShipyardRedeemableMintable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {IERC165} from "openzeppelin-contracts/contracts/interfaces/IERC165.sol"; 5 | import {ERC721ConduitPreapproved_Solady} from "shipyard-core/src/tokens/erc721/ERC721ConduitPreapproved_Solady.sol"; 6 | import {ConsiderationItem, OfferItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 7 | import {Ownable} from "solady/src/auth/Ownable.sol"; 8 | import {ERC7498NFTRedeemables} from "../lib/ERC7498NFTRedeemables.sol"; 9 | import {CampaignParams} from "../lib/RedeemablesStructs.sol"; 10 | import {IRedemptionMintable} from "../interfaces/IRedemptionMintable.sol"; 11 | import {ERC1155ShipyardRedeemable} from "../ERC1155ShipyardRedeemable.sol"; 12 | import {IRedemptionMintable} from "../interfaces/IRedemptionMintable.sol"; 13 | import {TraitRedemption} from "../lib/RedeemablesStructs.sol"; 14 | 15 | contract ERC1155ShipyardRedeemableMintable is ERC1155ShipyardRedeemable, IRedemptionMintable { 16 | /// @dev The ERC-7498 redeemables contracts. 17 | address[] internal _erc7498RedeemablesContracts; 18 | 19 | /// @dev The next token id to mint. Each token will have a supply of 1. 20 | uint256 _nextTokenId = 1; 21 | 22 | constructor(string memory name_, string memory symbol_) ERC1155ShipyardRedeemable(name_, symbol_) {} 23 | 24 | function mintRedemption( 25 | uint256, /* campaignId */ 26 | address recipient, 27 | OfferItem calldata, /* offer */ 28 | ConsiderationItem[] calldata, /* consideration */ 29 | TraitRedemption[] calldata /* traitRedemptions */ 30 | ) external virtual { 31 | // Require that msg.sender is valid. 32 | _requireValidRedeemablesCaller(); 33 | 34 | // Increment nextTokenId first so more of the same token id cannot be minted through reentrancy. 35 | ++_nextTokenId; 36 | 37 | _mint(recipient, _nextTokenId - 1, 1, ""); 38 | } 39 | 40 | function getRedeemablesContracts() external view returns (address[] memory) { 41 | return _erc7498RedeemablesContracts; 42 | } 43 | 44 | function setRedeemablesContracts(address[] calldata redeemablesContracts) external onlyOwner { 45 | _erc7498RedeemablesContracts = redeemablesContracts; 46 | } 47 | 48 | function _requireValidRedeemablesCaller() internal view { 49 | // Allow the contract to call itself. 50 | if (msg.sender == address(this)) return; 51 | 52 | bool validCaller; 53 | for (uint256 i; i < _erc7498RedeemablesContracts.length; i++) { 54 | if (msg.sender == _erc7498RedeemablesContracts[i]) { 55 | validCaller = true; 56 | } 57 | } 58 | if (!validCaller) { 59 | revert InvalidCaller(msg.sender); 60 | } 61 | } 62 | 63 | function supportsInterface(bytes4 interfaceId) 64 | public 65 | view 66 | virtual 67 | override(ERC1155ShipyardRedeemable) 68 | returns (bool) 69 | { 70 | return interfaceId == type(IRedemptionMintable).interfaceId 71 | || ERC1155ShipyardRedeemable.supportsInterface(interfaceId); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/extensions/ERC721ShipyardRedeemableMintable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {IERC165} from "openzeppelin-contracts/contracts/interfaces/IERC165.sol"; 5 | import {ERC721ConduitPreapproved_Solady} from "shipyard-core/src/tokens/erc721/ERC721ConduitPreapproved_Solady.sol"; 6 | import {ConsiderationItem, OfferItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 7 | import {Ownable} from "solady/src/auth/Ownable.sol"; 8 | import {ERC7498NFTRedeemables} from "../lib/ERC7498NFTRedeemables.sol"; 9 | import {CampaignParams} from "../lib/RedeemablesStructs.sol"; 10 | import {IRedemptionMintable} from "../interfaces/IRedemptionMintable.sol"; 11 | import {ERC721ShipyardRedeemable} from "../ERC721ShipyardRedeemable.sol"; 12 | import {IRedemptionMintable} from "../interfaces/IRedemptionMintable.sol"; 13 | import {TraitRedemption} from "../lib/RedeemablesStructs.sol"; 14 | 15 | contract ERC721ShipyardRedeemableMintable is ERC721ShipyardRedeemable, IRedemptionMintable { 16 | /// @dev The ERC-7498 redeemables contracts. 17 | address[] internal _erc7498RedeemablesContracts; 18 | 19 | /// @dev The next token id to mint. 20 | uint256 _nextTokenId = 1; 21 | 22 | constructor(string memory name_, string memory symbol_) ERC721ShipyardRedeemable(name_, symbol_) {} 23 | 24 | function mintRedemption( 25 | uint256, /* campaignId */ 26 | address recipient, 27 | OfferItem calldata, /* offer */ 28 | ConsiderationItem[] calldata, /* consideration */ 29 | TraitRedemption[] calldata /* traitRedemptions */ 30 | ) external virtual { 31 | // Require that msg.sender is valid. 32 | _requireValidRedeemablesCaller(); 33 | 34 | // Increment nextTokenId first so more of the same token id cannot be minted through reentrancy. 35 | ++_nextTokenId; 36 | 37 | _mint(recipient, _nextTokenId - 1); 38 | } 39 | 40 | function getRedeemablesContracts() external view returns (address[] memory) { 41 | return _erc7498RedeemablesContracts; 42 | } 43 | 44 | function setRedeemablesContracts(address[] calldata redeemablesContracts) external onlyOwner { 45 | _erc7498RedeemablesContracts = redeemablesContracts; 46 | } 47 | 48 | function _requireValidRedeemablesCaller() internal view { 49 | // Allow the contract to call itself. 50 | if (msg.sender == address(this)) return; 51 | 52 | bool validCaller; 53 | for (uint256 i; i < _erc7498RedeemablesContracts.length; i++) { 54 | if (msg.sender == _erc7498RedeemablesContracts[i]) { 55 | validCaller = true; 56 | } 57 | } 58 | if (!validCaller) revert InvalidCaller(msg.sender); 59 | } 60 | 61 | function supportsInterface(bytes4 interfaceId) 62 | public 63 | view 64 | virtual 65 | override(ERC721ShipyardRedeemable) 66 | returns (bool) 67 | { 68 | return interfaceId == type(IRedemptionMintable).interfaceId 69 | || ERC721ShipyardRedeemable.supportsInterface(interfaceId); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/interfaces/IERC1155Receiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | interface IERC1155Receiver { 5 | /** 6 | * @dev Handles the receipt of a single ERC1155 token type. This function is 7 | * called at the end of a `safeTransferFrom` after the balance has been updated. 8 | * 9 | * NOTE: To accept the transfer, this must return 10 | * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` 11 | * (i.e. 0xf23a6e61, or its own function selector). 12 | * 13 | * @param operator The address which initiated the transfer (i.e. msg.sender) 14 | * @param from The address which previously owned the token 15 | * @param id The ID of the token being transferred 16 | * @param value The amount of tokens being transferred 17 | * @param data Additional data with no specified format 18 | * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed 19 | */ 20 | function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes calldata data) 21 | external 22 | returns (bytes4); 23 | 24 | /** 25 | * @dev Handles the receipt of a multiple ERC1155 token types. This function 26 | * is called at the end of a `safeBatchTransferFrom` after the balances have 27 | * been updated. 28 | * 29 | * NOTE: To accept the transfer(s), this must return 30 | * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` 31 | * (i.e. 0xbc197c81, or its own function selector). 32 | * 33 | * @param operator The address which initiated the batch transfer (i.e. msg.sender) 34 | * @param from The address which previously owned the token 35 | * @param ids An array containing ids of each token being transferred (order and length must match values array) 36 | * @param values An array containing amounts of each token being transferred (order and length must match ids array) 37 | * @param data Additional data with no specified format 38 | * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed 39 | */ 40 | function onERC1155BatchReceived( 41 | address operator, 42 | address from, 43 | uint256[] calldata ids, 44 | uint256[] calldata values, 45 | bytes calldata data 46 | ) external returns (bytes4); 47 | } 48 | -------------------------------------------------------------------------------- /src/interfaces/IERC7498.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 5 | import {Campaign, TraitRedemption} from "../lib/RedeemablesStructs.sol"; 6 | 7 | interface IERC7498 { 8 | event CampaignUpdated(uint256 indexed campaignId, Campaign campaign, string metadataURI); 9 | event Redemption( 10 | uint256 indexed campaignId, 11 | uint256 requirementsIndex, 12 | bytes32 redemptionHash, 13 | uint256[] considerationTokenIds, 14 | uint256[] traitRedemptionTokenIds, 15 | address redeemedBy 16 | ); 17 | 18 | function createCampaign(Campaign calldata campaign, string calldata metadataURI) 19 | external 20 | returns (uint256 campaignId); 21 | 22 | function updateCampaign(uint256 campaignId, Campaign calldata campaign, string calldata metadataURI) external; 23 | 24 | function getCampaign(uint256 campaignId) 25 | external 26 | view 27 | returns (Campaign memory campaign, string memory metadataURI, uint256 totalRedemptions); 28 | 29 | function redeem(uint256[] calldata considerationTokenIds, address recipient, bytes calldata extraData) 30 | external 31 | payable; 32 | } 33 | -------------------------------------------------------------------------------- /src/interfaces/IRedeemableContractOfferer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import { 5 | OfferItem, 6 | ConsiderationItem, 7 | SpentItem, 8 | AdvancedOrder, 9 | OrderParameters, 10 | CriteriaResolver, 11 | FulfillmentComponent 12 | } from "seaport-types/src/lib/ConsiderationStructs.sol"; 13 | import {CampaignParams, TraitRedemption} from "../lib/RedeemablesStructs.sol"; 14 | 15 | interface IRedeemableContractOfferer { 16 | /* Events */ 17 | event CampaignUpdated(uint256 indexed campaignId, CampaignParams params, string metadataURI); 18 | event Redemption(uint256 indexed campaignId, bytes32 redemptionHash); 19 | 20 | /* Getters */ 21 | function getCampaign(uint256 campaignId) 22 | external 23 | view 24 | returns (CampaignParams memory params, string memory metadataURI, uint256 totalRedemptions); 25 | 26 | /* Setters */ 27 | function createCampaign(CampaignParams calldata params, string calldata metadataURI) 28 | external 29 | returns (uint256 campaignId); 30 | 31 | function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata metadataURI) external; 32 | } 33 | -------------------------------------------------------------------------------- /src/interfaces/IRedemptionMintable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {ConsiderationItem, OfferItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 5 | import {TraitRedemption} from "../lib/RedeemablesStructs.sol"; 6 | 7 | interface IRedemptionMintable { 8 | function mintRedemption( 9 | uint256 campaignId, 10 | address recipient, 11 | OfferItem calldata offer, 12 | ConsiderationItem[] calldata consideration, 13 | TraitRedemption[] calldata traitRedemptions 14 | ) external; 15 | } 16 | -------------------------------------------------------------------------------- /src/interfaces/IShipyardContractMetadata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | interface IShipyardContractMetadata { 5 | /// @dev Emit an event for token metadata reveals/updates, according to EIP-4906. 6 | event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); 7 | 8 | /// @dev Emit an event when the URI for the collection-level metadata is updated. 9 | event ContractURIUpdated(); 10 | 11 | /// @dev Emit an event when the provenance hash is updated. 12 | event ProvenanceHashUpdated(bytes32 oldProvenanceHash, bytes32 newProvenanceHash); 13 | 14 | /// @dev Emit an event when the royalties info is updated. 15 | event RoyaltyInfoUpdated(address receiver, uint256 basisPoints); 16 | 17 | /// @dev Revert with an error when attempting to set the provenance hash after it has already been set. 18 | error ProvenanceHashCannotBeSetAfterAlreadyBeingSet(); 19 | 20 | function name() external view returns (string memory); 21 | 22 | function symbol() external view returns (string memory); 23 | 24 | function baseURI() external view returns (string memory); 25 | 26 | function contractURI() external view returns (string memory); 27 | 28 | function provenanceHash() external view returns (bytes32); 29 | 30 | function setBaseURI(string calldata newURI) external; 31 | 32 | function setContractURI(string calldata newURI) external; 33 | 34 | function setProvenanceHash(bytes32 newProvenanceHash) external; 35 | 36 | function setDefaultRoyalty(address receiver, uint96 feeNumerator) external; 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/ERC1155ShipyardContractMetadata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC1155ConduitPreapproved_Solady} from "shipyard-core/src/tokens/erc1155/ERC1155ConduitPreapproved_Solady.sol"; 5 | import {ERC1155} from "solady/src/tokens/ERC1155.sol"; 6 | import {ERC2981} from "solady/src/tokens/ERC2981.sol"; 7 | import {Ownable} from "solady/src/auth/Ownable.sol"; 8 | import {IShipyardContractMetadata} from "../interfaces/IShipyardContractMetadata.sol"; 9 | 10 | contract ERC1155ShipyardContractMetadata is 11 | ERC1155ConduitPreapproved_Solady, 12 | IShipyardContractMetadata, 13 | ERC2981, 14 | Ownable 15 | { 16 | /// @dev The token name 17 | string internal _name; 18 | 19 | /// @dev The token symbol 20 | string internal _symbol; 21 | 22 | /// @dev The base URI. 23 | string public baseURI; 24 | 25 | /// @dev The contract URI. 26 | string public contractURI; 27 | 28 | /// @dev The provenance hash for guaranteeing metadata order for random reveals. 29 | bytes32 public provenanceHash; 30 | 31 | constructor(string memory name_, string memory symbol_) ERC1155ConduitPreapproved_Solady() { 32 | // Set the token name and symbol. 33 | _name = name_; 34 | _symbol = symbol_; 35 | 36 | // Initialize the owner of the contract. 37 | _initializeOwner(msg.sender); 38 | } 39 | 40 | /** 41 | * @notice Returns the name of this token contract. 42 | */ 43 | function name() public view returns (string memory) { 44 | return _name; 45 | } 46 | 47 | /** 48 | * @notice Returns the symbol of this token contract. 49 | */ 50 | function symbol() public view returns (string memory) { 51 | return _symbol; 52 | } 53 | 54 | /** 55 | * @notice Sets the base URI for the token metadata and emits an event. 56 | * 57 | * @param newURI The new base URI to set. 58 | */ 59 | function setBaseURI(string calldata newURI) external onlyOwner { 60 | baseURI = newURI; 61 | 62 | // Emit an event with the update. 63 | emit BatchMetadataUpdate(0, type(uint256).max); 64 | } 65 | 66 | /** 67 | * @notice Sets the contract URI for contract metadata. 68 | * 69 | * @param newURI The new contract URI. 70 | */ 71 | function setContractURI(string calldata newURI) external onlyOwner { 72 | // Set the new contract URI. 73 | contractURI = newURI; 74 | 75 | // Emit an event with the update. 76 | emit ContractURIUpdated(); 77 | } 78 | 79 | /** 80 | * @notice Sets the provenance hash and emits an event. 81 | * 82 | * The provenance hash is used for random reveals, which 83 | * is a hash of the ordered metadata to show it has not been 84 | * modified after mint started. 85 | * 86 | * This function will revert if the provenance hash has already 87 | * been set, so be sure to carefully set it only once. 88 | * 89 | * @param newProvenanceHash The new provenance hash to set. 90 | */ 91 | function setProvenanceHash(bytes32 newProvenanceHash) external onlyOwner { 92 | // Keep track of the old provenance hash for emitting with the event. 93 | bytes32 oldProvenanceHash = provenanceHash; 94 | 95 | // Revert if the provenance hash has already been set. 96 | if (oldProvenanceHash != bytes32(0)) { 97 | revert ProvenanceHashCannotBeSetAfterAlreadyBeingSet(); 98 | } 99 | 100 | // Set the new provenance hash. 101 | provenanceHash = newProvenanceHash; 102 | 103 | // Emit an event with the update. 104 | emit ProvenanceHashUpdated(oldProvenanceHash, newProvenanceHash); 105 | } 106 | 107 | /** 108 | * @notice Returns the URI for token metadata. 109 | * 110 | * This implementation returns the same URI for *all* token types. 111 | * It relies on the token type ID substitution mechanism defined 112 | * in the EIP to replace {id} with the token id. 113 | * 114 | * @custom:param tokenId The token id to get the URI for. 115 | */ 116 | function uri(uint256 /* tokenId */ ) public view virtual override returns (string memory) { 117 | // Return the base URI. 118 | return baseURI; 119 | } 120 | 121 | /** 122 | * @notice Sets the default royalty information. 123 | * 124 | * Requirements: 125 | * 126 | * - `receiver` cannot be the zero address. 127 | * - `feeNumerator` cannot be greater than the fee denominator of 10_000 basis points. 128 | */ 129 | function setDefaultRoyalty(address receiver, uint96 feeNumerator) external onlyOwner { 130 | // Set the default royalty. 131 | // ERC2981 implementation ensures feeNumerator <= feeDenominator 132 | // and receiver != address(0). 133 | _setDefaultRoyalty(receiver, feeNumerator); 134 | 135 | // Emit an event with the updated params. 136 | emit RoyaltyInfoUpdated(receiver, feeNumerator); 137 | } 138 | 139 | function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155, ERC2981) returns (bool) { 140 | return ERC1155.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/lib/ERC721ShipyardContractMetadata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC721ConduitPreapproved_Solady} from "shipyard-core/src/tokens/erc721/ERC721ConduitPreapproved_Solady.sol"; 5 | import {ERC721} from "solady/src/tokens/ERC721.sol"; 6 | import {ERC2981} from "solady/src/tokens/ERC2981.sol"; 7 | import {Ownable} from "solady/src/auth/Ownable.sol"; 8 | import {IShipyardContractMetadata} from "../interfaces/IShipyardContractMetadata.sol"; 9 | 10 | contract ERC721ShipyardContractMetadata is 11 | ERC721ConduitPreapproved_Solady, 12 | IShipyardContractMetadata, 13 | ERC2981, 14 | Ownable 15 | { 16 | /// @dev The token name 17 | string internal _name; 18 | 19 | /// @dev The token symbol 20 | string internal _symbol; 21 | 22 | /// @dev The base URI. 23 | string public baseURI; 24 | 25 | /// @dev The contract URI. 26 | string public contractURI; 27 | 28 | /// @dev The provenance hash for guaranteeing metadata order for random reveals. 29 | bytes32 public provenanceHash; 30 | 31 | constructor(string memory name_, string memory symbol_) ERC721ConduitPreapproved_Solady() { 32 | // Set the token name and symbol. 33 | _name = name_; 34 | _symbol = symbol_; 35 | 36 | // Initialize the owner of the contract. 37 | _initializeOwner(msg.sender); 38 | } 39 | 40 | /** 41 | * @notice Returns the name of this token contract. 42 | */ 43 | function name() public view override(ERC721, IShipyardContractMetadata) returns (string memory) { 44 | return _name; 45 | } 46 | 47 | /** 48 | * @notice Returns the symbol of this token contract. 49 | */ 50 | function symbol() public view override(ERC721, IShipyardContractMetadata) returns (string memory) { 51 | return _symbol; 52 | } 53 | 54 | /** 55 | * @notice Sets the base URI for the token metadata and emits an event. 56 | * 57 | * @param newURI The new base URI to set. 58 | */ 59 | function setBaseURI(string calldata newURI) external onlyOwner { 60 | baseURI = newURI; 61 | 62 | // Emit an event with the update. 63 | emit BatchMetadataUpdate(0, type(uint256).max); 64 | } 65 | 66 | /** 67 | * @notice Sets the contract URI for contract metadata. 68 | * 69 | * @param newURI The new contract URI. 70 | */ 71 | function setContractURI(string calldata newURI) external onlyOwner { 72 | // Set the new contract URI. 73 | contractURI = newURI; 74 | 75 | // Emit an event with the update. 76 | emit ContractURIUpdated(); 77 | } 78 | 79 | /** 80 | * @notice Sets the provenance hash and emits an event. 81 | * 82 | * The provenance hash is used for random reveals, which 83 | * is a hash of the ordered metadata to show it has not been 84 | * modified after mint started. 85 | * 86 | * This function will revert if the provenance hash has already 87 | * been set, so be sure to carefully set it only once. 88 | * 89 | * @param newProvenanceHash The new provenance hash to set. 90 | */ 91 | function setProvenanceHash(bytes32 newProvenanceHash) external onlyOwner { 92 | // Keep track of the old provenance hash for emitting with the event. 93 | bytes32 oldProvenanceHash = provenanceHash; 94 | 95 | // Revert if the provenance hash has already been set. 96 | if (oldProvenanceHash != bytes32(0)) { 97 | revert ProvenanceHashCannotBeSetAfterAlreadyBeingSet(); 98 | } 99 | 100 | // Set the new provenance hash. 101 | provenanceHash = newProvenanceHash; 102 | 103 | // Emit an event with the update. 104 | emit ProvenanceHashUpdated(oldProvenanceHash, newProvenanceHash); 105 | } 106 | 107 | /** 108 | * @notice Returns the token URI for token metadata. 109 | * 110 | * @param tokenId The token id to get the token URI for. 111 | */ 112 | function tokenURI(uint256 tokenId) public view virtual override returns (string memory uri) { 113 | // Revert if the tokenId doesn't exist. 114 | if (!_exists(tokenId)) revert TokenDoesNotExist(); 115 | 116 | // Put the baseURI on the stack. 117 | uri = baseURI; 118 | 119 | // Return empty if baseURI is empty. 120 | if (bytes(uri).length == 0) { 121 | return ""; 122 | } 123 | 124 | // If the last character of the baseURI is not a slash, then return 125 | // the baseURI to signal the same metadata for all tokens, such as 126 | // for a prereveal state. 127 | if (bytes(uri)[bytes(uri).length - 1] != bytes("/")[0]) { 128 | return uri; 129 | } 130 | 131 | // Append the tokenId to the baseURI and return. 132 | uri = string.concat(uri, _toString(tokenId)); 133 | } 134 | 135 | /** 136 | * @notice Sets the default royalty information. 137 | * 138 | * Requirements: 139 | * 140 | * - `receiver` cannot be the zero address. 141 | * - `feeNumerator` cannot be greater than the fee denominator of 10_000 basis points. 142 | */ 143 | function setDefaultRoyalty(address receiver, uint96 feeNumerator) external onlyOwner { 144 | // Set the default royalty. 145 | // ERC2981 implementation ensures feeNumerator <= feeDenominator 146 | // and receiver != address(0). 147 | _setDefaultRoyalty(receiver, feeNumerator); 148 | 149 | // Emit an event with the updated params. 150 | emit RoyaltyInfoUpdated(receiver, feeNumerator); 151 | } 152 | 153 | function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, ERC2981) returns (bool) { 154 | return ERC721.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId); 155 | } 156 | 157 | /** 158 | * @dev Converts a uint256 to its ASCII string decimal representation. 159 | */ 160 | function _toString(uint256 value) internal pure virtual returns (string memory str) { 161 | assembly { 162 | // The maximum value of a uint256 contains 78 digits (1 byte per digit), but 163 | // we allocate 0xa0 bytes to keep the free memory pointer 32-byte word aligned. 164 | // We will need 1 word for the trailing zeros padding, 1 word for the length, 165 | // and 3 words for a maximum of 78 digits. Total: 5 * 0x20 = 0xa0. 166 | let m := add(mload(0x40), 0xa0) 167 | // Update the free memory pointer to allocate. 168 | mstore(0x40, m) 169 | // Assign the `str` to the end. 170 | str := sub(m, 0x20) 171 | // Zeroize the slot after the string. 172 | mstore(str, 0) 173 | 174 | // Cache the end of the memory to calculate the length later. 175 | let end := str 176 | 177 | // We write the string from rightmost digit to leftmost digit. 178 | // The following is essentially a do-while loop that also handles the zero case. 179 | // prettier-ignore 180 | for { let temp := value } 1 {} { 181 | str := sub(str, 1) 182 | // Write the character to the pointer. 183 | // The ASCII index of the '0' character is 48. 184 | mstore8(str, add(48, mod(temp, 10))) 185 | // Keep dividing `temp` until zero. 186 | temp := div(temp, 10) 187 | // prettier-ignore 188 | if iszero(temp) { break } 189 | } 190 | 191 | let length := sub(end, str) 192 | // Move the pointer 32 bytes leftwards to make room for the length. 193 | str := sub(str, 0x20) 194 | // Store the length. 195 | mstore(str, length) 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/lib/RedeemablesConstants.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | address constant BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD; 5 | -------------------------------------------------------------------------------- /src/lib/RedeemablesErrors.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 5 | import {CampaignParams} from "./RedeemablesStructs.sol"; 6 | 7 | interface RedeemablesErrors { 8 | /// Configuration errors 9 | error NotManager(); 10 | error InvalidTime(); 11 | error ConsiderationItemRecipientCannotBeZeroAddress(); 12 | error ConsiderationItemAmountCannotBeZero(); 13 | error NonMatchingConsiderationItemAmounts(uint256 itemIndex, uint256 startAmount, uint256 endAmount); 14 | 15 | /// Redemption errors 16 | error InvalidCampaignId(); 17 | error CampaignAlreadyExists(); 18 | error InvalidCaller(address caller); 19 | error NotActive_(uint256 currentTimestamp, uint256 startTime, uint256 endTime); 20 | error MaxRedemptionsReached(uint256 total, uint256 max); 21 | error MaxCampaignRedemptionsReached(uint256 total, uint256 max); 22 | error NativeTransferFailed(); 23 | error InvalidOfferLength(uint256 got, uint256 want); 24 | error InvalidNativeOfferItem(); 25 | error InvalidOwner(); 26 | error InvalidRequiredTraitValue( 27 | address token, uint256 tokenId, bytes32 traitKey, bytes32 gotTraitValue, bytes32 wantTraitValue 28 | ); 29 | error InvalidTraitRedemption(); 30 | error InvalidTraitRedemptionToken(address token); 31 | error ConsiderationRecipientNotFound(address token); 32 | error RedemptionValuesAreImmutable(); 33 | error RequirementsIndexOutOfBounds(); 34 | error ConsiderationItemInsufficientBalance(address token, uint256 balance, uint256 amount); 35 | error EtherTransferFailed(); 36 | error InvalidTxValue(uint256 got, uint256 want); 37 | error InvalidConsiderationTokenIdSupplied(address token, uint256 got, uint256 want); 38 | error ConsiderationTokenIdsDontMatchConsiderationLength(uint256 considerationLength, uint256 tokenIdsLength); 39 | error TraitRedemptionTokenIdsDontMatchTraitRedemptionsLength(uint256 traitRedemptionsLength, uint256 tokenIdsLength); 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/RedeemablesStructs.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 5 | 6 | struct Campaign { 7 | CampaignParams params; 8 | CampaignRequirements[] requirements; 9 | } 10 | 11 | struct CampaignParams { 12 | uint32 startTime; 13 | uint32 endTime; 14 | uint32 maxCampaignRedemptions; 15 | address manager; 16 | address signer; 17 | } 18 | 19 | struct CampaignRequirements { 20 | OfferItem[] offer; 21 | ConsiderationItem[] consideration; 22 | TraitRedemption[] traitRedemptions; 23 | } 24 | 25 | struct TraitRedemption { 26 | uint8 substandard; 27 | address token; 28 | bytes32 traitKey; 29 | bytes32 traitValue; 30 | bytes32 substandardValue; 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/SignedRedeem.sol.txt: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Ownable} from "solady/src/auth/Ownable.sol"; 5 | import {SignatureCheckerLib} from "solady/src/utils/SignatureCheckerLib.sol"; 6 | import {SignedRedeemErrorsAndEvents} from "./SignedRedeemErrorsAndEvents.sol"; 7 | 8 | contract SignedRedeem is Ownable, SignedRedeemErrorsAndEvents { 9 | /// @dev Signer approval to redeem tokens (e.g. KYC), required when set. 10 | address internal _redeemSigner; 11 | 12 | /// @dev The used digests, each digest can only be used once. 13 | mapping(bytes32 => bool) internal _usedDigests; 14 | 15 | /// @notice Internal constants for EIP-712: Typed structured 16 | /// data hashing and signing 17 | bytes32 internal constant _SIGNED_REDEEM_TYPEHASH = 18 | keccak256("SignedRedeem(address owner,uint256[] tokenIds,uint256 salt)"); 19 | bytes32 internal constant _EIP_712_DOMAIN_TYPEHASH = 20 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); 21 | bytes32 internal constant _NAME_HASH = keccak256("SignedRedeem"); 22 | bytes32 internal constant _VERSION_HASH = keccak256("1.0"); 23 | uint256 internal immutable _CHAIN_ID = block.chainid; 24 | bytes32 internal immutable _DOMAIN_SEPARATOR; 25 | 26 | constructor() { 27 | _initializeOwner(msg.sender); 28 | _DOMAIN_SEPARATOR = _deriveDomainSeparator(); 29 | } 30 | 31 | function updateSigner(address newSigner) public onlyOwner { 32 | _redeemSigner = newSigner; 33 | } 34 | 35 | function _verifySignatureAndRecordDigest( 36 | address owner, 37 | uint256[] calldata tokenIds, 38 | uint256 salt, 39 | bytes calldata signature 40 | ) internal { 41 | // Get the digest. 42 | bytes32 digest = _getDigest(owner, tokenIds, salt); 43 | 44 | // Revert if signature does not recover to signer. 45 | if (!SignatureCheckerLib.isValidSignatureNowCalldata(_redeemSigner, digest, signature)) revert InvalidSigner(); 46 | 47 | // Revert if the digest is already used. 48 | if (_usedDigests[digest]) revert DigestAlreadyUsed(); 49 | 50 | // Record digest as used. 51 | _usedDigests[digest] = true; 52 | } 53 | 54 | /* 55 | * @notice Verify an EIP-712 signature by recreating the data structure 56 | * that we signed on the client side, and then using that to recover 57 | * the address that signed the signature for this data. 58 | */ 59 | function _getDigest(address owner, uint256[] calldata tokenIds, uint256 salt) 60 | internal 61 | view 62 | returns (bytes32 digest) 63 | { 64 | digest = keccak256( 65 | bytes.concat( 66 | bytes2(0x1901), 67 | _domainSeparator(), 68 | keccak256(abi.encode(_SIGNED_REDEEM_TYPEHASH, owner, tokenIds, salt)) 69 | ) 70 | ); 71 | } 72 | 73 | /** 74 | * @dev Internal view function to get the EIP-712 domain separator. If the 75 | * chainId matches the chainId set on deployment, the cached domain 76 | * separator will be returned; otherwise, it will be derived from 77 | * scratch. 78 | * 79 | * @return The domain separator. 80 | */ 81 | function _domainSeparator() internal view returns (bytes32) { 82 | return block.chainid == _CHAIN_ID ? _DOMAIN_SEPARATOR : _deriveDomainSeparator(); 83 | } 84 | 85 | /** 86 | * @dev Internal view function to derive the EIP-712 domain separator. 87 | * 88 | * @return The derived domain separator. 89 | */ 90 | function _deriveDomainSeparator() internal view returns (bytes32) { 91 | return keccak256(abi.encode(_EIP_712_DOMAIN_TYPEHASH, _NAME_HASH, _VERSION_HASH, block.chainid, address(this))); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/lib/SignedRedeemContractOfferer.sol.txt: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 5 | import {SignatureCheckerLib} from "solady/src/utils/SignatureCheckerLib.sol"; 6 | import {SignedRedeemErrorsAndEvents} from "./SignedRedeemErrorsAndEvents.sol"; 7 | 8 | contract SignedRedeemContractOfferer is SignedRedeemErrorsAndEvents { 9 | /// @dev The used digests, each digest can only be used once. 10 | mapping(bytes32 => bool) internal _usedDigests; 11 | 12 | /// @notice Internal constants for EIP-712: Typed structured 13 | /// data hashing and signing 14 | bytes32 internal constant _SIGNED_REDEEM_TYPEHASH = 15 | keccak256("SignedRedeem(address owner,address token,uint256[] tokenIds,bytes32 redemptionHash,uint256 salt)"); 16 | bytes32 internal constant _EIP_712_DOMAIN_TYPEHASH = 17 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); 18 | bytes32 internal constant _NAME_HASH = keccak256("SignedRedeem"); 19 | bytes32 internal constant _VERSION_HASH = keccak256("1.0"); 20 | uint256 internal immutable _CHAIN_ID = block.chainid; 21 | bytes32 internal immutable _DOMAIN_SEPARATOR; 22 | 23 | constructor() { 24 | _DOMAIN_SEPARATOR = _deriveDomainSeparator(); 25 | } 26 | 27 | function _verifySignature( 28 | address signer, 29 | address owner, 30 | SpentItem[] memory maximumSpent, 31 | bytes32 redemptionHash, 32 | uint256 salt, 33 | bytes memory signature, 34 | bool recordDigest 35 | ) internal { 36 | // Get the digest. 37 | bytes32 digest = _getDigest(owner, maximumSpent, redemptionHash, salt); 38 | 39 | // Revert if signature does not recover to signer. 40 | if (!SignatureCheckerLib.isValidSignatureNow(signer, digest, signature)) revert InvalidSigner(); 41 | 42 | // Revert if the digest is already used. 43 | if (_usedDigests[digest]) revert DigestAlreadyUsed(); 44 | 45 | // Record digest as used. 46 | if (recordDigest) _usedDigests[digest] = true; 47 | } 48 | 49 | /* 50 | * @notice Verify an EIP-712 signature by recreating the data structure 51 | * that we signed on the client side, and then using that to recover 52 | * the address that signed the signature for this data. 53 | */ 54 | function _getDigest(address owner, SpentItem[] memory maximumSpent, bytes32 redemptionHash, uint256 salt) 55 | internal 56 | view 57 | returns (bytes32 digest) 58 | { 59 | digest = keccak256( 60 | bytes.concat( 61 | bytes2(0x1901), 62 | _domainSeparator(), 63 | keccak256(abi.encode(_SIGNED_REDEEM_TYPEHASH, owner, maximumSpent, redemptionHash, salt)) 64 | ) 65 | ); 66 | } 67 | 68 | /** 69 | * @dev Internal view function to get the EIP-712 domain separator. If the 70 | * chainId matches the chainId set on deployment, the cached domain 71 | * separator will be returned; otherwise, it will be derived from 72 | * scratch. 73 | * 74 | * @return The domain separator. 75 | */ 76 | function _domainSeparator() internal view returns (bytes32) { 77 | return block.chainid == _CHAIN_ID ? _DOMAIN_SEPARATOR : _deriveDomainSeparator(); 78 | } 79 | 80 | /** 81 | * @dev Internal view function to derive the EIP-712 domain separator. 82 | * 83 | * @return The derived domain separator. 84 | */ 85 | function _deriveDomainSeparator() internal view returns (bytes32) { 86 | return keccak256(abi.encode(_EIP_712_DOMAIN_TYPEHASH, _NAME_HASH, _VERSION_HASH, block.chainid, address(this))); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/lib/SignedRedeemErrorsAndEvents.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | interface SignedRedeemErrorsAndEvents { 5 | error InvalidSigner(); 6 | error DigestAlreadyUsed(); 7 | } 8 | -------------------------------------------------------------------------------- /src/test/ERC1155SeaDropRedeemableOwnerMintable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC1155SeaDropRedeemable} from "../ERC1155SeaDropRedeemable.sol"; 5 | 6 | contract ERC1155SeaDropRedeemableOwnerMintable is ERC1155SeaDropRedeemable { 7 | constructor(address allowedConfigurer, address allowedSeaport, string memory name_, string memory symbol_) 8 | ERC1155SeaDropRedeemable(allowedConfigurer, allowedSeaport, name_, symbol_) 9 | {} 10 | 11 | function mint(address to, uint256 tokenId, uint256 amount) public onlyOwner { 12 | _mint(to, tokenId, amount, ""); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/ERC1155ShipyardRedeemableOwnerMintable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC1155ShipyardRedeemable} from "../ERC1155ShipyardRedeemable.sol"; 5 | 6 | contract ERC1155ShipyardRedeemableOwnerMintable is ERC1155ShipyardRedeemable { 7 | constructor(string memory name_, string memory symbol_) ERC1155ShipyardRedeemable(name_, symbol_) {} 8 | 9 | function mint(address to, uint256 tokenId, uint256 amount) public onlyOwner { 10 | _mint(to, tokenId, amount, ""); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/test/ERC721SeaDropRedeemableOwnerMintable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC721SeaDropRedeemable} from "../ERC721SeaDropRedeemable.sol"; 5 | 6 | contract ERC721SeaDropRedeemableOwnerMintable is ERC721SeaDropRedeemable { 7 | constructor(address allowedConfigurer, address allowedSeaport, string memory name_, string memory symbol_) 8 | ERC721SeaDropRedeemable(allowedConfigurer, allowedSeaport, name_, symbol_) 9 | {} 10 | 11 | function mint(address to, uint256 tokenId) public onlyOwner { 12 | _mint(to, tokenId); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/ERC721ShipyardRedeemableOwnerMintable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC721ShipyardRedeemable} from "../ERC721ShipyardRedeemable.sol"; 5 | 6 | contract ERC721ShipyardRedeemableOwnerMintable is ERC721ShipyardRedeemable { 7 | constructor(string memory name_, string memory symbol_) ERC721ShipyardRedeemable(name_, symbol_) {} 8 | 9 | function mint(address to, uint256 tokenId) public onlyOwner { 10 | _mint(to, tokenId); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/test/ERC721ShipyardRedeemableOwnerMintableWithoutInternalBurn.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC7498NFTRedeemables} from "../lib/ERC7498NFTRedeemables.sol"; 5 | import {ERC721ShipyardRedeemableOwnerMintable} from "./ERC721ShipyardRedeemableOwnerMintable.sol"; 6 | 7 | contract ERC721ShipyardRedeemableOwnerMintableWithoutInternalBurn is ERC721ShipyardRedeemableOwnerMintable { 8 | constructor(string memory name_, string memory symbol_) ERC721ShipyardRedeemableOwnerMintable(name_, symbol_) {} 9 | 10 | function _useInternalBurn() internal pure virtual override returns (bool) { 11 | // For coverage of ERC7498NFTRedeemables._useInternalBurn, return default value of false. 12 | return ERC7498NFTRedeemables._useInternalBurn(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/ERC721ShipyardRedeemableTraitSetters.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC721ShipyardRedeemableOwnerMintable} from "./ERC721ShipyardRedeemableOwnerMintable.sol"; 5 | import {ERC7498NFTRedeemables} from "../lib/ERC7498NFTRedeemables.sol"; 6 | import {DynamicTraits} from "shipyard-core/src/dynamic-traits/DynamicTraits.sol"; 7 | import {CampaignParams} from "../lib/RedeemablesStructs.sol"; 8 | 9 | contract ERC721ShipyardRedeemableTraitSetters is ERC721ShipyardRedeemableOwnerMintable { 10 | // TODO add the `allowedTraitSetters` logic to DynamicTraits.sol contract in shipyard-core 11 | // with getAllowedTraitSetters() and setAllowedTraitSetters(). add `is DynamicTraits` to 12 | // ERC721ShipyardRedeemable and ERC721SeaDropRedeemable contracts with onlyOwner on setAllowedTraitSetters(). 13 | address[] _allowedTraitSetters; 14 | 15 | constructor(string memory name_, string memory symbol_) ERC721ShipyardRedeemableOwnerMintable(name_, symbol_) {} 16 | 17 | function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) public virtual override { 18 | if (!_exists(tokenId)) revert TokenDoesNotExist(); 19 | 20 | _requireAllowedTraitSetter(); 21 | 22 | DynamicTraits.setTrait(tokenId, traitKey, value); 23 | } 24 | 25 | function getAllowedTraitSetters() public view returns (address[] memory) { 26 | return _allowedTraitSetters; 27 | } 28 | 29 | function setAllowedTraitSetters(address[] memory allowedTraitSetters) public onlyOwner { 30 | _allowedTraitSetters = allowedTraitSetters; 31 | } 32 | 33 | function _requireAllowedTraitSetter() internal view { 34 | // Allow the contract to call itself. 35 | if (msg.sender == address(this)) return; 36 | 37 | bool validCaller; 38 | for (uint256 i; i < _allowedTraitSetters.length; i++) { 39 | if (_allowedTraitSetters[i] == msg.sender) { 40 | validCaller = true; 41 | } 42 | } 43 | if (!validCaller) revert InvalidCaller(msg.sender); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/ERC1155ShipyardRedeemable.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Solarray} from "solarray/Solarray.sol"; 5 | import {BaseRedeemablesTest} from "./utils/BaseRedeemablesTest.sol"; 6 | import {ERC1155} from "solady/src/tokens/ERC1155.sol"; 7 | import {ERC1155ShipyardRedeemableOwnerMintable} from "../src/test/ERC1155ShipyardRedeemableOwnerMintable.sol"; 8 | 9 | contract TestERC1155ShipyardRedeemable is BaseRedeemablesTest { 10 | event TransferBatch( 11 | address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] amounts 12 | ); 13 | 14 | function testBurn() public { 15 | uint256 tokenId = 1; 16 | ERC1155ShipyardRedeemableOwnerMintable token = new ERC1155ShipyardRedeemableOwnerMintable("Test", "TEST"); 17 | address fred = makeAddr("fred"); 18 | _mintToken(address(token), tokenId, fred); 19 | 20 | vm.expectRevert(ERC1155.InsufficientBalance.selector); 21 | token.burn(address(this), tokenId, 1); 22 | 23 | vm.expectEmit(true, true, true, true); 24 | emit TransferSingle(fred, fred, address(0), tokenId, 1); 25 | vm.prank(fred); 26 | token.burn(fred, tokenId, 1); 27 | 28 | vm.prank(fred); 29 | vm.expectRevert(ERC1155.InsufficientBalance.selector); 30 | token.burn(fred, tokenId + 1, 1); 31 | 32 | _mintToken(address(token), tokenId + 1, fred); 33 | _mintToken(address(token), tokenId + 2, fred); 34 | 35 | uint256[] memory ids = Solarray.uint256s(tokenId + 1, tokenId + 2); 36 | uint256[] memory amounts = Solarray.uint256s(1, 1); 37 | vm.expectRevert(ERC1155.NotOwnerNorApproved.selector); 38 | token.batchBurn(fred, ids, amounts); 39 | 40 | vm.prank(fred); 41 | token.setApprovalForAll(address(this), true); 42 | 43 | vm.expectEmit(true, true, true, true); 44 | emit TransferBatch(address(this), fred, address(0), ids, amounts); 45 | token.batchBurn(fred, ids, amounts); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/ERC721ShipyardRedeemable.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {ERC721} from "solady/src/tokens/ERC721.sol"; 6 | import {ERC721ShipyardRedeemableOwnerMintable} from "../src/test/ERC721ShipyardRedeemableOwnerMintable.sol"; 7 | 8 | contract TestERC721ShipyardRedeemable is Test { 9 | // This Transfer event is different than the one in BaseOrderTest, since `id` is indexed. 10 | // We don't inherit BaseRedeemablesTest to avoid this conflict. 11 | // For more details see https://github.com/ethereum/solidity/issues/4168#issuecomment-1819912098 12 | event Transfer(address indexed from, address indexed to, uint256 indexed id); 13 | 14 | function testBurn() public { 15 | uint256 tokenId = 1; 16 | ERC721ShipyardRedeemableOwnerMintable token = new ERC721ShipyardRedeemableOwnerMintable("Test", "TEST"); 17 | address fred = makeAddr("fred"); 18 | _mintToken(address(token), tokenId, fred); 19 | 20 | vm.expectRevert(ERC721.NotOwnerNorApproved.selector); 21 | token.burn(tokenId); 22 | 23 | vm.expectEmit(true, true, false, false); 24 | emit Transfer(fred, address(0), tokenId); 25 | vm.prank(fred); 26 | token.burn(tokenId); 27 | 28 | vm.expectRevert(ERC721.TokenDoesNotExist.selector); 29 | token.burn(tokenId + 1); 30 | 31 | _mintToken(address(token), tokenId + 1, fred); 32 | vm.expectRevert(ERC721.NotOwnerNorApproved.selector); 33 | token.burn(tokenId + 1); 34 | 35 | vm.prank(fred); 36 | token.setApprovalForAll(address(this), true); 37 | 38 | vm.expectEmit(true, true, false, false); 39 | emit Transfer(fred, address(0), tokenId + 1); 40 | token.burn(tokenId + 1); 41 | } 42 | 43 | function _mintToken(address token, uint256 tokenId, address recipient) internal { 44 | ERC721ShipyardRedeemableOwnerMintable(address(token)).mint(recipient, tokenId); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/ERC7498-GetAndUpdateCampaign.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {BaseRedeemablesTest} from "./utils/BaseRedeemablesTest.sol"; 5 | import {Solarray} from "solarray/Solarray.sol"; 6 | import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 7 | import {OfferItemLib} from "seaport-sol/src/lib/OfferItemLib.sol"; 8 | import {ConsiderationItemLib} from "seaport-sol/src/lib/ConsiderationItemLib.sol"; 9 | import {IERC7498} from "../src/interfaces/IERC7498.sol"; 10 | import {BURN_ADDRESS} from "../src/lib/RedeemablesConstants.sol"; 11 | import {Campaign, CampaignParams, CampaignRequirements} from "../src/lib/RedeemablesStructs.sol"; 12 | import {RedeemablesErrors} from "../src/lib/RedeemablesErrors.sol"; 13 | 14 | contract ERC7498_GetAndUpdateCampaign is BaseRedeemablesTest { 15 | using OfferItemLib for OfferItem; 16 | using OfferItemLib for OfferItem[]; 17 | using ConsiderationItemLib for ConsiderationItem; 18 | using ConsiderationItemLib for ConsiderationItem[]; 19 | 20 | function testGetAndUpdateCampaign() public { 21 | for (uint256 i; i < erc7498Tokens.length; i++) { 22 | testRedeemable(this.getAndUpdateCampaign, RedeemablesContext({erc7498Token: IERC7498(erc7498Tokens[i])})); 23 | } 24 | } 25 | 26 | function getAndUpdateCampaign(RedeemablesContext memory context) external { 27 | // Should revert if the campaign does not exist. 28 | for (uint256 i = 0; i < 3; i++) { 29 | vm.expectRevert(InvalidCampaignId.selector); 30 | context.erc7498Token.getCampaign(i); 31 | } 32 | 33 | ConsiderationItem[] memory consideration = new ConsiderationItem[](1); 34 | consideration[0] = _getCampaignConsiderationItem(address(context.erc7498Token)); 35 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 36 | requirements[0] = CampaignRequirements({ 37 | offer: defaultCampaignOffer, 38 | consideration: consideration, 39 | traitRedemptions: defaultTraitRedemptions 40 | }); 41 | CampaignParams memory params = CampaignParams({ 42 | startTime: uint32(block.timestamp), 43 | endTime: uint32(block.timestamp + 1000), 44 | maxCampaignRedemptions: 5, 45 | manager: address(this), 46 | signer: address(0) 47 | }); 48 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 49 | uint256 campaignId = context.erc7498Token.createCampaign(campaign, "test123"); 50 | 51 | (Campaign memory gotCampaign, string memory metadataURI, uint256 totalRedemptions) = 52 | IERC7498(context.erc7498Token).getCampaign(campaignId); 53 | assertEq(keccak256(abi.encode(gotCampaign)), keccak256(abi.encode(campaign))); 54 | assertEq(metadataURI, "test123"); 55 | assertEq(totalRedemptions, 0); 56 | 57 | // Should revert if the campaign does not exist. 58 | vm.expectRevert(InvalidCampaignId.selector); 59 | context.erc7498Token.getCampaign(campaignId + 1); 60 | 61 | // Should revert if trying to get campaign id 0, since it starts at 1. 62 | vm.expectRevert(InvalidCampaignId.selector); 63 | context.erc7498Token.getCampaign(0); 64 | 65 | // Should revert if updating an invalid campaign id. 66 | vm.expectRevert(InvalidCampaignId.selector); 67 | context.erc7498Token.updateCampaign(0, campaign, "test111"); 68 | vm.expectRevert(InvalidCampaignId.selector); 69 | context.erc7498Token.updateCampaign(campaignId + 1, campaign, "test111"); 70 | 71 | // Update the campaign. 72 | campaign.params.endTime = 0; 73 | campaign.params.manager = address(0); 74 | // Should expect revert with InvalidTime since endTime > startTime. 75 | vm.expectRevert(InvalidTime.selector); 76 | context.erc7498Token.updateCampaign(campaignId, campaign, "test456"); 77 | 78 | campaign.params.startTime = 0; 79 | context.erc7498Token.updateCampaign(campaignId, campaign, "test456"); 80 | 81 | (gotCampaign, metadataURI, totalRedemptions) = IERC7498(context.erc7498Token).getCampaign(campaignId); 82 | assertEq(keccak256(abi.encode(gotCampaign)), keccak256(abi.encode(campaign))); 83 | assertEq(metadataURI, "test456"); 84 | assertEq(totalRedemptions, 0); 85 | 86 | // Updating the campaign again should fail since the manager is now the null address. 87 | vm.expectRevert(NotManager.selector); 88 | context.erc7498Token.updateCampaign(campaignId, campaign, "test456"); 89 | } 90 | 91 | function testCampaignReverts() public { 92 | for (uint256 i; i < erc7498Tokens.length; i++) { 93 | testRedeemable(this.campaignReverts, RedeemablesContext({erc7498Token: IERC7498(erc7498Tokens[i])})); 94 | } 95 | } 96 | 97 | function campaignReverts(RedeemablesContext memory context) external { 98 | ConsiderationItem[] memory consideration = new ConsiderationItem[](1); 99 | consideration[0] = _getCampaignConsiderationItem(address(context.erc7498Token)); 100 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 101 | requirements[0] = CampaignRequirements({ 102 | offer: defaultCampaignOffer, 103 | consideration: consideration, 104 | traitRedemptions: defaultTraitRedemptions 105 | }); 106 | CampaignParams memory params = CampaignParams({ 107 | startTime: uint32(block.timestamp), 108 | endTime: uint32(block.timestamp + 1000), 109 | maxCampaignRedemptions: 5, 110 | manager: address(this), 111 | signer: address(0) 112 | }); 113 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 114 | 115 | consideration[0].recipient = payable(address(0)); 116 | vm.expectRevert(ConsiderationItemRecipientCannotBeZeroAddress.selector); 117 | context.erc7498Token.createCampaign(campaign, "test123"); 118 | consideration[0].recipient = payable(BURN_ADDRESS); 119 | 120 | consideration[0].startAmount = 0; 121 | consideration[0].endAmount = 0; 122 | vm.expectRevert(ConsiderationItemAmountCannotBeZero.selector); 123 | context.erc7498Token.createCampaign(campaign, "test123"); 124 | consideration[0].startAmount = 1; 125 | consideration[0].endAmount = 1; 126 | 127 | consideration[0].endAmount = 2; 128 | vm.expectRevert(abi.encodeWithSelector(NonMatchingConsiderationItemAmounts.selector, 0, 1, 2)); 129 | context.erc7498Token.createCampaign(campaign, "test123"); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /test/ERC7498-MultiRedeem.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {BaseRedeemablesTest} from "./utils/BaseRedeemablesTest.sol"; 5 | import {Solarray} from "solarray/Solarray.sol"; 6 | import {ERC721} from "solady/src/tokens/ERC721.sol"; 7 | import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 8 | import {ItemType, OrderType, Side} from "seaport-sol/src/SeaportEnums.sol"; 9 | import {OfferItemLib} from "seaport-sol/src/lib/OfferItemLib.sol"; 10 | import {ConsiderationItemLib} from "seaport-sol/src/lib/ConsiderationItemLib.sol"; 11 | import {IERC7498} from "../src/interfaces/IERC7498.sol"; 12 | import {Campaign, CampaignParams, CampaignRequirements} from "../src/lib/RedeemablesStructs.sol"; 13 | import {ERC721ShipyardRedeemableMintable} from "../src/extensions/ERC721ShipyardRedeemableMintable.sol"; 14 | import {ERC1155ShipyardRedeemableMintable} from "../src/extensions/ERC1155ShipyardRedeemableMintable.sol"; 15 | import {ERC721ShipyardRedeemableOwnerMintable} from "../src/test/ERC721ShipyardRedeemableOwnerMintable.sol"; 16 | import {ERC1155ShipyardRedeemableOwnerMintable} from "../src/test/ERC1155ShipyardRedeemableOwnerMintable.sol"; 17 | 18 | contract ERC7498_MultiRedeem is BaseRedeemablesTest { 19 | using OfferItemLib for OfferItem; 20 | using OfferItemLib for OfferItem[]; 21 | using ConsiderationItemLib for ConsiderationItem; 22 | using ConsiderationItemLib for ConsiderationItem[]; 23 | 24 | uint256 tokenId = 2; 25 | 26 | function testBurnMultiErc721OrErc1155RedeemSingleErc721() public { 27 | for (uint256 i; i < erc7498Tokens.length; i++) { 28 | testRedeemable( 29 | this.burnMultiErc721OrErc1155RedeemSingleErc721, 30 | RedeemablesContext({erc7498Token: IERC7498(erc7498Tokens[i])}) 31 | ); 32 | } 33 | } 34 | 35 | function burnMultiErc721OrErc1155RedeemSingleErc721(RedeemablesContext memory context) public { 36 | address secondRedeemTokenAddress; 37 | _mintToken(address(context.erc7498Token), tokenId); 38 | if (_isERC721(address(context.erc7498Token))) { 39 | ERC721ShipyardRedeemableOwnerMintable secondRedeemToken721 = 40 | new ERC721ShipyardRedeemableOwnerMintable("", ""); 41 | secondRedeemTokenAddress = address(secondRedeemToken721); 42 | vm.label(secondRedeemTokenAddress, "secondRedeemToken721"); 43 | secondRedeemToken721.setApprovalForAll(address(context.erc7498Token), true); 44 | } else { 45 | ERC1155ShipyardRedeemableOwnerMintable secondRedeemToken1155 = 46 | new ERC1155ShipyardRedeemableOwnerMintable("", ""); 47 | secondRedeemTokenAddress = address(secondRedeemToken1155); 48 | vm.label(secondRedeemTokenAddress, "secondRedeemToken1155"); 49 | secondRedeemToken1155.setApprovalForAll(address(context.erc7498Token), true); 50 | } 51 | _mintToken(secondRedeemTokenAddress, tokenId); 52 | 53 | ERC721ShipyardRedeemableMintable receiveToken = new ERC721ShipyardRedeemableMintable("", ""); 54 | receiveToken.setRedeemablesContracts(erc7498Tokens); 55 | ConsiderationItem[] memory consideration = new ConsiderationItem[](2); 56 | consideration[0] = _getCampaignConsiderationItem(address(context.erc7498Token)); 57 | consideration[1] = _getCampaignConsiderationItem(secondRedeemTokenAddress); 58 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 59 | OfferItem[] memory offer = new OfferItem[](1); 60 | offer[0] = defaultCampaignOffer[0].withToken(address(receiveToken)); 61 | requirements[0].offer = offer; 62 | requirements[0].consideration = consideration; 63 | CampaignParams memory params = CampaignParams({ 64 | startTime: uint32(block.timestamp), 65 | endTime: uint32(block.timestamp + 1000), 66 | maxCampaignRedemptions: 5, 67 | manager: address(this), 68 | signer: address(0) 69 | }); 70 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 71 | context.erc7498Token.createCampaign(campaign, ""); 72 | bytes memory extraData = abi.encode( 73 | 1, // campaignId 74 | 0, // requirementsIndex 75 | bytes32(0), // redemptionHash 76 | defaultTraitRedemptionTokenIds, 77 | uint256(0), // salt 78 | bytes("") // signature 79 | ); 80 | consideration[0].identifierOrCriteria = tokenId; 81 | uint256[] memory tokenIds = Solarray.uint256s(tokenId, tokenId); 82 | context.erc7498Token.redeem(tokenIds, address(this), extraData); 83 | 84 | _checkTokenDoesNotExist(address(context.erc7498Token), tokenId); 85 | _checkTokenDoesNotExist(secondRedeemTokenAddress, tokenId); 86 | assertEq(receiveToken.ownerOf(1), address(this)); 87 | assertEq(receiveToken.balanceOf(address(this)), 1); 88 | } 89 | 90 | function testBurnOneErc721OrErc1155RedeemMultiErc1155() public { 91 | for (uint256 i; i < erc7498Tokens.length; i++) { 92 | testRedeemable( 93 | this.burnOneErc721OrErc1155RedeemMultiErc1155, 94 | RedeemablesContext({erc7498Token: IERC7498(erc7498Tokens[i])}) 95 | ); 96 | } 97 | } 98 | 99 | function burnOneErc721OrErc1155RedeemMultiErc1155(RedeemablesContext memory context) public { 100 | _mintToken(address(context.erc7498Token), tokenId); 101 | ERC1155ShipyardRedeemableMintable receiveToken = new ERC1155ShipyardRedeemableMintable("", ""); 102 | ERC721(address(context.erc7498Token)).setApprovalForAll(address(receiveToken), true); 103 | OfferItem[] memory offer = new OfferItem[](3); 104 | offer[0] = OfferItem({ 105 | itemType: ItemType.ERC1155_WITH_CRITERIA, 106 | token: address(receiveToken), 107 | identifierOrCriteria: 0, 108 | startAmount: 1, 109 | endAmount: 1 110 | }); 111 | offer[1] = OfferItem({ 112 | itemType: ItemType.ERC1155_WITH_CRITERIA, 113 | token: address(receiveToken), 114 | identifierOrCriteria: 0, 115 | startAmount: 1, 116 | endAmount: 1 117 | }); 118 | offer[2] = OfferItem({ 119 | itemType: ItemType.ERC1155_WITH_CRITERIA, 120 | token: address(receiveToken), 121 | identifierOrCriteria: 0, 122 | startAmount: 1, 123 | endAmount: 1 124 | }); 125 | ConsiderationItem[] memory consideration = new ConsiderationItem[](1); 126 | consideration[0] = _getCampaignConsiderationItem(address(context.erc7498Token)); 127 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 128 | requirements[0].offer = offer; 129 | requirements[0].consideration = consideration; 130 | CampaignParams memory params = CampaignParams({ 131 | startTime: uint32(block.timestamp), 132 | endTime: uint32(block.timestamp + 1000), 133 | maxCampaignRedemptions: 5, 134 | manager: address(this), 135 | signer: address(0) 136 | }); 137 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 138 | IERC7498(receiveToken).createCampaign(campaign, ""); 139 | 140 | bytes memory extraData = abi.encode( 141 | 1, // campaignId 142 | 0, // requirementsIndex 143 | bytes32(0), // redemptionHash 144 | defaultTraitRedemptionTokenIds, 145 | uint256(0), // salt 146 | bytes("") // signature 147 | ); 148 | consideration[0].identifierOrCriteria = tokenId; 149 | uint256[] memory tokenIds = Solarray.uint256s(tokenId); 150 | IERC7498(receiveToken).redeem(tokenIds, address(this), extraData); 151 | 152 | _checkTokenDoesNotExist(address(context.erc7498Token), tokenId); 153 | assertEq(receiveToken.balanceOf(address(this), 1), 1); 154 | assertEq(receiveToken.balanceOf(address(this), 2), 1); 155 | assertEq(receiveToken.balanceOf(address(this), 3), 1); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /test/ERC7498-RedemptionMintable.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {BaseRedeemablesTest} from "./utils/BaseRedeemablesTest.sol"; 5 | import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 6 | import {OfferItemLib} from "seaport-sol/src/lib/OfferItemLib.sol"; 7 | import {ConsiderationItemLib} from "seaport-sol/src/lib/ConsiderationItemLib.sol"; 8 | import {IERC7498} from "../src/interfaces/IERC7498.sol"; 9 | import {IRedemptionMintable} from "../src/interfaces/IRedemptionMintable.sol"; 10 | 11 | contract TestERC7498_RedemptionMintable is BaseRedeemablesTest { 12 | using OfferItemLib for OfferItem; 13 | using OfferItemLib for OfferItem[]; 14 | using ConsiderationItemLib for ConsiderationItem; 15 | using ConsiderationItemLib for ConsiderationItem[]; 16 | 17 | function testSupportsInterfaceId() public { 18 | assertTrue(receiveToken721.supportsInterface(type(IRedemptionMintable).interfaceId)); 19 | assertTrue(receiveToken1155.supportsInterface(type(IRedemptionMintable).interfaceId)); 20 | 21 | assertTrue(receiveToken721.supportsInterface(type(IERC7498).interfaceId)); 22 | assertTrue(receiveToken1155.supportsInterface(type(IERC7498).interfaceId)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/ERC7498-Revert.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {BaseRedeemablesTest} from "./utils/BaseRedeemablesTest.sol"; 5 | import {Solarray} from "solarray/Solarray.sol"; 6 | import {ERC721} from "solady/src/tokens/ERC721.sol"; 7 | import {IERC7498} from "../src/interfaces/IERC7498.sol"; 8 | import {TestERC20} from "./utils/mocks/TestERC20.sol"; 9 | import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 10 | import {ItemType, OrderType, Side} from "seaport-sol/src/SeaportEnums.sol"; 11 | import {OfferItemLib} from "seaport-sol/src/lib/OfferItemLib.sol"; 12 | import {ConsiderationItemLib} from "seaport-sol/src/lib/ConsiderationItemLib.sol"; 13 | import {BURN_ADDRESS} from "../src/lib/RedeemablesConstants.sol"; 14 | import {Campaign, CampaignParams, CampaignRequirements, TraitRedemption} from "../src/lib/RedeemablesStructs.sol"; 15 | import {ERC721ShipyardRedeemableOwnerMintable} from "../src/test/ERC721ShipyardRedeemableOwnerMintable.sol"; 16 | 17 | contract ERC7498_Revert is BaseRedeemablesTest { 18 | using OfferItemLib for OfferItem; 19 | using OfferItemLib for OfferItem[]; 20 | using ConsiderationItemLib for ConsiderationItem; 21 | using ConsiderationItemLib for ConsiderationItem[]; 22 | 23 | uint256 tokenId = 2; 24 | 25 | function testRevert721ConsiderationItemInsufficientBalance() public { 26 | _mintToken(erc7498Tokens[0], tokenId); 27 | uint256 invalidTokenId = tokenId + 1; 28 | _mintToken(erc7498Tokens[0], invalidTokenId, dillon.addr); 29 | 30 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 31 | requirements[0] = CampaignRequirements({ 32 | offer: defaultCampaignOffer, 33 | consideration: defaultCampaignConsideration, 34 | traitRedemptions: defaultTraitRedemptions 35 | }); 36 | CampaignParams memory params = CampaignParams({ 37 | startTime: uint32(block.timestamp), 38 | endTime: uint32(block.timestamp + 1000), 39 | maxCampaignRedemptions: 5, 40 | manager: address(this), 41 | signer: address(0) 42 | }); 43 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 44 | IERC7498(erc7498Tokens[0]).createCampaign(campaign, ""); 45 | bytes memory extraData = abi.encode( 46 | 1, // campaignId 47 | 0, // requirementsIndex 48 | bytes32(0), // redemptionHash 49 | defaultTraitRedemptionTokenIds, 50 | uint256(0), // salt 51 | bytes("") // signature 52 | ); 53 | uint256[] memory tokenIds = Solarray.uint256s(invalidTokenId); 54 | 55 | vm.expectRevert( 56 | abi.encodeWithSelector( 57 | ConsiderationItemInsufficientBalance.selector, 58 | requirements[0].consideration[0].token, 59 | 0, 60 | requirements[0].consideration[0].startAmount 61 | ) 62 | ); 63 | IERC7498(erc7498Tokens[0]).redeem(tokenIds, address(this), extraData); 64 | 65 | assertEq(ERC721(erc7498Tokens[0]).ownerOf(tokenId), address(this)); 66 | vm.expectRevert(ERC721.TokenDoesNotExist.selector); 67 | receiveToken721.ownerOf(1); 68 | } 69 | 70 | function testRevertConsiderationLengthNotMet() public { 71 | _mintToken(erc7498Tokens[0], tokenId); 72 | ERC721ShipyardRedeemableOwnerMintable secondRedeemToken = new ERC721ShipyardRedeemableOwnerMintable("", ""); 73 | ConsiderationItem[] memory consideration = new ConsiderationItem[](2); 74 | consideration[0] = ConsiderationItem({ 75 | itemType: ItemType.ERC721_WITH_CRITERIA, 76 | token: address(erc7498Tokens[0]), 77 | identifierOrCriteria: 0, 78 | startAmount: 1, 79 | endAmount: 1, 80 | recipient: payable(BURN_ADDRESS) 81 | }); 82 | consideration[1] = ConsiderationItem({ 83 | itemType: ItemType.ERC721_WITH_CRITERIA, 84 | token: address(secondRedeemToken), 85 | identifierOrCriteria: 0, 86 | startAmount: 1, 87 | endAmount: 1, 88 | recipient: payable(BURN_ADDRESS) 89 | }); 90 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 91 | requirements[0].offer = defaultCampaignOffer; 92 | requirements[0].consideration = consideration; 93 | CampaignParams memory params = CampaignParams({ 94 | startTime: uint32(block.timestamp), 95 | endTime: uint32(block.timestamp + 1000), 96 | maxCampaignRedemptions: 5, 97 | manager: address(this), 98 | signer: address(0) 99 | }); 100 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 101 | IERC7498(erc7498Tokens[0]).createCampaign(campaign, ""); 102 | 103 | bytes memory extraData = abi.encode( 104 | 1, // campaignId 105 | 0, // requirementsIndex 106 | bytes32(0), // redemptionHash 107 | defaultTraitRedemptionTokenIds, 108 | uint256(0), // salt 109 | bytes("") // signature 110 | ); 111 | consideration[0].identifierOrCriteria = tokenId; 112 | uint256[] memory tokenIds = Solarray.uint256s(tokenId); 113 | 114 | vm.expectRevert(abi.encodeWithSelector(ConsiderationTokenIdsDontMatchConsiderationLength.selector, 2, 1)); 115 | IERC7498(erc7498Tokens[0]).redeem(tokenIds, address(this), extraData); 116 | 117 | assertEq(ERC721(erc7498Tokens[0]).ownerOf(tokenId), address(this)); 118 | vm.expectRevert(ERC721.TokenDoesNotExist.selector); 119 | receiveToken721.ownerOf(1); 120 | } 121 | 122 | function testRevertInvalidTxValue() public { 123 | _mintToken(erc7498Tokens[0], tokenId); 124 | OfferItem[] memory offer = new OfferItem[](1); 125 | offer[0] = OfferItem({ 126 | itemType: ItemType.ERC721_WITH_CRITERIA, 127 | token: address(receiveToken721), 128 | identifierOrCriteria: 0, 129 | startAmount: 1, 130 | endAmount: 1 131 | }); 132 | ConsiderationItem[] memory consideration = new ConsiderationItem[](2); 133 | consideration[0] = ConsiderationItem({ 134 | itemType: ItemType.ERC721_WITH_CRITERIA, 135 | token: address(erc7498Tokens[0]), 136 | identifierOrCriteria: 0, 137 | startAmount: 1, 138 | endAmount: 1, 139 | recipient: payable(BURN_ADDRESS) 140 | }); 141 | consideration[1] = ConsiderationItem({ 142 | itemType: ItemType.NATIVE, 143 | token: address(0), 144 | identifierOrCriteria: 0, 145 | startAmount: 0.1 ether, 146 | endAmount: 0.1 ether, 147 | recipient: payable(dillon.addr) 148 | }); 149 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 150 | requirements[0].offer = offer; 151 | requirements[0].consideration = consideration; 152 | CampaignParams memory params = CampaignParams({ 153 | startTime: uint32(block.timestamp), 154 | endTime: uint32(block.timestamp + 1000), 155 | maxCampaignRedemptions: 5, 156 | manager: address(this), 157 | signer: address(0) 158 | }); 159 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 160 | IERC7498(erc7498Tokens[0]).createCampaign(campaign, ""); 161 | 162 | bytes memory extraData = abi.encode( 163 | 1, // campaignId 164 | 0, // requirementsIndex 165 | bytes32(0), // redemptionHash 166 | defaultTraitRedemptionTokenIds, 167 | uint256(0), // salt 168 | bytes("") // signature 169 | ); 170 | consideration[0].identifierOrCriteria = tokenId; 171 | uint256[] memory considerationTokenIds = Solarray.uint256s(tokenId, 0); 172 | 173 | vm.expectRevert(abi.encodeWithSelector(InvalidTxValue.selector, 0.05 ether, 0.1 ether)); 174 | IERC7498(erc7498Tokens[0]).redeem{value: 0.05 ether}(considerationTokenIds, address(this), extraData); 175 | vm.expectRevert(ERC721.TokenDoesNotExist.selector); 176 | receiveToken721.ownerOf(1); 177 | assertEq(ERC721(erc7498Tokens[0]).ownerOf(tokenId), address(this)); 178 | } 179 | 180 | function testRevertErc20ConsiderationItemInsufficientBalance() public { 181 | _mintToken(erc7498Tokens[0], tokenId); 182 | TestERC20 redeemErc20 = new TestERC20(); 183 | redeemErc20.mint(address(this), 0.05 ether); 184 | OfferItem[] memory offer = new OfferItem[](1); 185 | offer[0] = OfferItem({ 186 | itemType: ItemType.ERC721_WITH_CRITERIA, 187 | token: address(receiveToken721), 188 | identifierOrCriteria: 0, 189 | startAmount: 1, 190 | endAmount: 1 191 | }); 192 | ConsiderationItem[] memory consideration = new ConsiderationItem[](2); 193 | consideration[0] = ConsiderationItem({ 194 | itemType: ItemType.ERC721_WITH_CRITERIA, 195 | token: address(erc7498Tokens[0]), 196 | identifierOrCriteria: 0, 197 | startAmount: 1, 198 | endAmount: 1, 199 | recipient: payable(BURN_ADDRESS) 200 | }); 201 | consideration[1] = ConsiderationItem({ 202 | itemType: ItemType.ERC20, 203 | token: address(redeemErc20), 204 | identifierOrCriteria: 0, 205 | startAmount: 0.1 ether, 206 | endAmount: 0.1 ether, 207 | recipient: payable(dillon.addr) 208 | }); 209 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 210 | requirements[0].offer = offer; 211 | requirements[0].consideration = consideration; 212 | CampaignParams memory params = CampaignParams({ 213 | startTime: uint32(block.timestamp), 214 | endTime: uint32(block.timestamp + 1000), 215 | maxCampaignRedemptions: 5, 216 | manager: address(this), 217 | signer: address(0) 218 | }); 219 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 220 | IERC7498(erc7498Tokens[0]).createCampaign(campaign, ""); 221 | 222 | bytes memory extraData = abi.encode( 223 | 1, // campaignId 224 | 0, // requirementsIndex 225 | bytes32(0), // redemptionHash 226 | defaultTraitRedemptionTokenIds, 227 | uint256(0), // salt 228 | bytes("") // signature 229 | ); 230 | consideration[0].identifierOrCriteria = tokenId; 231 | uint256[] memory considerationTokenIds = Solarray.uint256s(tokenId, 0); 232 | vm.expectRevert( 233 | abi.encodeWithSelector( 234 | ConsiderationItemInsufficientBalance.selector, address(redeemErc20), 0.05 ether, 0.1 ether 235 | ) 236 | ); 237 | IERC7498(erc7498Tokens[0]).redeem(considerationTokenIds, address(this), extraData); 238 | vm.expectRevert(ERC721.TokenDoesNotExist.selector); 239 | receiveToken721.ownerOf(1); 240 | assertEq(ERC721(erc7498Tokens[0]).ownerOf(tokenId), address(this)); 241 | } 242 | 243 | function testRevertErc721InvalidConsiderationTokenIdSupplied() public { 244 | uint256 considerationTokenId = 1; 245 | _mintToken(erc7498Tokens[0], tokenId); 246 | _mintToken(erc7498Tokens[0], considerationTokenId); 247 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 248 | ConsiderationItem[] memory consideration = new ConsiderationItem[](1); 249 | consideration[0] = defaultCampaignConsideration[0].withIdentifierOrCriteria(considerationTokenId); 250 | requirements[0] = CampaignRequirements({ 251 | offer: defaultCampaignOffer, 252 | consideration: consideration, 253 | traitRedemptions: defaultTraitRedemptions 254 | }); 255 | CampaignParams memory params = CampaignParams({ 256 | startTime: uint32(block.timestamp), 257 | endTime: uint32(block.timestamp + 1000), 258 | maxCampaignRedemptions: 5, 259 | manager: address(this), 260 | signer: address(0) 261 | }); 262 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 263 | IERC7498(erc7498Tokens[0]).createCampaign(campaign, ""); 264 | 265 | bytes memory extraData = abi.encode( 266 | 1, // campaignId 267 | 0, // requirementsIndex 268 | bytes32(0), // redemptionHash 269 | defaultTraitRedemptionTokenIds, 270 | uint256(0), // salt 271 | bytes("") // signature 272 | ); 273 | uint256[] memory considerationTokenIds = Solarray.uint256s(tokenId); 274 | vm.expectRevert( 275 | abi.encodeWithSelector( 276 | InvalidConsiderationTokenIdSupplied.selector, address(erc7498Tokens[0]), tokenId, considerationTokenId 277 | ) 278 | ); 279 | IERC7498(erc7498Tokens[0]).redeem(considerationTokenIds, address(this), extraData); 280 | vm.expectRevert(ERC721.TokenDoesNotExist.selector); 281 | receiveToken721.ownerOf(1); 282 | assertEq(ERC721(erc7498Tokens[0]).ownerOf(tokenId), address(this)); 283 | assertEq(ERC721(erc7498Tokens[0]).ownerOf(considerationTokenId), address(this)); 284 | } 285 | 286 | function testRevertErc1155InvalidConsiderationTokenIdSupplied() public { 287 | uint256 considerationTokenId = 1; 288 | _mintToken(address(erc1155s[0]), tokenId); 289 | _mintToken(address(erc1155s[0]), considerationTokenId); 290 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 291 | ConsiderationItem[] memory consideration = new ConsiderationItem[](1); 292 | consideration[0] = defaultCampaignConsideration[0].withToken(address(erc1155s[0])).withItemType( 293 | ItemType.ERC1155 294 | ).withIdentifierOrCriteria(considerationTokenId); 295 | requirements[0] = CampaignRequirements({ 296 | offer: defaultCampaignOffer, 297 | consideration: consideration, 298 | traitRedemptions: defaultTraitRedemptions 299 | }); 300 | CampaignParams memory params = CampaignParams({ 301 | startTime: uint32(block.timestamp), 302 | endTime: uint32(block.timestamp + 1000), 303 | maxCampaignRedemptions: 5, 304 | manager: address(this), 305 | signer: address(0) 306 | }); 307 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 308 | IERC7498(erc7498Tokens[0]).createCampaign(campaign, ""); 309 | 310 | bytes memory extraData = abi.encode( 311 | 1, // campaignId 312 | 0, // requirementsIndex 313 | bytes32(0), // redemptionHash 314 | defaultTraitRedemptionTokenIds, 315 | uint256(0), // salt 316 | bytes("") // signature 317 | ); 318 | uint256[] memory considerationTokenIds = Solarray.uint256s(tokenId); 319 | vm.expectRevert( 320 | abi.encodeWithSelector( 321 | InvalidConsiderationTokenIdSupplied.selector, address(erc1155s[0]), tokenId, considerationTokenId 322 | ) 323 | ); 324 | IERC7498(erc7498Tokens[0]).redeem(considerationTokenIds, address(this), extraData); 325 | vm.expectRevert(ERC721.TokenDoesNotExist.selector); 326 | receiveToken721.ownerOf(1); 327 | assertEq(erc1155s[0].balanceOf(address(this), tokenId), 1); 328 | assertEq(erc1155s[0].balanceOf(address(this), considerationTokenId), 1); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /test/ERC7498-SimpleRedeem.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {BaseRedeemablesTest} from "./utils/BaseRedeemablesTest.sol"; 5 | import {Solarray} from "solarray/Solarray.sol"; 6 | import {ERC721} from "solady/src/tokens/ERC721.sol"; 7 | import {IERC721} from "openzeppelin-contracts/contracts/interfaces/IERC721.sol"; 8 | import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 9 | import {ItemType, OrderType, Side} from "seaport-sol/src/SeaportEnums.sol"; 10 | import {OfferItemLib} from "seaport-sol/src/lib/OfferItemLib.sol"; 11 | import {ConsiderationItemLib} from "seaport-sol/src/lib/ConsiderationItemLib.sol"; 12 | import {IERC7498} from "../src/interfaces/IERC7498.sol"; 13 | import {BURN_ADDRESS} from "../src/lib/RedeemablesConstants.sol"; 14 | import {Campaign, CampaignParams, CampaignRequirements} from "../src/lib/RedeemablesStructs.sol"; 15 | import {ERC721ShipyardRedeemableOwnerMintable} from "../src/test/ERC721ShipyardRedeemableOwnerMintable.sol"; 16 | 17 | contract ERC7498_SimpleRedeem is BaseRedeemablesTest { 18 | using OfferItemLib for OfferItem; 19 | using OfferItemLib for OfferItem[]; 20 | using ConsiderationItemLib for ConsiderationItem; 21 | using ConsiderationItemLib for ConsiderationItem[]; 22 | 23 | uint256 tokenId = 2; 24 | 25 | function testBurnErc721OrErc1155RedeemErc721() public { 26 | for (uint256 i; i < erc7498Tokens.length; i++) { 27 | testRedeemable( 28 | this.burnErc721OrErc1155RedeemErc721, RedeemablesContext({erc7498Token: IERC7498(erc7498Tokens[i])}) 29 | ); 30 | } 31 | } 32 | 33 | function burnErc721OrErc1155RedeemErc721(RedeemablesContext memory context) external { 34 | _mintToken(address(context.erc7498Token), tokenId); 35 | ConsiderationItem[] memory consideration = new ConsiderationItem[](1); 36 | consideration[0] = _getCampaignConsiderationItem(address(context.erc7498Token)); 37 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 38 | requirements[0] = CampaignRequirements({ 39 | offer: defaultCampaignOffer, 40 | consideration: consideration, 41 | traitRedemptions: defaultTraitRedemptions 42 | }); 43 | CampaignParams memory params = CampaignParams({ 44 | startTime: 0, 45 | endTime: 0, // will revert with NotActive until updated 46 | maxCampaignRedemptions: 1, 47 | manager: address(this), 48 | signer: address(0) 49 | }); 50 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 51 | uint256 campaignId = context.erc7498Token.createCampaign(campaign, ""); 52 | 53 | (,, uint256 totalRedemptionsPreRedeem) = context.erc7498Token.getCampaign(1); 54 | assertEq(totalRedemptionsPreRedeem, 0); 55 | 56 | bytes memory extraData = abi.encode( 57 | 1, // campaignId 58 | 0, // requirementsIndex 59 | bytes32(0), // redemptionHash 60 | defaultTraitRedemptionTokenIds, 61 | uint256(0), // salt 62 | bytes("") // signature 63 | ); 64 | uint256[] memory considerationTokenIds = Solarray.uint256s(tokenId); 65 | 66 | vm.expectRevert( 67 | abi.encodeWithSelector( 68 | NotActive_.selector, block.timestamp, campaign.params.startTime, campaign.params.endTime 69 | ) 70 | ); 71 | context.erc7498Token.redeem(considerationTokenIds, address(0), extraData); 72 | 73 | // Update the campaign to an active endTime. 74 | campaign.params.endTime = uint32(block.timestamp + 1000); 75 | context.erc7498Token.updateCampaign(campaignId, campaign, ""); 76 | 77 | // Clear the receiveToken mintRedemption allowed callers to check for error coverage. 78 | receiveToken721.setRedeemablesContracts(new address[](0)); 79 | vm.expectRevert(abi.encodeWithSelector(InvalidCaller.selector, address(context.erc7498Token))); 80 | context.erc7498Token.redeem(considerationTokenIds, address(0), extraData); 81 | // Re-add allowed callers 82 | receiveToken721.setRedeemablesContracts(erc7498Tokens); 83 | 84 | vm.expectEmit(true, true, true, true); 85 | emit Redemption(1, 0, bytes32(0), considerationTokenIds, defaultTraitRedemptionTokenIds, address(this)); 86 | // Using address(0) for recipient should assign to msg.sender. 87 | context.erc7498Token.redeem(considerationTokenIds, address(0), extraData); 88 | 89 | _checkTokenDoesNotExist(address(context.erc7498Token), tokenId); 90 | assertEq(receiveToken721.ownerOf(1), address(this)); 91 | (,, uint256 totalRedemptionsPostRedeem) = context.erc7498Token.getCampaign(1); 92 | assertEq(totalRedemptionsPostRedeem, 1); 93 | 94 | // Redeeming one more should exceed maxCampaignRedemptions of 1. 95 | tokenId = 3; 96 | _mintToken(address(context.erc7498Token), tokenId); 97 | considerationTokenIds[0] = tokenId; 98 | vm.expectRevert(abi.encodeWithSelector(MaxCampaignRedemptionsReached.selector, 2, 1)); 99 | context.erc7498Token.redeem(considerationTokenIds, address(0), extraData); 100 | // Reset tokenId back to its original value. 101 | tokenId = 2; 102 | } 103 | 104 | function testSendErc721OrErc1155RedeemErc721() public { 105 | for (uint256 i; i < erc7498Tokens.length; i++) { 106 | testRedeemable( 107 | this.sendErc721OrErc1155RedeemErc721, RedeemablesContext({erc7498Token: IERC7498(erc7498Tokens[i])}) 108 | ); 109 | } 110 | } 111 | 112 | function sendErc721OrErc1155RedeemErc721(RedeemablesContext memory context) external { 113 | _mintToken(address(context.erc7498Token), tokenId); 114 | ConsiderationItem[] memory consideration = new ConsiderationItem[](1); 115 | consideration[0] = _getCampaignConsiderationItem(address(context.erc7498Token)); 116 | // Set consideration recipient to greg. 117 | address greg = makeAddr("greg"); 118 | consideration[0].recipient = payable(greg); 119 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 120 | requirements[0] = CampaignRequirements({ 121 | offer: defaultCampaignOffer, 122 | consideration: consideration, 123 | traitRedemptions: defaultTraitRedemptions 124 | }); 125 | CampaignParams memory params = CampaignParams({ 126 | startTime: uint32(block.timestamp), 127 | endTime: uint32(block.timestamp + 1000), 128 | maxCampaignRedemptions: 1, 129 | manager: address(this), 130 | signer: address(0) 131 | }); 132 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 133 | context.erc7498Token.createCampaign(campaign, ""); 134 | 135 | // Grant approval to the erc7498Token. 136 | IERC721(address(context.erc7498Token)).setApprovalForAll(address(context.erc7498Token), true); 137 | 138 | bytes memory extraData = abi.encode( 139 | 1, // campaignId 140 | 0, // requirementsIndex 141 | bytes32(0), // redemptionHash 142 | defaultTraitRedemptionTokenIds, 143 | uint256(0), // salt 144 | bytes("") // signature 145 | ); 146 | uint256[] memory considerationTokenIds = Solarray.uint256s(tokenId); 147 | vm.expectEmit(true, true, true, true); 148 | emit Redemption(1, 0, bytes32(0), considerationTokenIds, defaultTraitRedemptionTokenIds, address(this)); 149 | context.erc7498Token.redeem(considerationTokenIds, address(0), extraData); 150 | 151 | _checkTokenIsOwnedBy(address(context.erc7498Token), tokenId, greg); 152 | assertEq(receiveToken721.ownerOf(1), address(this)); 153 | } 154 | 155 | function testBurnErc721RedeemErc721WithSecondRequirementsIndex() public { 156 | for (uint256 i; i < erc7498Tokens.length; i++) { 157 | testRedeemable( 158 | this.burnErc721OrErc1155RedeemErc721WithSecondRequirementsIndex, 159 | RedeemablesContext({erc7498Token: IERC7498(erc7498Tokens[i])}) 160 | ); 161 | } 162 | } 163 | 164 | function burnErc721OrErc1155RedeemErc721WithSecondRequirementsIndex(RedeemablesContext memory context) public { 165 | ERC721ShipyardRedeemableOwnerMintable firstRequirementRedeemToken = 166 | new ERC721ShipyardRedeemableOwnerMintable("", ""); 167 | vm.label(address(firstRequirementRedeemToken), "firstRequirementRedeemToken"); 168 | firstRequirementRedeemToken.setApprovalForAll(address(context.erc7498Token), true); 169 | 170 | _mintToken(address(context.erc7498Token), tokenId); 171 | _mintToken(address(firstRequirementRedeemToken), tokenId); 172 | 173 | ConsiderationItem[] memory firstRequirementConsideration = new ConsiderationItem[](1); 174 | firstRequirementConsideration[0] = ConsiderationItem({ 175 | itemType: ItemType.ERC721_WITH_CRITERIA, 176 | token: address(firstRequirementRedeemToken), 177 | identifierOrCriteria: 0, 178 | startAmount: 1, 179 | endAmount: 1, 180 | recipient: payable(BURN_ADDRESS) 181 | }); 182 | ConsiderationItem[] memory secondRequirementConsideration = new ConsiderationItem[](1); 183 | secondRequirementConsideration[0] = _getCampaignConsiderationItem(address(context.erc7498Token)); 184 | CampaignRequirements[] memory requirements = new CampaignRequirements[](2); 185 | requirements[0] = CampaignRequirements({ 186 | offer: defaultCampaignOffer, 187 | consideration: firstRequirementConsideration, 188 | traitRedemptions: defaultTraitRedemptions 189 | }); 190 | requirements[1] = CampaignRequirements({ 191 | offer: defaultCampaignOffer, 192 | consideration: secondRequirementConsideration, 193 | traitRedemptions: defaultTraitRedemptions 194 | }); 195 | CampaignParams memory params = CampaignParams({ 196 | startTime: uint32(block.timestamp), 197 | endTime: uint32(block.timestamp + 1000), 198 | maxCampaignRedemptions: 5, 199 | manager: address(this), 200 | signer: address(0) 201 | }); 202 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 203 | context.erc7498Token.createCampaign(campaign, ""); 204 | 205 | // Redeeming with an invalid requirementsIndex should revert. 206 | vm.expectRevert(RequirementsIndexOutOfBounds.selector); 207 | bytes memory extraData = abi.encode( 208 | 1, // campaignId 209 | 3, // requirementsIndex 210 | bytes32(0), // redemptionHash 211 | defaultTraitRedemptionTokenIds, 212 | uint256(0), // salt 213 | bytes("") // signature 214 | ); 215 | uint256[] memory tokenIds = Solarray.uint256s(tokenId); 216 | context.erc7498Token.redeem(tokenIds, address(this), extraData); 217 | 218 | // Valid requirementsIndex should succeed. 219 | extraData = abi.encode( 220 | 1, // campaignId 221 | 1, // requirementsIndex 222 | bytes32(0), // redemptionHash 223 | defaultTraitRedemptionTokenIds, 224 | uint256(0), // salt 225 | bytes("") // signature 226 | ); 227 | context.erc7498Token.redeem(tokenIds, address(this), extraData); 228 | 229 | _checkTokenDoesNotExist(address(context.erc7498Token), tokenId); 230 | assertEq(firstRequirementRedeemToken.ownerOf(tokenId), address(this)); 231 | assertEq(receiveToken721.ownerOf(1), address(this)); 232 | } 233 | 234 | function testBurnErc20RedeemErc721() public { 235 | erc20s[0].mint(address(this), 0.5 ether); 236 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 237 | ConsiderationItem[] memory consideration = new ConsiderationItem[](1); 238 | consideration[0] = defaultCampaignConsideration[0].withToken(address(erc20s[0])).withItemType(ItemType.ERC20) 239 | .withStartAmount(0.5 ether).withEndAmount(0.5 ether); 240 | requirements[0] = CampaignRequirements({ 241 | offer: defaultCampaignOffer, 242 | consideration: consideration, 243 | traitRedemptions: defaultTraitRedemptions 244 | }); 245 | CampaignParams memory params = CampaignParams({ 246 | startTime: uint32(block.timestamp), 247 | endTime: uint32(block.timestamp + 1000), 248 | maxCampaignRedemptions: 5, 249 | manager: address(this), 250 | signer: address(0) 251 | }); 252 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 253 | IERC7498(erc7498Tokens[0]).createCampaign(campaign, ""); 254 | 255 | bytes memory extraData = abi.encode( 256 | 1, // campaignId 257 | 0, // requirementsIndex 258 | bytes32(0), // redemptionHash 259 | defaultTraitRedemptionTokenIds, 260 | uint256(0), // salt 261 | bytes("") // signature 262 | ); 263 | 264 | uint256[] memory considerationTokenIds = Solarray.uint256s(0); 265 | 266 | vm.expectEmit(true, true, true, true); 267 | emit Redemption(1, 0, bytes32(0), considerationTokenIds, defaultTraitRedemptionTokenIds, address(this)); 268 | IERC7498(erc7498Tokens[0]).redeem(considerationTokenIds, address(this), extraData); 269 | 270 | vm.expectRevert(ERC721.TokenDoesNotExist.selector); 271 | IERC721(erc7498Tokens[0]).ownerOf(tokenId); 272 | assertEq(receiveToken721.ownerOf(1), address(this)); 273 | } 274 | 275 | function testSendErc20RedeemErc721() public { 276 | erc20s[0].mint(address(this), 0.5 ether); 277 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 278 | ConsiderationItem[] memory consideration = new ConsiderationItem[](1); 279 | consideration[0] = defaultCampaignConsideration[0].withToken(address(erc20s[0])).withItemType(ItemType.ERC20) 280 | .withStartAmount(0.5 ether).withEndAmount(0.5 ether); 281 | // Set consideration recipient to greg. 282 | address greg = makeAddr("greg"); 283 | consideration[0].recipient = payable(greg); 284 | requirements[0] = CampaignRequirements({ 285 | offer: defaultCampaignOffer, 286 | consideration: consideration, 287 | traitRedemptions: defaultTraitRedemptions 288 | }); 289 | CampaignParams memory params = CampaignParams({ 290 | startTime: uint32(block.timestamp), 291 | endTime: uint32(block.timestamp + 1000), 292 | maxCampaignRedemptions: 5, 293 | manager: address(this), 294 | signer: address(0) 295 | }); 296 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 297 | IERC7498(erc7498Tokens[0]).createCampaign(campaign, ""); 298 | 299 | bytes memory extraData = abi.encode( 300 | 1, // campaignId 301 | 0, // requirementsIndex 302 | bytes32(0), // redemptionHash 303 | defaultTraitRedemptionTokenIds, 304 | uint256(0), // salt 305 | bytes("") // signature 306 | ); 307 | 308 | uint256[] memory considerationTokenIds = Solarray.uint256s(0); 309 | 310 | vm.expectEmit(true, true, true, true); 311 | emit Redemption(1, 0, bytes32(0), considerationTokenIds, defaultTraitRedemptionTokenIds, address(this)); 312 | IERC7498(erc7498Tokens[0]).redeem(considerationTokenIds, address(this), extraData); 313 | 314 | _checkTokenIsOwnedBy(address(erc20s[0]), tokenId, greg); 315 | assertEq(receiveToken721.ownerOf(1), address(this)); 316 | } 317 | 318 | function testBurnErc721RedeemErc1155() public { 319 | _mintToken(address(erc7498Tokens[0]), tokenId); 320 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 321 | OfferItem[] memory offer = new OfferItem[](1); 322 | offer[0] = defaultCampaignOffer[0].withItemType(ItemType.ERC1155).withToken(address(receiveToken1155)); 323 | requirements[0] = CampaignRequirements({ 324 | offer: offer, 325 | consideration: defaultCampaignConsideration, 326 | traitRedemptions: defaultTraitRedemptions 327 | }); 328 | CampaignParams memory params = CampaignParams({ 329 | startTime: uint32(block.timestamp), 330 | endTime: uint32(block.timestamp + 1000), 331 | maxCampaignRedemptions: 5, 332 | manager: address(this), 333 | signer: address(0) 334 | }); 335 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 336 | IERC7498(erc7498Tokens[0]).createCampaign(campaign, ""); 337 | 338 | bytes memory extraData = abi.encode( 339 | 1, // campaignId 340 | 0, // requirementsIndex 341 | bytes32(0), // redemptionHash 342 | defaultTraitRedemptionTokenIds, 343 | uint256(0), // salt 344 | bytes("") // signature 345 | ); 346 | uint256[] memory considerationTokenIds = Solarray.uint256s(tokenId); 347 | 348 | // Clear the receiveToken mintRedemption allowed callers to check for error coverage. 349 | receiveToken1155.setRedeemablesContracts(new address[](0)); 350 | vm.expectRevert(abi.encodeWithSelector(InvalidCaller.selector, erc7498Tokens[0])); 351 | IERC7498(erc7498Tokens[0]).redeem(considerationTokenIds, address(0), extraData); 352 | // Re-add allowed callers 353 | receiveToken1155.setRedeemablesContracts(erc7498Tokens); 354 | 355 | vm.expectEmit(true, true, true, true); 356 | emit Redemption(1, 0, bytes32(0), considerationTokenIds, defaultTraitRedemptionTokenIds, address(this)); 357 | IERC7498(erc7498Tokens[0]).redeem(considerationTokenIds, address(this), extraData); 358 | 359 | vm.expectRevert(ERC721.TokenDoesNotExist.selector); 360 | IERC721(erc7498Tokens[0]).ownerOf(tokenId); 361 | assertEq(receiveToken1155.balanceOf(address(this), 1), 1); 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /test/ERC7498-TraitRedemption.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {BaseRedeemablesTest} from "./utils/BaseRedeemablesTest.sol"; 5 | import {Solarray} from "solarray/Solarray.sol"; 6 | import {IERC7496} from "shipyard-core/src/dynamic-traits/interfaces/IERC7496.sol"; 7 | import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 8 | import {ItemType, OrderType, Side} from "seaport-sol/src/SeaportEnums.sol"; 9 | import {OfferItemLib} from "seaport-sol/src/lib/OfferItemLib.sol"; 10 | import {ConsiderationItemLib} from "seaport-sol/src/lib/ConsiderationItemLib.sol"; 11 | import {ERC721} from "solady/src/tokens/ERC721.sol"; 12 | import {IERC7496} from "shipyard-core/src/dynamic-traits/interfaces/IERC7496.sol"; 13 | import {IERC7498} from "../src/interfaces/IERC7498.sol"; 14 | import {Campaign, CampaignParams, CampaignRequirements, TraitRedemption} from "../src/lib/RedeemablesStructs.sol"; 15 | import {ERC721ShipyardRedeemableMintable} from "../src/extensions/ERC721ShipyardRedeemableMintable.sol"; 16 | import {ERC1155ShipyardRedeemableMintable} from "../src/extensions/ERC1155ShipyardRedeemableMintable.sol"; 17 | import {ERC721ShipyardRedeemableOwnerMintable} from "../src/test/ERC721ShipyardRedeemableOwnerMintable.sol"; 18 | import {ERC1155ShipyardRedeemableOwnerMintable} from "../src/test/ERC1155ShipyardRedeemableOwnerMintable.sol"; 19 | import {ERC721ShipyardRedeemableTraitSetters} from "../src/test/ERC721ShipyardRedeemableTraitSetters.sol"; 20 | 21 | contract ERC7498_TraitRedemption is BaseRedeemablesTest { 22 | using OfferItemLib for OfferItem; 23 | using OfferItemLib for OfferItem[]; 24 | using ConsiderationItemLib for ConsiderationItem; 25 | using ConsiderationItemLib for ConsiderationItem[]; 26 | 27 | uint256 tokenId = 2; 28 | bytes32 traitKey = bytes32("hasRedeemed"); 29 | 30 | function testGetAndSetTrait() public { 31 | for (uint256 i; i < erc7498Tokens.length; i++) { 32 | testRedeemable(this.getAndSetTrait, RedeemablesContext({erc7498Token: IERC7498(erc7498Tokens[i])})); 33 | } 34 | } 35 | 36 | function getAndSetTrait(RedeemablesContext memory context) public { 37 | if (_isERC721(address(context.erc7498Token))) { 38 | vm.expectRevert(ERC721.TokenDoesNotExist.selector); 39 | IERC7496(address(context.erc7498Token)).getTraitValue(tokenId, traitKey); 40 | vm.expectRevert(ERC721.TokenDoesNotExist.selector); 41 | IERC7496(address(context.erc7498Token)).getTraitValues(tokenId, Solarray.bytes32s(traitKey)); 42 | } 43 | 44 | _mintToken(address(context.erc7498Token), tokenId); 45 | bytes32 traitValue = IERC7496(address(context.erc7498Token)).getTraitValue(tokenId, traitKey); 46 | assertEq(traitValue, bytes32(0)); 47 | 48 | IERC7496(address(context.erc7498Token)).setTrait(tokenId, traitKey, bytes32(uint256(1))); 49 | traitValue = IERC7496(address(context.erc7498Token)).getTraitValue(tokenId, traitKey); 50 | assertEq(traitValue, bytes32(uint256(1))); 51 | 52 | bytes32[] memory traitValues = 53 | IERC7496(address(context.erc7498Token)).getTraitValues(tokenId, Solarray.bytes32s(traitKey)); 54 | assertEq(traitValues.length, 1); 55 | assertEq(traitValues[0], bytes32(uint256(1))); 56 | } 57 | 58 | function testErc721TraitRedemptionSubstandardOneForErc721() public { 59 | for (uint256 i; i < erc7498Tokens.length; i++) { 60 | testRedeemable( 61 | this.erc721TraitRedemptionSubstandardOneForErc721, 62 | RedeemablesContext({erc7498Token: IERC7498(erc7498Tokens[i])}) 63 | ); 64 | } 65 | } 66 | 67 | function erc721TraitRedemptionSubstandardOneForErc721(RedeemablesContext memory context) public { 68 | address[] memory allowedTraitSetters = Solarray.addresses(address(context.erc7498Token)); 69 | ERC721ShipyardRedeemableTraitSetters redeemToken = new ERC721ShipyardRedeemableTraitSetters("", ""); 70 | assertEq(redeemToken.getAllowedTraitSetters(), new address[](0)); 71 | redeemToken.setAllowedTraitSetters(allowedTraitSetters); 72 | assertEq(redeemToken.getAllowedTraitSetters(), allowedTraitSetters); 73 | _mintToken(address(redeemToken), tokenId); 74 | TraitRedemption[] memory traitRedemptions = new TraitRedemption[](1); 75 | // previous trait value (`substandardValue`) should be 0 76 | // new trait value should be 1 77 | traitRedemptions[0] = TraitRedemption({ 78 | substandard: 1, 79 | token: address(redeemToken), 80 | traitKey: traitKey, 81 | traitValue: bytes32(uint256(1)), 82 | substandardValue: bytes32(uint256(0)) 83 | }); 84 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 85 | // consideration is empty 86 | ConsiderationItem[] memory consideration = new ConsiderationItem[](0); 87 | requirements[0] = CampaignRequirements({ 88 | offer: defaultCampaignOffer, 89 | consideration: consideration, 90 | traitRedemptions: traitRedemptions 91 | }); 92 | CampaignParams memory params = CampaignParams({ 93 | startTime: uint32(block.timestamp), 94 | endTime: uint32(block.timestamp + 1000), 95 | maxCampaignRedemptions: 5, 96 | manager: address(this), 97 | signer: address(0) 98 | }); 99 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 100 | context.erc7498Token.createCampaign(campaign, ""); 101 | 102 | uint256[] memory considerationTokenIds; 103 | // First test reverts with TraitRedemptionTokenIdsDontMatchTraitRedemptionsLength. 104 | uint256[] memory traitRedemptionTokenIds = Solarray.uint256s(tokenId, tokenId + 1); 105 | bytes memory extraData = abi.encode( 106 | 1, // campaignId 107 | 0, // requirementsIndex 108 | bytes32(0), // redemptionHash 109 | traitRedemptionTokenIds, 110 | uint256(0), // salt 111 | bytes("") // signature 112 | ); 113 | 114 | vm.expectRevert(abi.encodeWithSelector(TraitRedemptionTokenIdsDontMatchTraitRedemptionsLength.selector, 1, 2)); 115 | context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); 116 | 117 | // Now test with valid trait redemptions token ids length. 118 | traitRedemptionTokenIds = Solarray.uint256s(tokenId); 119 | extraData = abi.encode( 120 | 1, // campaignId 121 | 0, // requirementsIndex 122 | bytes32(0), // redemptionHash 123 | traitRedemptionTokenIds, 124 | uint256(0), // salt 125 | bytes("") // signature 126 | ); 127 | vm.expectEmit(true, true, true, true); 128 | emit Redemption(1, 0, bytes32(0), considerationTokenIds, traitRedemptionTokenIds, address(this)); 129 | context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); 130 | 131 | bytes32 actualTraitValue = redeemToken.getTraitValue(tokenId, traitKey); 132 | assertEq(bytes32(uint256(1)), actualTraitValue); 133 | assertEq(receiveToken721.ownerOf(1), address(this)); 134 | 135 | // Redeeming one more time should fail with InvalidRequiredTraitValue since it is already 1. 136 | vm.expectRevert( 137 | abi.encodeWithSelector( 138 | InvalidRequiredTraitValue.selector, redeemToken, tokenId, traitKey, bytes32(uint256(1)), bytes32(0) 139 | ) 140 | ); 141 | context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); 142 | } 143 | 144 | function testErc721TraitRedemptionSubstandardTwoForErc721() public { 145 | for (uint256 i; i < erc7498Tokens.length; i++) { 146 | testRedeemable( 147 | this.erc721TraitRedemptionSubstandardTwoForErc721, 148 | RedeemablesContext({erc7498Token: IERC7498(erc7498Tokens[i])}) 149 | ); 150 | } 151 | } 152 | 153 | function erc721TraitRedemptionSubstandardTwoForErc721(RedeemablesContext memory context) public { 154 | address[] memory allowedTraitSetters = Solarray.addresses(address(context.erc7498Token), address(this)); 155 | ERC721ShipyardRedeemableTraitSetters redeemToken = new ERC721ShipyardRedeemableTraitSetters("", ""); 156 | redeemToken.setAllowedTraitSetters(allowedTraitSetters); 157 | _mintToken(address(redeemToken), tokenId); 158 | redeemToken.setTrait(tokenId, traitKey, bytes32(uint256(1))); 159 | TraitRedemption[] memory traitRedemptions = new TraitRedemption[](1); 160 | // previous trait value should not be greater than 1 (`substandardValue`) 161 | // new trait value should be 2 (adding traitValue of 1) 162 | traitRedemptions[0] = TraitRedemption({ 163 | substandard: 2, 164 | token: address(redeemToken), 165 | traitKey: traitKey, 166 | traitValue: bytes32(uint256(1)), 167 | substandardValue: bytes32(uint256(1)) 168 | }); 169 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 170 | // consideration is empty 171 | ConsiderationItem[] memory consideration = new ConsiderationItem[](0); 172 | requirements[0] = CampaignRequirements({ 173 | offer: defaultCampaignOffer, 174 | consideration: consideration, 175 | traitRedemptions: traitRedemptions 176 | }); 177 | CampaignParams memory params = CampaignParams({ 178 | startTime: uint32(block.timestamp), 179 | endTime: uint32(block.timestamp + 1000), 180 | maxCampaignRedemptions: 5, 181 | manager: address(this), 182 | signer: address(0) 183 | }); 184 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 185 | context.erc7498Token.createCampaign(campaign, ""); 186 | 187 | uint256[] memory considerationTokenIds; 188 | uint256[] memory traitRedemptionTokenIds = Solarray.uint256s(tokenId); 189 | bytes memory extraData = abi.encode( 190 | 1, // campaignId 191 | 0, // requirementsIndex 192 | bytes32(0), // redemptionHash 193 | traitRedemptionTokenIds, 194 | uint256(0), // salt 195 | bytes("") // signature 196 | ); 197 | vm.expectEmit(true, true, true, true); 198 | emit Redemption(1, 0, bytes32(0), considerationTokenIds, traitRedemptionTokenIds, address(this)); 199 | context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); 200 | 201 | bytes32 actualTraitValue = redeemToken.getTraitValue(tokenId, traitKey); 202 | assertEq(bytes32(uint256(2)), actualTraitValue); 203 | assertEq(receiveToken721.ownerOf(1), address(this)); 204 | 205 | // Redeeming one more time should fail with InvalidRequiredTraitValue since it is already 2. 206 | vm.expectRevert( 207 | abi.encodeWithSelector( 208 | InvalidRequiredTraitValue.selector, 209 | redeemToken, 210 | tokenId, 211 | traitKey, 212 | bytes32(uint256(2)), 213 | bytes32(uint256(1)) 214 | ) 215 | ); 216 | context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); 217 | } 218 | 219 | function testErc721TraitRedemptionSubstandardThreeForErc721() public { 220 | for (uint256 i; i < erc7498Tokens.length; i++) { 221 | testRedeemable( 222 | this.erc721TraitRedemptionSubstandardThreeForErc721, 223 | RedeemablesContext({erc7498Token: IERC7498(erc7498Tokens[i])}) 224 | ); 225 | } 226 | } 227 | 228 | function erc721TraitRedemptionSubstandardThreeForErc721(RedeemablesContext memory context) public { 229 | address[] memory allowedTraitSetters = Solarray.addresses(address(context.erc7498Token), address(this)); 230 | ERC721ShipyardRedeemableTraitSetters redeemToken = new ERC721ShipyardRedeemableTraitSetters("", ""); 231 | redeemToken.setAllowedTraitSetters(allowedTraitSetters); 232 | _mintToken(address(redeemToken), tokenId); 233 | redeemToken.setTrait(tokenId, traitKey, bytes32(uint256(5))); 234 | TraitRedemption[] memory traitRedemptions = new TraitRedemption[](1); 235 | // previous trait value should not be less than 4 (`substandardValue`) 236 | // new trait value should be 4 (adding traitValue of 1) 237 | traitRedemptions[0] = TraitRedemption({ 238 | substandard: 3, 239 | token: address(redeemToken), 240 | traitKey: traitKey, 241 | traitValue: bytes32(uint256(1)), 242 | substandardValue: bytes32(uint256(5)) 243 | }); 244 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 245 | // consideration is empty 246 | ConsiderationItem[] memory consideration = new ConsiderationItem[](0); 247 | requirements[0] = CampaignRequirements({ 248 | offer: defaultCampaignOffer, 249 | consideration: consideration, 250 | traitRedemptions: traitRedemptions 251 | }); 252 | CampaignParams memory params = CampaignParams({ 253 | startTime: uint32(block.timestamp), 254 | endTime: uint32(block.timestamp + 1000), 255 | maxCampaignRedemptions: 5, 256 | manager: address(this), 257 | signer: address(0) 258 | }); 259 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 260 | context.erc7498Token.createCampaign(campaign, ""); 261 | 262 | uint256[] memory considerationTokenIds; 263 | uint256[] memory traitRedemptionTokenIds = Solarray.uint256s(tokenId); 264 | bytes memory extraData = abi.encode( 265 | 1, // campaignId 266 | 0, // requirementsIndex 267 | bytes32(0), // redemptionHash 268 | traitRedemptionTokenIds, 269 | uint256(0), // salt 270 | bytes("") // signature 271 | ); 272 | vm.expectEmit(true, true, true, true); 273 | emit Redemption(1, 0, bytes32(0), considerationTokenIds, traitRedemptionTokenIds, address(this)); 274 | context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); 275 | 276 | bytes32 actualTraitValue = redeemToken.getTraitValue(tokenId, traitKey); 277 | assertEq(bytes32(uint256(4)), actualTraitValue); 278 | assertEq(receiveToken721.ownerOf(1), address(this)); 279 | 280 | // Redeeming one more time should fail with InvalidRequiredTraitValue since it is now 4. 281 | vm.expectRevert( 282 | abi.encodeWithSelector( 283 | InvalidRequiredTraitValue.selector, 284 | redeemToken, 285 | tokenId, 286 | traitKey, 287 | bytes32(uint256(4)), 288 | bytes32(uint256(5)) 289 | ) 290 | ); 291 | context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); 292 | } 293 | 294 | function testErc721TraitRedemptionSubstandardFourForErc721() public { 295 | for (uint256 i; i < erc7498Tokens.length; i++) { 296 | testRedeemable( 297 | this.erc721TraitRedemptionSubstandardFourForErc721, 298 | RedeemablesContext({erc7498Token: IERC7498(erc7498Tokens[i])}) 299 | ); 300 | } 301 | } 302 | 303 | function erc721TraitRedemptionSubstandardFourForErc721(RedeemablesContext memory context) public { 304 | address[] memory allowedTraitSetters = Solarray.addresses(address(context.erc7498Token), address(this)); 305 | ERC721ShipyardRedeemableTraitSetters redeemToken = new ERC721ShipyardRedeemableTraitSetters("", ""); 306 | redeemToken.setAllowedTraitSetters(allowedTraitSetters); 307 | _mintToken(address(redeemToken), tokenId); 308 | redeemToken.setTrait(tokenId, traitKey, bytes32(uint256(4))); 309 | TraitRedemption[] memory traitRedemptions = new TraitRedemption[](1); 310 | // previous trait value should be the trait value 311 | // trait value does not change in substandard 4 312 | traitRedemptions[0] = TraitRedemption({ 313 | substandard: 4, 314 | token: address(redeemToken), 315 | traitKey: traitKey, 316 | traitValue: bytes32(uint256(5)), 317 | substandardValue: bytes32(0) // unused in substandard 4 318 | }); 319 | CampaignRequirements[] memory requirements = new CampaignRequirements[](1); 320 | // consideration is empty 321 | ConsiderationItem[] memory consideration = new ConsiderationItem[](0); 322 | requirements[0] = CampaignRequirements({ 323 | offer: defaultCampaignOffer, 324 | consideration: consideration, 325 | traitRedemptions: traitRedemptions 326 | }); 327 | CampaignParams memory params = CampaignParams({ 328 | startTime: uint32(block.timestamp), 329 | endTime: uint32(block.timestamp + 1000), 330 | maxCampaignRedemptions: 5, 331 | manager: address(this), 332 | signer: address(0) 333 | }); 334 | Campaign memory campaign = Campaign({params: params, requirements: requirements}); 335 | context.erc7498Token.createCampaign(campaign, ""); 336 | 337 | uint256[] memory considerationTokenIds; 338 | uint256[] memory traitRedemptionTokenIds = Solarray.uint256s(tokenId); 339 | bytes memory extraData = abi.encode( 340 | 1, // campaignId 341 | 0, // requirementsIndex 342 | bytes32(0), // redemptionHash 343 | traitRedemptionTokenIds, 344 | uint256(0), // salt 345 | bytes("") // signature 346 | ); 347 | 348 | // Redeeming should fail since the trait value does not match. 349 | vm.expectRevert( 350 | abi.encodeWithSelector( 351 | InvalidRequiredTraitValue.selector, 352 | redeemToken, 353 | tokenId, 354 | traitKey, 355 | bytes32(uint256(4)), 356 | bytes32(uint256(0)) 357 | ) 358 | ); 359 | context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); 360 | 361 | // Update the trait value, now it should match. 362 | redeemToken.setTrait(tokenId, traitKey, bytes32(uint256(5))); 363 | 364 | vm.expectEmit(true, true, true, true); 365 | emit Redemption(1, 0, bytes32(0), considerationTokenIds, traitRedemptionTokenIds, address(this)); 366 | context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); 367 | 368 | bytes32 actualTraitValue = redeemToken.getTraitValue(tokenId, traitKey); 369 | assertEq(bytes32(uint256(5)), actualTraitValue); 370 | assertEq(receiveToken721.ownerOf(1), address(this)); 371 | 372 | // Redeeming one more time should succeed since it has not changed. 373 | emit Redemption(1, 0, bytes32(0), considerationTokenIds, traitRedemptionTokenIds, address(this)); 374 | context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); 375 | 376 | actualTraitValue = redeemToken.getTraitValue(tokenId, traitKey); 377 | assertEq(bytes32(uint256(5)), actualTraitValue); 378 | assertEq(receiveToken721.ownerOf(2), address(this)); 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /test/ERC7498.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {BaseRedeemablesTest} from "./utils/BaseRedeemablesTest.sol"; 5 | import {IERC165} from "openzeppelin-contracts/contracts/interfaces/IERC165.sol"; 6 | import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; 7 | import {OfferItemLib} from "seaport-sol/src/lib/OfferItemLib.sol"; 8 | import {ConsiderationItemLib} from "seaport-sol/src/lib/ConsiderationItemLib.sol"; 9 | import {IERC7498} from "../src/interfaces/IERC7498.sol"; 10 | 11 | contract TestERC7498 is BaseRedeemablesTest { 12 | using OfferItemLib for OfferItem; 13 | using OfferItemLib for OfferItem[]; 14 | using ConsiderationItemLib for ConsiderationItem; 15 | using ConsiderationItemLib for ConsiderationItem[]; 16 | 17 | function testSupportsInterfaceId() public { 18 | for (uint256 i; i < erc7498Tokens.length; i++) { 19 | testRedeemable(this.supportsInterfaceId, RedeemablesContext({erc7498Token: IERC7498(erc7498Tokens[i])})); 20 | } 21 | } 22 | 23 | function supportsInterfaceId(RedeemablesContext memory context) public { 24 | assertTrue(IERC165(address(context.erc7498Token)).supportsInterface(type(IERC7498).interfaceId)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/RedeemableContractOfferer-Revert.t.sol.txt: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Solarray} from "solarray/Solarray.sol"; 5 | import {BaseOrderTest} from "./utils/BaseOrderTest.sol"; 6 | import {TestERC20} from "./utils/mocks/TestERC20.sol"; 7 | import {TestERC721} from "./utils/mocks/TestERC721.sol"; 8 | import { 9 | OfferItem, 10 | ConsiderationItem, 11 | SpentItem, 12 | AdvancedOrder, 13 | OrderParameters, 14 | CriteriaResolver, 15 | FulfillmentComponent 16 | } from "seaport-types/src/lib/ConsiderationStructs.sol"; 17 | // import {CriteriaResolutionErrors} from "seaport-types/src/interfaces/CriteriaResolutionErrors.sol"; 18 | import {ItemType, OrderType, Side} from "seaport-sol/src/SeaportEnums.sol"; 19 | import {OfferItemLib, ConsiderationItemLib, OrderParametersLib} from "seaport-sol/src/SeaportSol.sol"; 20 | import {RedeemableContractOfferer} from "../src/RedeemableContractOfferer.sol"; 21 | import {CampaignParams} from "../src/lib/RedeemablesStructs.sol"; 22 | import {BURN_ADDRESS} from "../src/lib/RedeemablesConstants.sol"; 23 | import {RedeemablesErrors} from "../src/lib/RedeemablesErrors.sol"; 24 | import {ERC721ShipyardRedeemableMintable} from "../src/extensions/ERC721ShipyardRedeemableMintable.sol"; 25 | import {ERC721ShipyardRedeemableMintable} from "../src/extensions/ERC721ShipyardRedeemableMintable.sol"; 26 | import {Merkle} from "../lib/murky/src/Merkle.sol"; 27 | 28 | contract TestRedeemableContractOfferer_Revert is BaseOrderTest, RedeemablesErrors { 29 | using OrderParametersLib for OrderParameters; 30 | 31 | error InvalidContractOrder(bytes32 orderHash); 32 | 33 | RedeemableContractOfferer offerer; 34 | TestERC721 redeemableToken; 35 | ERC721ShipyardRedeemableMintable redemptionToken; 36 | CriteriaResolver[] criteriaResolvers; 37 | Merkle merkle = new Merkle(); 38 | 39 | function setUp() public override { 40 | super.setUp(); 41 | offerer = new RedeemableContractOfferer( 42 | address(conduit), 43 | conduitKey, 44 | address(seaport) 45 | ); 46 | redeemableToken = new TestERC721(); 47 | redemptionToken = new ERC721ShipyardRedeemableMintable( 48 | address(offerer), 49 | address(redeemableToken) 50 | ); 51 | vm.label(address(redeemableToken), "redeemableToken"); 52 | vm.label(address(redemptionToken), "redemptionToken"); 53 | } 54 | 55 | function testRevertRedeemWithCriteriaResolversViaSeaport() public { 56 | uint256 tokenId = 7; 57 | _mintToken(address(redeemableToken), tokenId); 58 | redeemableToken.setApprovalForAll(address(conduit), true); 59 | 60 | CriteriaResolver[] memory resolvers = new CriteriaResolver[](1); 61 | 62 | // Create an array of hashed identifiers (0-4) 63 | // Get the merkle root of the hashed identifiers to pass into updateCampaign 64 | // Only tokenIds 0-4 can be redeemed 65 | bytes32[] memory hashedIdentifiers = new bytes32[](5); 66 | for (uint256 i = 0; i < hashedIdentifiers.length; i++) { 67 | hashedIdentifiers[i] = keccak256(abi.encode(i)); 68 | } 69 | bytes32 root = merkle.getRoot(hashedIdentifiers); 70 | 71 | OfferItem[] memory offer = new OfferItem[](1); 72 | offer[0] = OfferItem({ 73 | itemType: ItemType.ERC721_WITH_CRITERIA, 74 | token: address(redemptionToken), 75 | identifierOrCriteria: 0, 76 | startAmount: 1, 77 | endAmount: 1 78 | }); 79 | 80 | // Contract offerer will only consider tokenIds 0-4 81 | ConsiderationItem[] memory consideration = new ConsiderationItem[](1); 82 | consideration[0] = ConsiderationItem({ 83 | itemType: ItemType.ERC721_WITH_CRITERIA, 84 | token: address(redeemableToken), 85 | identifierOrCriteria: uint256(root), 86 | startAmount: 1, 87 | endAmount: 1, 88 | recipient: payable(BURN_ADDRESS) 89 | }); 90 | 91 | { 92 | CampaignParams memory params = CampaignParams({ 93 | offer: offer, 94 | consideration: consideration, 95 | startTime: uint32(block.timestamp), 96 | endTime: uint32(block.timestamp + 1000), 97 | maxCampaignRedemptions: 5, 98 | manager: address(this), 99 | signer: address(0) 100 | }); 101 | 102 | offerer.createCampaign(params, ""); 103 | } 104 | 105 | { 106 | // Hash identifiers 5 - 9 and create invalid merkle root 107 | // to pass into consideration 108 | for (uint256 i = 0; i < hashedIdentifiers.length; i++) { 109 | hashedIdentifiers[i] = keccak256(abi.encode(i + 5)); 110 | } 111 | root = merkle.getRoot(hashedIdentifiers); 112 | consideration[0].identifierOrCriteria = uint256(root); 113 | 114 | OfferItem[] memory offerFromEvent = new OfferItem[](1); 115 | offerFromEvent[0] = OfferItem({ 116 | itemType: ItemType.ERC721, 117 | token: address(redemptionToken), 118 | identifierOrCriteria: tokenId, 119 | startAmount: 1, 120 | endAmount: 1 121 | }); 122 | ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); 123 | considerationFromEvent[0] = ConsiderationItem({ 124 | itemType: ItemType.ERC721, 125 | token: address(redeemableToken), 126 | identifierOrCriteria: tokenId, 127 | startAmount: 1, 128 | endAmount: 1, 129 | recipient: payable(BURN_ADDRESS) 130 | }); 131 | 132 | assertGt(uint256(consideration[0].itemType), uint256(considerationFromEvent[0].itemType)); 133 | 134 | bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash 135 | 136 | OrderParameters memory parameters = OrderParametersLib.empty().withOfferer(address(offerer)).withOrderType( 137 | OrderType.CONTRACT 138 | ).withConsideration(consideration).withOffer(offer).withConduitKey(conduitKey).withStartTime( 139 | block.timestamp 140 | ).withEndTime(block.timestamp + 1).withTotalOriginalConsiderationItems(consideration.length); 141 | AdvancedOrder memory order = AdvancedOrder({ 142 | parameters: parameters, 143 | numerator: 1, 144 | denominator: 1, 145 | signature: "", 146 | extraData: extraData 147 | }); 148 | 149 | resolvers[0] = CriteriaResolver({ 150 | orderIndex: 0, 151 | side: Side.CONSIDERATION, 152 | index: 0, 153 | identifier: tokenId, 154 | criteriaProof: merkle.getProof(hashedIdentifiers, 2) 155 | }); 156 | 157 | vm.expectRevert( 158 | abi.encodeWithSelector( 159 | InvalidContractOrder.selector, 160 | (uint256(uint160(address(offerer))) << 96) + seaport.getContractOffererNonce(address(offerer)) 161 | ) 162 | ); 163 | seaport.fulfillAdvancedOrder({ 164 | advancedOrder: order, 165 | criteriaResolvers: resolvers, 166 | fulfillerConduitKey: conduitKey, 167 | recipient: address(0) 168 | }); 169 | } 170 | } 171 | 172 | function testRevertMaxCampaignRedemptionsReached() public { 173 | _mintToken(address(redeemableToken), 0); 174 | _mintToken(address(redeemableToken), 1); 175 | _mintToken(address(redeemableToken), 2); 176 | redeemableToken.setApprovalForAll(address(conduit), true); 177 | 178 | ERC721ShipyardRedeemableMintable redemptionTokenWithCounter = new ERC721ShipyardRedeemableMintable( 179 | address(offerer), 180 | address(redeemableToken) 181 | ); 182 | 183 | OfferItem[] memory offer = new OfferItem[](1); 184 | offer[0] = OfferItem({ 185 | itemType: ItemType.ERC721_WITH_CRITERIA, 186 | token: address(redemptionTokenWithCounter), 187 | identifierOrCriteria: 0, 188 | startAmount: 1, 189 | endAmount: 1 190 | }); 191 | 192 | ConsiderationItem[] memory consideration = new ConsiderationItem[](1); 193 | consideration[0] = ConsiderationItem({ 194 | itemType: ItemType.ERC721_WITH_CRITERIA, 195 | token: address(redeemableToken), 196 | identifierOrCriteria: 0, 197 | startAmount: 1, 198 | endAmount: 1, 199 | recipient: payable(BURN_ADDRESS) 200 | }); 201 | 202 | { 203 | CampaignParams memory params = CampaignParams({ 204 | offer: offer, 205 | consideration: consideration, 206 | startTime: uint32(block.timestamp), 207 | endTime: uint32(block.timestamp + 1000), 208 | maxCampaignRedemptions: 2, 209 | manager: address(this), 210 | signer: address(0) 211 | }); 212 | 213 | offerer.createCampaign(params, ""); 214 | } 215 | 216 | { 217 | OfferItem[] memory offerFromEvent = new OfferItem[](1); 218 | offerFromEvent[0] = OfferItem({ 219 | itemType: ItemType.ERC721, 220 | token: address(redemptionToken), 221 | identifierOrCriteria: 0, 222 | startAmount: 1, 223 | endAmount: 1 224 | }); 225 | ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); 226 | considerationFromEvent[0] = ConsiderationItem({ 227 | itemType: ItemType.ERC721, 228 | token: address(redeemableToken), 229 | identifierOrCriteria: 0, 230 | startAmount: 1, 231 | endAmount: 1, 232 | recipient: payable(BURN_ADDRESS) 233 | }); 234 | 235 | offerFromEvent[0] = OfferItem({ 236 | itemType: ItemType.ERC721, 237 | token: address(redemptionToken), 238 | identifierOrCriteria: 1, 239 | startAmount: 1, 240 | endAmount: 1 241 | }); 242 | 243 | considerationFromEvent[0] = ConsiderationItem({ 244 | itemType: ItemType.ERC721, 245 | token: address(redeemableToken), 246 | identifierOrCriteria: 1, 247 | startAmount: 1, 248 | endAmount: 1, 249 | recipient: payable(BURN_ADDRESS) 250 | }); 251 | 252 | assertGt(uint256(consideration[0].itemType), uint256(considerationFromEvent[0].itemType)); 253 | 254 | bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash 255 | 256 | considerationFromEvent[0].identifierOrCriteria = 0; 257 | 258 | OrderParameters memory parameters = OrderParametersLib.empty().withOfferer(address(offerer)).withOrderType( 259 | OrderType.CONTRACT 260 | ).withConsideration(considerationFromEvent).withConduitKey(conduitKey).withStartTime(block.timestamp) 261 | .withEndTime(block.timestamp + 1).withTotalOriginalConsiderationItems(consideration.length); 262 | AdvancedOrder memory order = AdvancedOrder({ 263 | parameters: parameters, 264 | numerator: 1, 265 | denominator: 1, 266 | signature: "", 267 | extraData: extraData 268 | }); 269 | 270 | seaport.fulfillAdvancedOrder({ 271 | advancedOrder: order, 272 | criteriaResolvers: criteriaResolvers, 273 | fulfillerConduitKey: conduitKey, 274 | recipient: address(0) 275 | }); 276 | 277 | considerationFromEvent[0].identifierOrCriteria = 1; 278 | 279 | // vm.expectEmit(true, true, true, true); 280 | // emit Or( 281 | // address(this), 282 | // campaignId, 283 | // ConsiderationItemLib.toSpentItemArray(considerationFromEvent), 284 | // OfferItemLib.toSpentItemArray(offerFromEvent), 285 | // redemptionHash 286 | // ); 287 | 288 | seaport.fulfillAdvancedOrder({ 289 | advancedOrder: order, 290 | criteriaResolvers: criteriaResolvers, 291 | fulfillerConduitKey: conduitKey, 292 | recipient: address(0) 293 | }); 294 | 295 | considerationFromEvent[0].identifierOrCriteria = 2; 296 | 297 | // Should revert on the third redemption 298 | // The call to Seaport should revert with maxCampaignRedemptionsReached(3, 2) 299 | // vm.expectRevert( 300 | // abi.encodeWithSelector( 301 | // maxCampaignRedemptionsReached.selector, 302 | // 3, 303 | // 2 304 | // ) 305 | // ); 306 | vm.expectRevert( 307 | abi.encodeWithSelector( 308 | InvalidContractOrder.selector, 309 | (uint256(uint160(address(offerer))) << 96) + seaport.getContractOffererNonce(address(offerer)) 310 | ) 311 | ); 312 | seaport.fulfillAdvancedOrder({ 313 | advancedOrder: order, 314 | criteriaResolvers: criteriaResolvers, 315 | fulfillerConduitKey: conduitKey, 316 | recipient: address(0) 317 | }); 318 | 319 | assertEq(redeemableToken.ownerOf(0), BURN_ADDRESS); 320 | assertEq(redeemableToken.ownerOf(1), BURN_ADDRESS); 321 | assertEq(redemptionTokenWithCounter.ownerOf(0), address(this)); 322 | assertEq(redemptionTokenWithCounter.ownerOf(1), address(this)); 323 | } 324 | } 325 | 326 | function testRevertConsiderationItemRecipientCannotBeZeroAddress() public { 327 | uint256 tokenId = 2; 328 | _mintToken(address(redeemableToken), tokenId); 329 | redeemableToken.setApprovalForAll(address(conduit), true); 330 | 331 | OfferItem[] memory offer = new OfferItem[](1); 332 | offer[0] = OfferItem({ 333 | itemType: ItemType.ERC721_WITH_CRITERIA, 334 | token: address(redemptionToken), 335 | identifierOrCriteria: 0, 336 | startAmount: 1, 337 | endAmount: 1 338 | }); 339 | 340 | ConsiderationItem[] memory consideration = new ConsiderationItem[](1); 341 | consideration[0] = ConsiderationItem({ 342 | itemType: ItemType.ERC721_WITH_CRITERIA, 343 | token: address(redeemableToken), 344 | identifierOrCriteria: 0, 345 | startAmount: 1, 346 | endAmount: 1, 347 | recipient: payable(address(0)) 348 | }); 349 | 350 | { 351 | CampaignParams memory params = CampaignParams({ 352 | offer: offer, 353 | consideration: consideration, 354 | startTime: uint32(block.timestamp), 355 | endTime: uint32(block.timestamp + 1000), 356 | maxCampaignRedemptions: 5, 357 | manager: address(this), 358 | signer: address(0) 359 | }); 360 | 361 | vm.expectRevert(abi.encodeWithSelector(ConsiderationItemRecipientCannotBeZeroAddress.selector)); 362 | offerer.createCampaign(params, ""); 363 | } 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /test/ShipyardContractMetadata.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {BaseRedeemablesTest} from "./utils/BaseRedeemablesTest.sol"; 5 | import {Solarray} from "solarray/Solarray.sol"; 6 | import {Ownable} from "solady/src/auth/Ownable.sol"; 7 | import {IERC721Metadata} from "openzeppelin-contracts/contracts/interfaces/IERC721Metadata.sol"; 8 | import {IERC1155MetadataURI} from "openzeppelin-contracts/contracts/interfaces/IERC1155MetadataURI.sol"; 9 | import {IERC2981} from "openzeppelin-contracts/contracts/interfaces/IERC2981.sol"; 10 | import {ERC721ShipyardContractMetadata} from "../src/lib/ERC721ShipyardContractMetadata.sol"; 11 | import {ERC1155ShipyardContractMetadata} from "../src/lib/ERC1155ShipyardContractMetadata.sol"; 12 | import {IShipyardContractMetadata} from "../src/interfaces/IShipyardContractMetadata.sol"; 13 | 14 | contract ERC721ShipyardContractMetadataOwnerMintable is ERC721ShipyardContractMetadata { 15 | constructor(string memory name_, string memory symbol_) ERC721ShipyardContractMetadata(name_, symbol_) {} 16 | 17 | function mint(address to, uint256 tokenId) external onlyOwner { 18 | _mint(to, tokenId); 19 | } 20 | } 21 | 22 | contract ERC1155ShipyardContractMetadataOwnerMintable is ERC1155ShipyardContractMetadata { 23 | constructor(string memory name_, string memory symbol_) ERC1155ShipyardContractMetadata(name_, symbol_) {} 24 | 25 | function mint(address to, uint256 tokenId, uint256 amount) external onlyOwner { 26 | _mint(to, tokenId, amount, ""); 27 | } 28 | } 29 | 30 | contract TestShipyardContractMetadata is BaseRedeemablesTest { 31 | ERC721ShipyardContractMetadataOwnerMintable token721; 32 | ERC1155ShipyardContractMetadataOwnerMintable token1155; 33 | address[] tokens; 34 | 35 | event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); 36 | event ContractURIUpdated(); 37 | event ProvenanceHashUpdated(bytes32 oldProvenanceHash, bytes32 newProvenanceHash); 38 | event RoyaltyInfoUpdated(address receiver, uint256 basisPoints); 39 | 40 | function setUp() public override { 41 | token721 = new ERC721ShipyardContractMetadataOwnerMintable("Test", "TST"); 42 | token1155 = new ERC1155ShipyardContractMetadataOwnerMintable("Test", "TST"); 43 | tokens = new address[](2); 44 | tokens[0] = address(token721); 45 | tokens[1] = address(token1155); 46 | } 47 | 48 | function testNameAndSymbol() external { 49 | for (uint256 i; i < tokens.length; i++) { 50 | this.nameAndSymbol(IShipyardContractMetadata(tokens[i])); 51 | } 52 | } 53 | 54 | function nameAndSymbol(IShipyardContractMetadata token) external { 55 | assertEq(token.name(), "Test"); 56 | assertEq(token.symbol(), "TST"); 57 | } 58 | 59 | function testOwner() external { 60 | for (uint256 i; i < tokens.length; i++) { 61 | this.owner(IShipyardContractMetadata(tokens[i])); 62 | } 63 | } 64 | 65 | function owner(IShipyardContractMetadata token) external { 66 | assertEq(Ownable(address(token)).owner(), address(this)); 67 | } 68 | 69 | function testBaseURI() external { 70 | for (uint256 i; i < tokens.length; i++) { 71 | this.baseURI(IShipyardContractMetadata(tokens[i])); 72 | } 73 | } 74 | 75 | function baseURI(IShipyardContractMetadata token) external { 76 | uint256 tokenId = 1; 77 | _mintToken(address(token), tokenId); 78 | 79 | if (_isERC721(address(token))) { 80 | assertEq(IERC721Metadata(address(token)).tokenURI(tokenId), ""); 81 | } else { 82 | // token is 1155 83 | assertEq(IERC1155MetadataURI(address(token)).uri(tokenId), ""); 84 | } 85 | 86 | assertEq(token.baseURI(), ""); 87 | vm.expectEmit(true, true, true, true); 88 | emit BatchMetadataUpdate(0, type(uint256).max); 89 | token.setBaseURI("https://example.com/"); 90 | assertEq(token.baseURI(), "https://example.com/"); 91 | 92 | if (_isERC721(address(token))) { 93 | assertEq(IERC721Metadata(address(token)).tokenURI(tokenId), string.concat("https://example.com/", "1")); 94 | 95 | // For ERC721, without the slash shouldn't append tokenId, 96 | // for e.g. prereveal states of when all tokens have the same metadata. 97 | // For ERC1155, {id} substitution defined in spec can be used. 98 | token.setBaseURI("https://example.com"); 99 | assertEq(token.baseURI(), "https://example.com"); 100 | assertEq(IERC721Metadata(address(token)).tokenURI(tokenId), "https://example.com"); 101 | } else { 102 | // token is 1155 103 | assertEq(IERC1155MetadataURI(address(token)).uri(tokenId), string.concat("https://example.com/")); 104 | } 105 | } 106 | 107 | function testContractURI() external { 108 | for (uint256 i; i < tokens.length; i++) { 109 | this.contractURI(IShipyardContractMetadata(tokens[i])); 110 | } 111 | } 112 | 113 | function contractURI(IShipyardContractMetadata token) external { 114 | assertEq(token.contractURI(), ""); 115 | vm.expectEmit(true, true, true, true); 116 | emit ContractURIUpdated(); 117 | token.setContractURI("https://example.com/"); 118 | assertEq(token.contractURI(), "https://example.com/"); 119 | } 120 | 121 | function testSetProvenanceHash() external { 122 | for (uint256 i; i < tokens.length; i++) { 123 | this.setProvenanceHash(IShipyardContractMetadata(tokens[i])); 124 | } 125 | } 126 | 127 | function setProvenanceHash(IShipyardContractMetadata token) external { 128 | assertEq(token.provenanceHash(), ""); 129 | vm.expectEmit(true, true, true, true); 130 | emit ProvenanceHashUpdated(bytes32(0), bytes32(uint256(1234))); 131 | token.setProvenanceHash(bytes32(uint256(1234))); 132 | assertEq(token.provenanceHash(), bytes32(uint256(1234))); 133 | 134 | // Setting the provenance hash again should revert. 135 | vm.expectRevert(IShipyardContractMetadata.ProvenanceHashCannotBeSetAfterAlreadyBeingSet.selector); 136 | token.setProvenanceHash(bytes32(uint256(5678))); 137 | } 138 | 139 | function testSetDefaultRoyalty() external { 140 | for (uint256 i; i < tokens.length; i++) { 141 | this.setDefaultRoyalty(IShipyardContractMetadata(tokens[i])); 142 | } 143 | } 144 | 145 | function setDefaultRoyalty(IShipyardContractMetadata token) external { 146 | address greg = makeAddr("greg"); 147 | vm.expectEmit(true, true, true, true); 148 | emit RoyaltyInfoUpdated(greg, 9_000); 149 | token.setDefaultRoyalty(greg, 9_000); 150 | (address receiver, uint256 amount) = IERC2981(address(token)).royaltyInfo(0, 100); 151 | assertEq(receiver, greg); 152 | assertEq(amount, 100 * 9_000 / 10_000); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /test/utils/ArithmeticUtil.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | library ArithmeticUtil { 5 | ///@dev utility function to avoid overflows when multiplying fuzzed uints with widths <256 6 | function mul(uint256 a, uint256 b) internal pure returns (uint256) { 7 | return a * b; 8 | } 9 | 10 | ///@dev utility function to avoid overflows when adding fuzzed uints with widths <256 11 | function add(uint256 a, uint256 b) internal pure returns (uint256) { 12 | return a + b; 13 | } 14 | 15 | ///@dev utility function to avoid overflows when subtracting fuzzed uints with widths <256 16 | function sub(uint256 a, uint256 b) internal pure returns (uint256) { 17 | return a - b; 18 | } 19 | 20 | ///@dev utility function to avoid overflows when dividing fuzzed uints with widths <256 21 | function div(uint256 a, uint256 b) internal pure returns (uint256) { 22 | return a / b; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/utils/BaseOrderTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {LibString} from "solady/src/utils/LibString.sol"; 5 | import {FulfillAvailableHelper} from "seaport-sol/src/fulfillments/available/FulfillAvailableHelper.sol"; 6 | import {MatchFulfillmentHelper} from "seaport-sol/src/fulfillments/match/MatchFulfillmentHelper.sol"; 7 | import { 8 | AdvancedOrderLib, 9 | ConsiderationItemLib, 10 | FulfillmentComponentLib, 11 | FulfillmentLib, 12 | OfferItemLib, 13 | OrderComponentsLib, 14 | OrderLib, 15 | OrderParametersLib, 16 | SeaportArrays 17 | } from "seaport-sol/src/SeaportSol.sol"; 18 | import { 19 | AdvancedOrder, 20 | ConsiderationItem, 21 | Fulfillment, 22 | FulfillmentComponent, 23 | OfferItem, 24 | Order, 25 | OrderComponents, 26 | OrderParameters 27 | } from "seaport-sol/src/SeaportStructs.sol"; 28 | import {ItemType, OrderType} from "seaport-sol/src/SeaportEnums.sol"; 29 | import {SeaportInterface} from "seaport-sol/src/SeaportInterface.sol"; 30 | import {AmountDeriver} from "seaport-core/src/lib/AmountDeriver.sol"; 31 | import {BaseSeaportTest} from "./BaseSeaportTest.sol"; 32 | import {ArithmeticUtil} from "./ArithmeticUtil.sol"; 33 | import {ERC1155Recipient} from "./mocks/ERC1155Recipient.sol"; 34 | import {ERC721Recipient} from "./mocks/ERC721Recipient.sol"; 35 | import {TestERC20} from "./mocks/TestERC20.sol"; 36 | import {TestERC721} from "./mocks/TestERC721.sol"; 37 | import {TestERC1155} from "./mocks/TestERC1155.sol"; 38 | 39 | /** 40 | * @dev This is a base test class for cases that depend on pre-deployed token 41 | * contracts. Note that it is different from the BaseOrderTest in the 42 | * legacy test suite. 43 | */ 44 | contract BaseOrderTest is BaseSeaportTest, AmountDeriver, ERC721Recipient, ERC1155Recipient { 45 | using ArithmeticUtil for *; 46 | 47 | using AdvancedOrderLib for AdvancedOrder; 48 | using AdvancedOrderLib for AdvancedOrder[]; 49 | using ConsiderationItemLib for ConsiderationItem; 50 | using ConsiderationItemLib for ConsiderationItem[]; 51 | using FulfillmentComponentLib for FulfillmentComponent; 52 | using FulfillmentComponentLib for FulfillmentComponent[]; 53 | using FulfillmentLib for Fulfillment; 54 | using FulfillmentLib for Fulfillment[]; 55 | using OfferItemLib for OfferItem; 56 | using OfferItemLib for OfferItem[]; 57 | using OrderComponentsLib for OrderComponents; 58 | using OrderLib for Order; 59 | using OrderLib for Order[]; 60 | using OrderParametersLib for OrderParameters; 61 | 62 | event Transfer(address indexed from, address indexed to, uint256 value); 63 | event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); 64 | 65 | struct Context { 66 | SeaportInterface seaport; 67 | } 68 | 69 | // SeaportValidatorHelper validatorHelper; 70 | // SeaportValidator validator; 71 | FulfillAvailableHelper fulfill; 72 | MatchFulfillmentHelper matcher; 73 | 74 | Account offerer1; 75 | Account offerer2; 76 | 77 | Account dillon; 78 | Account eve; 79 | Account frank; 80 | 81 | TestERC20[] erc20s; 82 | TestERC721[] erc721s; 83 | TestERC1155[] erc1155s; 84 | 85 | // ExpectedBalances public balanceChecker; 86 | 87 | address[] preapprovals; 88 | 89 | string constant SINGLE_ERC721 = "single erc721"; 90 | string constant STANDARD = "standard"; 91 | string constant STANDARD_CONDUIT = "standard conduit"; 92 | string constant FULL = "full"; 93 | string constant FIRST_FIRST = "first first"; 94 | string constant FIRST_SECOND = "first second"; 95 | string constant SECOND_FIRST = "second first"; 96 | string constant SECOND_SECOND = "second second"; 97 | string constant FF_SF = "ff to sf"; 98 | string constant SF_FF = "sf to ff"; 99 | 100 | function setUp() public virtual override { 101 | super.setUp(); 102 | 103 | // balanceChecker = new ExpectedBalances(); 104 | 105 | // TODO: push to 24 if performance allows 106 | // criteriaResolverHelper = new CriteriaResolverHelper(6); 107 | 108 | preapprovals = [address(seaport), address(conduit)]; 109 | 110 | _deployTestTokenContracts(); 111 | 112 | offerer1 = makeAndAllocateAccount("alice"); 113 | offerer2 = makeAndAllocateAccount("bob"); 114 | 115 | dillon = makeAndAllocateAccount("dillon"); 116 | eve = makeAndAllocateAccount("eve"); 117 | frank = makeAndAllocateAccount("frank"); 118 | 119 | // allocate funds and tokens to test addresses 120 | allocateTokensAndApprovals(address(this), type(uint128).max); 121 | 122 | _configureStructDefaults(); 123 | 124 | fulfill = new FulfillAvailableHelper(); 125 | matcher = new MatchFulfillmentHelper(); 126 | } 127 | 128 | /** 129 | * @dev Creates a set of globally available default structs for use in 130 | * tests. 131 | */ 132 | function _configureStructDefaults() internal { 133 | OfferItemLib.empty().withItemType(ItemType.ERC721).withStartAmount(1).withEndAmount(1).saveDefault( 134 | SINGLE_ERC721 135 | ); 136 | ConsiderationItemLib.empty().withItemType(ItemType.ERC721).withStartAmount(1).withEndAmount(1).saveDefault( 137 | SINGLE_ERC721 138 | ); 139 | 140 | OrderComponentsLib.empty().withOrderType(OrderType.FULL_OPEN).withStartTime(block.timestamp).withEndTime( 141 | block.timestamp + 100 142 | ).saveDefault(STANDARD); 143 | 144 | OrderComponentsLib.fromDefault(STANDARD).withConduitKey(conduitKey).saveDefault(STANDARD_CONDUIT); 145 | 146 | AdvancedOrderLib.empty().withNumerator(1).withDenominator(1).saveDefault(FULL); 147 | 148 | FulfillmentComponentLib.empty().withOrderIndex(0).withItemIndex(0).saveDefault(FIRST_FIRST); 149 | FulfillmentComponentLib.empty().withOrderIndex(0).withItemIndex(1).saveDefault(FIRST_SECOND); 150 | FulfillmentComponentLib.empty().withOrderIndex(1).withItemIndex(0).saveDefault(SECOND_FIRST); 151 | FulfillmentComponentLib.empty().withOrderIndex(1).withItemIndex(1).saveDefault(SECOND_SECOND); 152 | 153 | SeaportArrays.FulfillmentComponents(FulfillmentComponentLib.fromDefault(FIRST_FIRST)).saveDefaultMany( 154 | FIRST_FIRST 155 | ); 156 | SeaportArrays.FulfillmentComponents(FulfillmentComponentLib.fromDefault(FIRST_SECOND)).saveDefaultMany( 157 | FIRST_SECOND 158 | ); 159 | SeaportArrays.FulfillmentComponents(FulfillmentComponentLib.fromDefault(SECOND_FIRST)).saveDefaultMany( 160 | SECOND_FIRST 161 | ); 162 | SeaportArrays.FulfillmentComponents(FulfillmentComponentLib.fromDefault(SECOND_SECOND)).saveDefaultMany( 163 | SECOND_SECOND 164 | ); 165 | 166 | FulfillmentLib.empty().withOfferComponents(FulfillmentComponentLib.fromDefaultMany(SECOND_FIRST)) 167 | .withConsiderationComponents(FulfillmentComponentLib.fromDefaultMany(FIRST_FIRST)).saveDefault(SF_FF); 168 | FulfillmentLib.empty().withOfferComponents(FulfillmentComponentLib.fromDefaultMany(FIRST_FIRST)) 169 | .withConsiderationComponents(FulfillmentComponentLib.fromDefaultMany(SECOND_FIRST)).saveDefault(FF_SF); 170 | } 171 | 172 | function test(function(Context memory) external fn, Context memory context) internal { 173 | try fn(context) { 174 | fail("Differential test should have reverted with failure status"); 175 | } catch (bytes memory reason) { 176 | assertPass(reason); 177 | } 178 | } 179 | 180 | /** 181 | * @dev Wrapper for forge-std's makeAccount that has public visibility 182 | * instead of internal visibility, so that we can access it in 183 | * libraries. 184 | */ 185 | function makeAccountWrapper(string memory name) public returns (Account memory) { 186 | return makeAccount(name); 187 | } 188 | 189 | /** 190 | * @dev Convenience wrapper for makeAddrAndKey that also allocates tokens, 191 | * ether, and approvals. 192 | */ 193 | function makeAndAllocateAccount(string memory name) internal returns (Account memory) { 194 | Account memory account = makeAccountWrapper(name); 195 | allocateTokensAndApprovals(account.addr, type(uint128).max); 196 | return account; 197 | } 198 | 199 | /** 200 | * @dev Sets up a new address and sets up token approvals for it. 201 | */ 202 | function makeAddrWithAllocationsAndApprovals(string memory label) internal returns (address) { 203 | address addr = makeAddr(label); 204 | allocateTokensAndApprovals(addr, type(uint128).max); 205 | return addr; 206 | } 207 | 208 | /** 209 | * @dev Deploy test token contracts. 210 | */ 211 | function _deployTestTokenContracts() internal { 212 | for (uint256 i; i < 3; i++) { 213 | createErc20Token(); 214 | createErc721Token(); 215 | createErc1155Token(); 216 | } 217 | // preapproved721 = new PreapprovedERC721(preapprovals); 218 | } 219 | 220 | /** 221 | * @dev Creates a new ERC20 token contract and stores it in the erc20s 222 | * array. 223 | */ 224 | function createErc20Token() internal returns (uint256 i) { 225 | i = erc20s.length; 226 | TestERC20 token = new TestERC20(); 227 | erc20s.push(token); 228 | vm.label(address(token), string.concat("ERC20", LibString.toString(i))); 229 | } 230 | 231 | /** 232 | * @dev Creates a new ERC721 token contract and stores it in the erc721s 233 | * array. 234 | */ 235 | function createErc721Token() internal returns (uint256 i) { 236 | i = erc721s.length; 237 | TestERC721 token = new TestERC721(); 238 | erc721s.push(token); 239 | vm.label(address(token), string.concat("ERC721", LibString.toString(i))); 240 | } 241 | 242 | /** 243 | * @dev Creates a new ERC1155 token contract and stores it in the erc1155s 244 | * array. 245 | */ 246 | function createErc1155Token() internal returns (uint256 i) { 247 | i = erc1155s.length; 248 | TestERC1155 token = new TestERC1155(); 249 | erc1155s.push(token); 250 | vm.label(address(token), string.concat("ERC1155", LibString.toString(i))); 251 | } 252 | 253 | /** 254 | * @dev Allocate amount of ether and each erc20 token; set approvals for all 255 | * tokens. 256 | */ 257 | function allocateTokensAndApprovals(address _to, uint128 _amount) public { 258 | vm.deal(_to, _amount); 259 | for (uint256 i = 0; i < erc20s.length; ++i) { 260 | erc20s[i].mint(_to, _amount); 261 | } 262 | _setApprovals(_to); 263 | } 264 | 265 | /** 266 | * @dev Set approvals for all tokens. 267 | * 268 | * @param _owner The address to set approvals for. 269 | */ 270 | function _setApprovals(address _owner) internal virtual { 271 | vm.startPrank(_owner); 272 | for (uint256 i = 0; i < erc20s.length; ++i) { 273 | erc20s[i].approve(address(seaport), type(uint256).max); 274 | erc20s[i].approve(address(conduit), type(uint256).max); 275 | } 276 | for (uint256 i = 0; i < erc721s.length; ++i) { 277 | erc721s[i].setApprovalForAll(address(seaport), true); 278 | erc721s[i].setApprovalForAll(address(conduit), true); 279 | } 280 | for (uint256 i = 0; i < erc1155s.length; ++i) { 281 | erc1155s[i].setApprovalForAll(address(seaport), true); 282 | erc1155s[i].setApprovalForAll(address(conduit), true); 283 | } 284 | 285 | vm.stopPrank(); 286 | } 287 | 288 | receive() external payable virtual {} 289 | } 290 | -------------------------------------------------------------------------------- /test/utils/BaseRedeemablesTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Solarray} from "solarray/Solarray.sol"; 5 | import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; 6 | import {ERC1155} from "openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol"; 7 | import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; 8 | import {IERC165} from "openzeppelin-contracts/contracts/interfaces/IERC165.sol"; 9 | import {IERC721A} from "seadrop/lib/ERC721A/contracts/IERC721A.sol"; 10 | import {IERC721} from "openzeppelin-contracts/contracts/interfaces/IERC721.sol"; 11 | import {IERC1155} from "openzeppelin-contracts/contracts/interfaces/IERC1155.sol"; 12 | import {IERC721SeaDrop} from "seadrop/src/interfaces/IERC721SeaDrop.sol"; 13 | import {IERC1155SeaDrop} from "seadrop/src/interfaces/IERC1155SeaDrop.sol"; 14 | import {BaseOrderTest} from "./BaseOrderTest.sol"; 15 | import {IERC7498} from "../../src/interfaces/IERC7498.sol"; 16 | import {TestERC20} from "../utils/mocks/TestERC20.sol"; 17 | import {TestERC721} from "../utils/mocks/TestERC721.sol"; 18 | import {TestERC1155} from "../utils/mocks/TestERC1155.sol"; 19 | import {OfferItemLib, ConsiderationItemLib} from "seaport-sol/src/SeaportSol.sol"; 20 | import {OfferItem, ConsiderationItem} from "seaport-sol/src/SeaportStructs.sol"; 21 | import {ItemType} from "seaport-sol/src/SeaportEnums.sol"; 22 | import {ERC721ShipyardRedeemableMintable} from "../../src/extensions/ERC721ShipyardRedeemableMintable.sol"; 23 | import {ERC1155ShipyardRedeemableMintable} from "../../src/extensions/ERC1155ShipyardRedeemableMintable.sol"; 24 | import {ERC721SeaDropRedeemableOwnerMintable} from "../../src/test/ERC721SeaDropRedeemableOwnerMintable.sol"; 25 | import {ERC721ShipyardRedeemableOwnerMintable} from "../../src/test/ERC721ShipyardRedeemableOwnerMintable.sol"; 26 | import {ERC1155ShipyardRedeemableOwnerMintable} from "../../src/test/ERC1155ShipyardRedeemableOwnerMintable.sol"; 27 | import {ERC1155SeaDropRedeemableOwnerMintable} from "../../src/test/ERC1155SeaDropRedeemableOwnerMintable.sol"; 28 | import {ERC721ShipyardRedeemableOwnerMintableWithoutInternalBurn} from 29 | "../../src/test/ERC721ShipyardRedeemableOwnerMintableWithoutInternalBurn.sol"; 30 | import {RedeemablesErrors} from "../../src/lib/RedeemablesErrors.sol"; 31 | import {CampaignParams, CampaignRequirements, TraitRedemption} from "../../src/lib/RedeemablesStructs.sol"; 32 | import {BURN_ADDRESS} from "../../src/lib/RedeemablesConstants.sol"; 33 | 34 | contract BaseRedeemablesTest is RedeemablesErrors, BaseOrderTest { 35 | using OfferItemLib for OfferItem; 36 | using OfferItemLib for OfferItem[]; 37 | using ConsiderationItemLib for ConsiderationItem; 38 | using ConsiderationItemLib for ConsiderationItem[]; 39 | 40 | struct RedeemablesContext { 41 | IERC7498 erc7498Token; 42 | } 43 | 44 | event Redemption( 45 | uint256 indexed campaignId, 46 | uint256 requirementsIndex, 47 | bytes32 redemptionHash, 48 | uint256[] considerationTokenIds, 49 | uint256[] traitRedemptionTokenIds, 50 | address redeemedBy 51 | ); 52 | 53 | address[] erc7498Tokens; 54 | ERC721ShipyardRedeemableOwnerMintable erc721ShipyardRedeemable; 55 | ERC721SeaDropRedeemableOwnerMintable erc721SeaDropRedeemable; 56 | ERC1155ShipyardRedeemableOwnerMintable erc1155ShipyardRedeemable; 57 | ERC1155SeaDropRedeemableOwnerMintable erc1155SeaDropRedeemable; 58 | ERC721ShipyardRedeemableOwnerMintableWithoutInternalBurn erc721ShipyardRedeemableWithoutInternalBurn; 59 | 60 | address[] receiveTokens; 61 | ERC721ShipyardRedeemableMintable receiveToken721; 62 | ERC1155ShipyardRedeemableMintable receiveToken1155; 63 | 64 | OfferItem[] defaultCampaignOffer; 65 | ConsiderationItem[] defaultCampaignConsideration; 66 | TraitRedemption[] defaultTraitRedemptions; 67 | uint256[] defaultTraitRedemptionTokenIds; 68 | 69 | string constant DEFAULT_ERC721_CAMPAIGN_OFFER = "default erc721 campaign offer"; 70 | string constant DEFAULT_ERC721_CAMPAIGN_CONSIDERATION = "default erc721 campaign consideration"; 71 | 72 | function setUp() public virtual override { 73 | super.setUp(); 74 | 75 | erc721ShipyardRedeemable = new ERC721ShipyardRedeemableOwnerMintable("", ""); 76 | erc721SeaDropRedeemable = new ERC721SeaDropRedeemableOwnerMintable(address(1), address(1), "", ""); 77 | erc1155ShipyardRedeemable = new ERC1155ShipyardRedeemableOwnerMintable("", ""); 78 | erc1155SeaDropRedeemable = new ERC1155SeaDropRedeemableOwnerMintable(address(1), address(1), "", ""); 79 | erc721ShipyardRedeemableWithoutInternalBurn = 80 | new ERC721ShipyardRedeemableOwnerMintableWithoutInternalBurn("", ""); 81 | // Not using internal burn needs approval for the contract itself to transfer tokens on users' behalf. 82 | erc721ShipyardRedeemableWithoutInternalBurn.setApprovalForAll( 83 | address(erc721ShipyardRedeemableWithoutInternalBurn), true 84 | ); 85 | 86 | erc721SeaDropRedeemable.setMaxSupply(10); 87 | erc1155SeaDropRedeemable.setMaxSupply(1, 10); 88 | erc1155SeaDropRedeemable.setMaxSupply(2, 10); 89 | erc1155SeaDropRedeemable.setMaxSupply(3, 10); 90 | 91 | erc7498Tokens = new address[](5); 92 | erc7498Tokens[0] = address(erc721ShipyardRedeemable); 93 | erc7498Tokens[1] = address(erc721SeaDropRedeemable); 94 | erc7498Tokens[2] = address(erc1155ShipyardRedeemable); 95 | erc7498Tokens[3] = address(erc1155SeaDropRedeemable); 96 | erc7498Tokens[4] = address(erc721ShipyardRedeemableWithoutInternalBurn); 97 | vm.label(erc7498Tokens[0], "erc721ShipyardRedeemable"); 98 | vm.label(erc7498Tokens[1], "erc721SeaDropRedeemable"); 99 | vm.label(erc7498Tokens[2], "erc1155ShipyardRedeemable"); 100 | vm.label(erc7498Tokens[3], "erc1155SeaDropRedeemable"); 101 | vm.label(erc7498Tokens[4], "erc721ShipyardRedeemableWithoutInternalBurn"); 102 | 103 | receiveToken721 = new ERC721ShipyardRedeemableMintable("", ""); 104 | receiveToken1155 = new ERC1155ShipyardRedeemableMintable("", ""); 105 | receiveTokens = new address[](2); 106 | receiveTokens[0] = address(receiveToken721); 107 | receiveTokens[1] = address(receiveToken1155); 108 | vm.label(receiveTokens[0], "erc721ShipyardRedeemableMintable"); 109 | vm.label(receiveTokens[1], "erc1155ShipyardRedeemableMintable"); 110 | for (uint256 i = 0; i < receiveTokens.length; ++i) { 111 | ERC721ShipyardRedeemableMintable(receiveTokens[i]).setRedeemablesContracts(erc7498Tokens); 112 | assertEq(ERC721ShipyardRedeemableMintable(receiveTokens[i]).getRedeemablesContracts(), erc7498Tokens); 113 | } 114 | 115 | _setApprovals(address(this)); 116 | 117 | // Save the default campaign offer and consideration 118 | OfferItemLib.fromDefault(SINGLE_ERC721).withToken(address(receiveToken721)).withItemType( 119 | ItemType.ERC721_WITH_CRITERIA 120 | ).saveDefault(DEFAULT_ERC721_CAMPAIGN_OFFER); 121 | ConsiderationItemLib.fromDefault(SINGLE_ERC721).withToken(address(erc7498Tokens[0])).withRecipient(BURN_ADDRESS) 122 | .withItemType(ItemType.ERC721_WITH_CRITERIA).saveDefault(DEFAULT_ERC721_CAMPAIGN_CONSIDERATION); 123 | defaultCampaignOffer.push(OfferItemLib.fromDefault(DEFAULT_ERC721_CAMPAIGN_OFFER)); 124 | defaultCampaignConsideration.push(ConsiderationItemLib.fromDefault(DEFAULT_ERC721_CAMPAIGN_CONSIDERATION)); 125 | } 126 | 127 | function testRedeemable(function(RedeemablesContext memory) external fn, RedeemablesContext memory context) 128 | internal 129 | { 130 | fn(context); 131 | } 132 | 133 | function _setApprovals(address _owner) internal virtual override { 134 | vm.startPrank(_owner); 135 | for (uint256 i = 0; i < erc20s.length; ++i) { 136 | for (uint256 j = 0; j < erc7498Tokens.length; ++j) { 137 | erc20s[i].approve(address(erc7498Tokens[j]), type(uint256).max); 138 | } 139 | } 140 | for (uint256 i = 0; i < erc721s.length; ++i) { 141 | for (uint256 j = 0; j < erc7498Tokens.length; ++j) { 142 | erc721s[i].setApprovalForAll(address(erc7498Tokens[j]), true); 143 | } 144 | } 145 | for (uint256 i = 0; i < erc1155s.length; ++i) { 146 | for (uint256 j = 0; j < erc7498Tokens.length; ++j) { 147 | erc1155s[i].setApprovalForAll(address(erc7498Tokens[j]), true); 148 | } 149 | } 150 | vm.stopPrank(); 151 | } 152 | 153 | function _isSeaDrop(address token) internal view returns (bool isSeaDrop) { 154 | if ( 155 | IERC165(token).supportsInterface(type(IERC721SeaDrop).interfaceId) 156 | || IERC165(token).supportsInterface(type(IERC1155SeaDrop).interfaceId) 157 | ) { 158 | isSeaDrop = true; 159 | } 160 | } 161 | 162 | function _getCampaignConsiderationItem(address token) 163 | internal 164 | view 165 | returns (ConsiderationItem memory considerationItem) 166 | { 167 | considerationItem = defaultCampaignConsideration[0].withToken(token).withItemType( 168 | _isERC721(address(token)) ? ItemType.ERC721_WITH_CRITERIA : ItemType.ERC1155_WITH_CRITERIA 169 | ); 170 | } 171 | 172 | function _checkTokenDoesNotExist(address token, uint256 tokenId) internal { 173 | if (_isERC721(token)) { 174 | try IERC721(address(token)).ownerOf(tokenId) returns (address owner) { 175 | assertEq(owner, address(BURN_ADDRESS)); 176 | } catch {} 177 | } else { 178 | // token is ERC1155 179 | assertEq(IERC1155(address(token)).balanceOf(address(this), tokenId), 0); 180 | } 181 | } 182 | 183 | function _checkTokenSentToBurnAddress(address token, uint256 tokenId) internal { 184 | if (_isERC721(token)) { 185 | assertEq(IERC721(address(token)).ownerOf(tokenId), BURN_ADDRESS); 186 | } else { 187 | // token is ERC1155 188 | assertEq(IERC1155(address(token)).balanceOf(address(this), tokenId), 0); 189 | } 190 | } 191 | 192 | function _checkTokenIsOwnedBy(address token, uint256 tokenId, address owner) internal { 193 | if (_isERC721(token)) { 194 | assertEq(IERC721(address(token)).ownerOf(tokenId), owner); 195 | } else if (_isERC20(token)) { 196 | assertGt(IERC20(address(token)).balanceOf(owner), 0); 197 | } else { 198 | // token is ERC1155 199 | assertGt(IERC1155(address(token)).balanceOf(owner, tokenId), 0); 200 | } 201 | } 202 | 203 | function _mintToken(address token, uint256 tokenId) internal { 204 | if (_isERC721(token)) { 205 | ERC721ShipyardRedeemableOwnerMintable(address(token)).mint(address(this), tokenId); 206 | } else { 207 | // token is ERC1155 208 | ERC1155ShipyardRedeemableOwnerMintable(address(token)).mint(address(this), tokenId, 1); 209 | } 210 | } 211 | 212 | function _mintToken(address token, uint256 tokenId, address recipient) internal { 213 | if (_isERC721(token)) { 214 | ERC721ShipyardRedeemableOwnerMintable(address(token)).mint(recipient, tokenId); 215 | } else { 216 | // token is ERC1155 217 | ERC1155ShipyardRedeemableOwnerMintable(address(token)).mint(recipient, tokenId, 1); 218 | } 219 | } 220 | 221 | function _isERC721(address token) internal view returns (bool isERC721) { 222 | isERC721 = IERC165(token).supportsInterface(type(IERC721).interfaceId); 223 | } 224 | 225 | function _isERC20(address token) internal view returns (bool isERC20) { 226 | isERC20 = IERC165(token).supportsInterface(type(IERC20).interfaceId); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /test/utils/BaseSeaportTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {stdStorage, StdStorage} from "forge-std/Test.sol"; 5 | import {DifferentialTest} from "./DifferentialTest.sol"; 6 | import {ConduitControllerInterface} from "seaport-sol/src/ConduitControllerInterface.sol"; 7 | import {ConduitController} from "seaport-core/src/conduit/ConduitController.sol"; 8 | import {ConsiderationInterface} from "seaport-types/src/interfaces/ConsiderationInterface.sol"; 9 | import {Consideration} from "seaport-core/src/lib/Consideration.sol"; 10 | import {Conduit} from "seaport-core/src/conduit/Conduit.sol"; 11 | 12 | /// @dev Base test case that deploys Consideration and its dependencies. 13 | contract BaseSeaportTest is DifferentialTest { 14 | using stdStorage for StdStorage; 15 | 16 | bool coverage_or_debug; 17 | bytes32 conduitKey; 18 | 19 | Conduit conduit; 20 | Conduit referenceConduit; 21 | ConduitControllerInterface conduitController; 22 | ConsiderationInterface seaport; 23 | 24 | function stringEq(string memory a, string memory b) internal pure returns (bool) { 25 | return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b)); 26 | } 27 | 28 | function debugEnabled() internal returns (bool) { 29 | return vm.envOr("SEAPORT_COVERAGE", false) || debugProfileEnabled(); 30 | } 31 | 32 | function debugProfileEnabled() internal returns (bool) { 33 | string memory env = vm.envOr("FOUNDRY_PROFILE", string("")); 34 | return stringEq(env, "debug") || stringEq(env, "moat_debug"); 35 | } 36 | 37 | function setUp() public virtual { 38 | // Conditionally deploy contracts normally or from precompiled source 39 | // deploys normally when SEAPORT_COVERAGE is true for coverage analysis 40 | // or when FOUNDRY_PROFILE is "debug" for debugging with source maps 41 | // deploys from precompiled source when both are false. 42 | coverage_or_debug = debugEnabled(); 43 | 44 | conduitKey = bytes32(uint256(uint160(address(this))) << 96); 45 | _deployAndConfigurePrecompiledOptimizedConsideration(); 46 | 47 | vm.label(address(conduitController), "conduitController"); 48 | vm.label(address(seaport), "seaport"); 49 | vm.label(address(conduit), "conduit"); 50 | vm.label(address(this), "testContract"); 51 | } 52 | 53 | /** 54 | * @dev Get the configured preferred Seaport 55 | */ 56 | function getSeaport() internal view returns (ConsiderationInterface seaport_) { 57 | seaport_ = seaport; 58 | } 59 | 60 | /** 61 | * @dev Get the configured preferred ConduitController 62 | */ 63 | function getConduitController() internal view returns (ConduitControllerInterface conduitController_) { 64 | conduitController_ = conduitController; 65 | } 66 | 67 | ///@dev deploy optimized consideration contracts from pre-compiled source 68 | // (solc-0.8.19, IR pipeline enabled, unless running coverage or debug) 69 | function _deployAndConfigurePrecompiledOptimizedConsideration() public { 70 | conduitController = new ConduitController(); 71 | seaport = new Consideration(address(conduitController)); 72 | 73 | //create conduit, update channel 74 | conduit = Conduit(conduitController.createConduit(conduitKey, address(this))); 75 | conduitController.updateChannel(address(conduit), address(seaport), true); 76 | } 77 | 78 | function signOrder(ConsiderationInterface _consideration, uint256 _pkOfSigner, bytes32 _orderHash) 79 | internal 80 | view 81 | returns (bytes memory) 82 | { 83 | (bytes32 r, bytes32 s, uint8 v) = getSignatureComponents(_consideration, _pkOfSigner, _orderHash); 84 | return abi.encodePacked(r, s, v); 85 | } 86 | 87 | function getSignatureComponents(ConsiderationInterface _consideration, uint256 _pkOfSigner, bytes32 _orderHash) 88 | internal 89 | view 90 | returns (bytes32, bytes32, uint8) 91 | { 92 | (, bytes32 domainSeparator,) = _consideration.information(); 93 | (uint8 v, bytes32 r, bytes32 s) = 94 | vm.sign(_pkOfSigner, keccak256(abi.encodePacked(bytes2(0x1901), domainSeparator, _orderHash))); 95 | return (r, s, v); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /test/utils/DifferentialTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | 6 | contract DifferentialTest is Test { 7 | ///@dev error to supply 8 | error RevertWithFailureStatus(bool status); 9 | error DifferentialTestAssertionFailed(); 10 | 11 | // slot where HEVM stores a bool representing whether or not an assertion has failed 12 | bytes32 HEVM_FAILED_SLOT = bytes32("failed"); 13 | 14 | // hash of the bytes surfaced by `revert RevertWithFailureStatus(false)` 15 | bytes32 PASSING_HASH = keccak256(abi.encodeWithSelector(RevertWithFailureStatus.selector, false)); 16 | 17 | ///@dev reverts after function body with HEVM failure status, which clears all state changes 18 | /// but still surfaces assertion failure status. 19 | modifier stateless() { 20 | _; 21 | revert RevertWithFailureStatus(readHevmFailureSlot()); 22 | } 23 | 24 | ///@dev revert if the supplied bytes do not match the expected "passing" revert bytes 25 | function assertPass(bytes memory reason) internal view { 26 | // hash the reason and compare to the hash of the passing revert bytes 27 | if (keccak256(reason) != PASSING_HASH) { 28 | revert DifferentialTestAssertionFailed(); 29 | } 30 | } 31 | 32 | ///@dev read the failure slot of the HEVM using the vm.load cheatcode 33 | /// Returns true if there was an assertion failure. recorded. 34 | function readHevmFailureSlot() internal view returns (bool) { 35 | return vm.load(address(vm), HEVM_FAILED_SLOT) == bytes32(uint256(1)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/utils/mocks/ERC1155Recipient.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity ^0.8.19; 3 | 4 | import {IERC1155Receiver} from "../../../src/interfaces/IERC1155Receiver.sol"; 5 | 6 | contract ERC1155Recipient is IERC1155Receiver { 7 | function onERC1155Received(address, address, uint256, uint256, bytes calldata) 8 | public 9 | virtual 10 | override 11 | returns (bytes4) 12 | { 13 | return IERC1155Receiver.onERC1155Received.selector; 14 | } 15 | 16 | function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) 17 | external 18 | virtual 19 | override 20 | returns (bytes4) 21 | { 22 | return IERC1155Receiver.onERC1155BatchReceived.selector; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/utils/mocks/ERC721Recipient.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity ^0.8.19; 3 | 4 | import {IERC721Receiver} from "seaport-types/src/interfaces/IERC721Receiver.sol"; 5 | 6 | contract ERC721Recipient is IERC721Receiver { 7 | function onERC721Received(address, address, uint256, bytes calldata) public virtual override returns (bytes4) { 8 | return IERC721Receiver.onERC721Received.selector; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/utils/mocks/MockERC1271Wallet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import "solady/src/utils/ECDSA.sol"; 5 | 6 | contract MockERC1271Wallet { 7 | address signer; 8 | 9 | constructor(address signer_) { 10 | signer = signer_; 11 | } 12 | 13 | function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { 14 | return ECDSA.recover(hash, signature) == signer ? bytes4(0x1626ba7e) : bytes4(0); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/utils/mocks/MockERC721DynamicTraits.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC721} from "solady/src/tokens/ERC721.sol"; 5 | 6 | contract MockERC721DynamicTraits is ERC721 { 7 | error InvalidCaller(); 8 | 9 | // The manager account that can set traits 10 | address public _manager; 11 | 12 | // The dynamic traits 13 | mapping(bytes32 traitKey => mapping(uint256 tokenId => bytes32 value)) public traits; 14 | 15 | // Set the manager at construction 16 | constructor(address manager) { 17 | _manager = manager; 18 | } 19 | 20 | function mint(address to, uint256 tokenId) public { 21 | _mint(to, tokenId); 22 | } 23 | 24 | function tokenURI(uint256) public pure override returns (string memory) { 25 | return "tokenURI"; 26 | } 27 | 28 | function name() public view virtual override returns (string memory) { 29 | return "TestERC721"; 30 | } 31 | 32 | function symbol() public view virtual override returns (string memory) { 33 | return "TST721"; 34 | } 35 | 36 | function getTrait(bytes32 traitKey, uint256 tokenId) public view returns (bytes32) { 37 | return traits[traitKey][tokenId]; 38 | } 39 | 40 | function getTraits(bytes32 traitKey, uint256[] calldata tokenIds) public view returns (bytes32[] memory) { 41 | bytes32[] memory values = new bytes32[](tokenIds.length); 42 | for (uint256 i = 0; i < tokenIds.length; i++) { 43 | values[i] = traits[traitKey][tokenIds[i]]; 44 | } 45 | return values; 46 | } 47 | 48 | function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) public { 49 | if (msg.sender != _manager) { 50 | revert InvalidCaller(); 51 | } 52 | traits[traitKey][tokenId] = value; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/utils/mocks/TestERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC1155} from "solady/src/tokens/ERC1155.sol"; 5 | 6 | // Used for minting test ERC1155s in our tests 7 | contract TestERC1155 is ERC1155 { 8 | function mint(address to, uint256 tokenId, uint256 amount) public returns (bool) { 9 | _mint(to, tokenId, amount, ""); 10 | return true; 11 | } 12 | 13 | function uri(uint256) public pure override returns (string memory) { 14 | return "uri"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/utils/mocks/TestERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.19; 3 | 4 | import {IERC165} from "openzeppelin-contracts/contracts/interfaces/IERC165.sol"; 5 | import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; 6 | import {ERC20} from "solady/src/tokens/ERC20.sol"; 7 | 8 | // Used for minting test ERC20s in our tests 9 | contract TestERC20 is ERC20 { 10 | bool public blocked; 11 | 12 | bool public noReturnData; 13 | 14 | constructor() { 15 | blocked = false; 16 | noReturnData = false; 17 | } 18 | 19 | function blockTransfer(bool blocking) external { 20 | blocked = blocking; 21 | } 22 | 23 | function setNoReturnData(bool noReturn) external { 24 | noReturnData = noReturn; 25 | } 26 | 27 | function mint(address to, uint256 amount) external returns (bool) { 28 | _mint(to, amount); 29 | return true; 30 | } 31 | 32 | function transferFrom(address from, address to, uint256 amount) public override returns (bool ok) { 33 | if (blocked) { 34 | return false; 35 | } 36 | 37 | uint256 allowed = allowance(from, msg.sender); 38 | 39 | if (amount > allowed) { 40 | revert("NOT_AUTHORIZED"); 41 | } 42 | 43 | super.transferFrom(from, to, amount); 44 | 45 | if (noReturnData) { 46 | assembly { 47 | return(0, 0) 48 | } 49 | } 50 | 51 | ok = true; 52 | } 53 | 54 | function name() public view virtual override returns (string memory) { 55 | return "TestERC20"; 56 | } 57 | 58 | function symbol() public view virtual override returns (string memory) { 59 | return "TST20"; 60 | } 61 | 62 | function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { 63 | return interfaceId == type(IERC20).interfaceId; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/utils/mocks/TestERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC721} from "solady/src/tokens/ERC721.sol"; 5 | 6 | // Used for minting test ERC721s in our tests 7 | contract TestERC721 is ERC721 { 8 | function mint(address to, uint256 tokenId) public { 9 | _mint(to, tokenId); 10 | } 11 | 12 | function tokenURI(uint256) public pure override returns (string memory) { 13 | return "tokenURI"; 14 | } 15 | 16 | function name() public view virtual override returns (string memory) { 17 | return "TestERC721"; 18 | } 19 | 20 | function symbol() public view virtual override returns (string memory) { 21 | return "TST721"; 22 | } 23 | } 24 | --------------------------------------------------------------------------------