diff --git a/script/DeployAndRedeemTrait.s.sol b/script/DeployAndRedeemTrait.s.sol index 8ef6356..f213f73 100644 --- a/script/DeployAndRedeemTrait.s.sol +++ b/script/DeployAndRedeemTrait.s.sol @@ -24,12 +24,13 @@ contract DeployAndRedeemTrait is Script, Test { address[] memory allowedTraitSetters = new address[](1); allowedTraitSetters[0] = address(receiveToken); - // deploy the redeem token with the receive token as an allowed trait setter + // deploy the redeem token ERC721ShipyardRedeemableTraitSetters redeemToken = new ERC721ShipyardRedeemableTraitSetters( "DynamicTraitsRedeemToken", - "TEST", - allowedTraitSetters + "TEST" ); + // set the receive token as an allowed trait setter + redeemToken.setAllowedTraitSetters(allowedTraitSetters); // configure the campaign. OfferItem[] memory offer = new OfferItem[](1); diff --git a/src/lib/ERC7498NFTRedeemables.sol b/src/lib/ERC7498NFTRedeemables.sol index 9fab898..dc600ba 100644 --- a/src/lib/ERC7498NFTRedeemables.sol +++ b/src/lib/ERC7498NFTRedeemables.sol @@ -461,12 +461,13 @@ contract ERC7498NFTRedeemables is IERC165, IERC7498, DynamicTraits, RedeemablesE // Decrement the trait by the trait value. IERC7496(token).setTrait(identifier, traitRedemptions[i].traitKey, bytes32(newTraitValue)); } else if (substandard == 4) { - // Revert if the current trait value is not equal to the substandard value. - if (currentTraitValue != substandardValue) { + // Revert if the current trait value is not equal to the trait value. + if (currentTraitValue != traitValue) { revert InvalidRequiredTraitValue( token, identifier, traitKey, currentTraitValue, substandardValue ); } + // No-op: substandard 4 has no set trait action. } } diff --git a/src/lib/SignedRedeem.sol b/src/lib/SignedRedeem.sol.txt similarity index 100% rename from src/lib/SignedRedeem.sol rename to src/lib/SignedRedeem.sol.txt diff --git a/src/lib/SignedRedeemContractOfferer.sol b/src/lib/SignedRedeemContractOfferer.sol.txt similarity index 100% rename from src/lib/SignedRedeemContractOfferer.sol rename to src/lib/SignedRedeemContractOfferer.sol.txt diff --git a/src/test/ERC721ShipyardRedeemableOwnerMintableWithoutInternalBurn.sol b/src/test/ERC721ShipyardRedeemableOwnerMintableWithoutInternalBurn.sol new file mode 100644 index 0000000..0b61e7f --- /dev/null +++ b/src/test/ERC721ShipyardRedeemableOwnerMintableWithoutInternalBurn.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ERC7498NFTRedeemables} from "../lib/ERC7498NFTRedeemables.sol"; +import {ERC721ShipyardRedeemableOwnerMintable} from "./ERC721ShipyardRedeemableOwnerMintable.sol"; + +contract ERC721ShipyardRedeemableOwnerMintableWithoutInternalBurn is ERC721ShipyardRedeemableOwnerMintable { + constructor(string memory name_, string memory symbol_) ERC721ShipyardRedeemableOwnerMintable(name_, symbol_) {} + + function _useInternalBurn() internal pure virtual override returns (bool) { + // For coverage of ERC7498NFTRedeemables._useInternalBurn, return default value of false. + return ERC7498NFTRedeemables._useInternalBurn(); + } +} diff --git a/src/test/ERC721ShipyardRedeemableTraitSetters.sol b/src/test/ERC721ShipyardRedeemableTraitSetters.sol index c20c18c..542e630 100644 --- a/src/test/ERC721ShipyardRedeemableTraitSetters.sol +++ b/src/test/ERC721ShipyardRedeemableTraitSetters.sol @@ -12,11 +12,7 @@ contract ERC721ShipyardRedeemableTraitSetters is ERC721ShipyardRedeemableOwnerMi // ERC721ShipyardRedeemable and ERC721SeaDropRedeemable contracts with onlyOwner on setAllowedTraitSetters(). address[] _allowedTraitSetters; - constructor(string memory name_, string memory symbol_, address[] memory allowedTraitSetters) - ERC721ShipyardRedeemableOwnerMintable(name_, symbol_) - { - _allowedTraitSetters = allowedTraitSetters; - } + constructor(string memory name_, string memory symbol_) ERC721ShipyardRedeemableOwnerMintable(name_, symbol_) {} function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) public virtual override { if (!_exists(tokenId)) revert TokenDoesNotExist(); @@ -26,6 +22,14 @@ contract ERC721ShipyardRedeemableTraitSetters is ERC721ShipyardRedeemableOwnerMi DynamicTraits.setTrait(tokenId, traitKey, value); } + function getAllowedTraitSetters() public view returns (address[] memory) { + return _allowedTraitSetters; + } + + function setAllowedTraitSetters(address[] memory allowedTraitSetters) public onlyOwner { + _allowedTraitSetters = allowedTraitSetters; + } + function _requireAllowedTraitSetter() internal view { // Allow the contract to call itself. if (msg.sender == address(this)) return; diff --git a/test/ERC7498-TraitRedemption.t.sol b/test/ERC7498-TraitRedemption.t.sol index 90e135b..36a1266 100644 --- a/test/ERC7498-TraitRedemption.t.sol +++ b/test/ERC7498-TraitRedemption.t.sol @@ -55,7 +55,7 @@ contract ERC7498_TraitRedemption is BaseRedeemablesTest { assertEq(traitValues[0], bytes32(uint256(1))); } - function testErc721TraitRedemptionForErc721() public { + function testErc721TraitRedemptionSubstandardOneForErc721() public { for (uint256 i; i < erc7498Tokens.length; i++) { testRedeemable( this.erc721TraitRedemptionSubstandardOneForErc721, @@ -65,13 +65,14 @@ contract ERC7498_TraitRedemption is BaseRedeemablesTest { } function erc721TraitRedemptionSubstandardOneForErc721(RedeemablesContext memory context) public { - address[] memory allowedTraitSetters = new address[](1); - allowedTraitSetters[0] = address(context.erc7498Token); + address[] memory allowedTraitSetters = Solarray.addresses(address(context.erc7498Token)); ERC721ShipyardRedeemableTraitSetters redeemToken = new ERC721ShipyardRedeemableTraitSetters( "", - "", - allowedTraitSetters + "" ); + assertEq(redeemToken.getAllowedTraitSetters(), new address[](0)); + redeemToken.setAllowedTraitSetters(allowedTraitSetters); + assertEq(redeemToken.getAllowedTraitSetters(), allowedTraitSetters); _mintToken(address(redeemToken), tokenId); TraitRedemption[] memory traitRedemptions = new TraitRedemption[](1); // previous trait value (`substandardValue`) should be 0 @@ -144,4 +145,256 @@ contract ERC7498_TraitRedemption is BaseRedeemablesTest { ); context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); } + + function testErc721TraitRedemptionSubstandardTwoForErc721() public { + for (uint256 i; i < erc7498Tokens.length; i++) { + testRedeemable( + this.erc721TraitRedemptionSubstandardTwoForErc721, + RedeemablesContext({erc7498Token: IERC7498(erc7498Tokens[i])}) + ); + } + } + + function erc721TraitRedemptionSubstandardTwoForErc721(RedeemablesContext memory context) public { + address[] memory allowedTraitSetters = Solarray.addresses(address(context.erc7498Token), address(this)); + ERC721ShipyardRedeemableTraitSetters redeemToken = new ERC721ShipyardRedeemableTraitSetters( + "", + "" + ); + redeemToken.setAllowedTraitSetters(allowedTraitSetters); + _mintToken(address(redeemToken), tokenId); + redeemToken.setTrait(tokenId, traitKey, bytes32(uint256(1))); + TraitRedemption[] memory traitRedemptions = new TraitRedemption[](1); + // previous trait value should not be greater than 1 (`substandardValue`) + // new trait value should be 2 (adding traitValue of 1) + traitRedemptions[0] = TraitRedemption({ + substandard: 2, + token: address(redeemToken), + traitKey: traitKey, + traitValue: bytes32(uint256(1)), + substandardValue: bytes32(uint256(1)) + }); + CampaignRequirements[] memory requirements = new CampaignRequirements[]( + 1 + ); + // consideration is empty + ConsiderationItem[] memory consideration = new ConsiderationItem[](0); + requirements[0] = CampaignRequirements({ + offer: defaultCampaignOffer, + consideration: consideration, + traitRedemptions: traitRedemptions + }); + CampaignParams memory params = CampaignParams({ + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this), + signer: address(0) + }); + Campaign memory campaign = Campaign({params: params, requirements: requirements}); + context.erc7498Token.createCampaign(campaign, ""); + + uint256[] memory considerationTokenIds; + uint256[] memory traitRedemptionTokenIds = Solarray.uint256s(tokenId); + bytes memory extraData = abi.encode( + 1, // campaignId + 0, // requirementsIndex + bytes32(0), // redemptionHash + traitRedemptionTokenIds, + uint256(0), // salt + bytes("") // signature + ); + vm.expectEmit(true, true, true, true); + emit Redemption(1, 0, bytes32(0), considerationTokenIds, traitRedemptionTokenIds, address(this)); + context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); + + bytes32 actualTraitValue = redeemToken.getTraitValue(tokenId, traitKey); + assertEq(bytes32(uint256(2)), actualTraitValue); + assertEq(receiveToken721.ownerOf(1), address(this)); + + // Redeeming one more time should fail with InvalidRequiredTraitValue since it is already 2. + vm.expectRevert( + abi.encodeWithSelector( + InvalidRequiredTraitValue.selector, + redeemToken, + tokenId, + traitKey, + bytes32(uint256(2)), + bytes32(uint256(1)) + ) + ); + context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); + } + + function testErc721TraitRedemptionSubstandardThreeForErc721() public { + for (uint256 i; i < erc7498Tokens.length; i++) { + testRedeemable( + this.erc721TraitRedemptionSubstandardThreeForErc721, + RedeemablesContext({erc7498Token: IERC7498(erc7498Tokens[i])}) + ); + } + } + + function erc721TraitRedemptionSubstandardThreeForErc721(RedeemablesContext memory context) public { + address[] memory allowedTraitSetters = Solarray.addresses(address(context.erc7498Token), address(this)); + ERC721ShipyardRedeemableTraitSetters redeemToken = new ERC721ShipyardRedeemableTraitSetters( + "", + "" + ); + redeemToken.setAllowedTraitSetters(allowedTraitSetters); + _mintToken(address(redeemToken), tokenId); + redeemToken.setTrait(tokenId, traitKey, bytes32(uint256(5))); + TraitRedemption[] memory traitRedemptions = new TraitRedemption[](1); + // previous trait value should not be less than 4 (`substandardValue`) + // new trait value should be 4 (adding traitValue of 1) + traitRedemptions[0] = TraitRedemption({ + substandard: 3, + token: address(redeemToken), + traitKey: traitKey, + traitValue: bytes32(uint256(1)), + substandardValue: bytes32(uint256(5)) + }); + CampaignRequirements[] memory requirements = new CampaignRequirements[]( + 1 + ); + // consideration is empty + ConsiderationItem[] memory consideration = new ConsiderationItem[](0); + requirements[0] = CampaignRequirements({ + offer: defaultCampaignOffer, + consideration: consideration, + traitRedemptions: traitRedemptions + }); + CampaignParams memory params = CampaignParams({ + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this), + signer: address(0) + }); + Campaign memory campaign = Campaign({params: params, requirements: requirements}); + context.erc7498Token.createCampaign(campaign, ""); + + uint256[] memory considerationTokenIds; + uint256[] memory traitRedemptionTokenIds = Solarray.uint256s(tokenId); + bytes memory extraData = abi.encode( + 1, // campaignId + 0, // requirementsIndex + bytes32(0), // redemptionHash + traitRedemptionTokenIds, + uint256(0), // salt + bytes("") // signature + ); + vm.expectEmit(true, true, true, true); + emit Redemption(1, 0, bytes32(0), considerationTokenIds, traitRedemptionTokenIds, address(this)); + context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); + + bytes32 actualTraitValue = redeemToken.getTraitValue(tokenId, traitKey); + assertEq(bytes32(uint256(4)), actualTraitValue); + assertEq(receiveToken721.ownerOf(1), address(this)); + + // Redeeming one more time should fail with InvalidRequiredTraitValue since it is now 4. + vm.expectRevert( + abi.encodeWithSelector( + InvalidRequiredTraitValue.selector, + redeemToken, + tokenId, + traitKey, + bytes32(uint256(4)), + bytes32(uint256(5)) + ) + ); + context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); + } + + function testErc721TraitRedemptionSubstandardFourForErc721() public { + for (uint256 i; i < erc7498Tokens.length; i++) { + testRedeemable( + this.erc721TraitRedemptionSubstandardFourForErc721, + RedeemablesContext({erc7498Token: IERC7498(erc7498Tokens[i])}) + ); + } + } + + function erc721TraitRedemptionSubstandardFourForErc721(RedeemablesContext memory context) public { + address[] memory allowedTraitSetters = Solarray.addresses(address(context.erc7498Token), address(this)); + ERC721ShipyardRedeemableTraitSetters redeemToken = new ERC721ShipyardRedeemableTraitSetters( + "", + "" + ); + redeemToken.setAllowedTraitSetters(allowedTraitSetters); + _mintToken(address(redeemToken), tokenId); + redeemToken.setTrait(tokenId, traitKey, bytes32(uint256(4))); + TraitRedemption[] memory traitRedemptions = new TraitRedemption[](1); + // previous trait value should be the trait value + // trait value does not change in substandard 4 + traitRedemptions[0] = TraitRedemption({ + substandard: 4, + token: address(redeemToken), + traitKey: traitKey, + traitValue: bytes32(uint256(5)), + substandardValue: bytes32(0) // unused in substandard 4 + }); + CampaignRequirements[] memory requirements = new CampaignRequirements[]( + 1 + ); + // consideration is empty + ConsiderationItem[] memory consideration = new ConsiderationItem[](0); + requirements[0] = CampaignRequirements({ + offer: defaultCampaignOffer, + consideration: consideration, + traitRedemptions: traitRedemptions + }); + CampaignParams memory params = CampaignParams({ + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this), + signer: address(0) + }); + Campaign memory campaign = Campaign({params: params, requirements: requirements}); + context.erc7498Token.createCampaign(campaign, ""); + + uint256[] memory considerationTokenIds; + uint256[] memory traitRedemptionTokenIds = Solarray.uint256s(tokenId); + bytes memory extraData = abi.encode( + 1, // campaignId + 0, // requirementsIndex + bytes32(0), // redemptionHash + traitRedemptionTokenIds, + uint256(0), // salt + bytes("") // signature + ); + + // Redeeming should fail since the trait value does not match. + vm.expectRevert( + abi.encodeWithSelector( + InvalidRequiredTraitValue.selector, + redeemToken, + tokenId, + traitKey, + bytes32(uint256(4)), + bytes32(uint256(0)) + ) + ); + context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); + + // Update the trait value, now it should match. + redeemToken.setTrait(tokenId, traitKey, bytes32(uint256(5))); + + vm.expectEmit(true, true, true, true); + emit Redemption(1, 0, bytes32(0), considerationTokenIds, traitRedemptionTokenIds, address(this)); + context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); + + bytes32 actualTraitValue = redeemToken.getTraitValue(tokenId, traitKey); + assertEq(bytes32(uint256(5)), actualTraitValue); + assertEq(receiveToken721.ownerOf(1), address(this)); + + // Redeeming one more time should succeed since it has not changed. + emit Redemption(1, 0, bytes32(0), considerationTokenIds, traitRedemptionTokenIds, address(this)); + context.erc7498Token.redeem(considerationTokenIds, address(this), extraData); + + actualTraitValue = redeemToken.getTraitValue(tokenId, traitKey); + assertEq(bytes32(uint256(5)), actualTraitValue); + assertEq(receiveToken721.ownerOf(2), address(this)); + } } diff --git a/test/utils/BaseRedeemablesTest.sol b/test/utils/BaseRedeemablesTest.sol index a487103..908f92e 100644 --- a/test/utils/BaseRedeemablesTest.sol +++ b/test/utils/BaseRedeemablesTest.sol @@ -25,6 +25,8 @@ import {ERC721SeaDropRedeemableOwnerMintable} from "../../src/test/ERC721SeaDrop import {ERC721ShipyardRedeemableOwnerMintable} from "../../src/test/ERC721ShipyardRedeemableOwnerMintable.sol"; import {ERC1155ShipyardRedeemableOwnerMintable} from "../../src/test/ERC1155ShipyardRedeemableOwnerMintable.sol"; import {ERC1155SeaDropRedeemableOwnerMintable} from "../../src/test/ERC1155SeaDropRedeemableOwnerMintable.sol"; +import {ERC721ShipyardRedeemableOwnerMintableWithoutInternalBurn} from + "../../src/test/ERC721ShipyardRedeemableOwnerMintableWithoutInternalBurn.sol"; import {RedeemablesErrors} from "../../src/lib/RedeemablesErrors.sol"; import {CampaignParams, CampaignRequirements, TraitRedemption} from "../../src/lib/RedeemablesStructs.sol"; import {BURN_ADDRESS} from "../../src/lib/RedeemablesConstants.sol"; @@ -48,13 +50,12 @@ contract BaseRedeemablesTest is RedeemablesErrors, BaseOrderTest { address redeemedBy ); - bytes32 private constant CAMPAIGN_PARAMS_MAP_POSITION = keccak256("CampaignParamsDefault"); - address[] erc7498Tokens; ERC721ShipyardRedeemableOwnerMintable erc721ShipyardRedeemable; ERC721SeaDropRedeemableOwnerMintable erc721SeaDropRedeemable; ERC1155ShipyardRedeemableOwnerMintable erc1155ShipyardRedeemable; ERC1155SeaDropRedeemableOwnerMintable erc1155SeaDropRedeemable; + ERC721ShipyardRedeemableOwnerMintableWithoutInternalBurn erc721ShipyardRedeemableWithoutInternalBurn; address[] receiveTokens; ERC721ShipyardRedeemableMintable receiveToken721; @@ -65,9 +66,6 @@ contract BaseRedeemablesTest is RedeemablesErrors, BaseOrderTest { TraitRedemption[] defaultTraitRedemptions; uint256[] defaultTraitRedemptionTokenIds; - CampaignRequirements[] defaultCampaignRequirements; - // CampaignParams defaultCampaignParams; - string constant DEFAULT_ERC721_CAMPAIGN_OFFER = "default erc721 campaign offer"; string constant DEFAULT_ERC721_CAMPAIGN_CONSIDERATION = "default erc721 campaign consideration"; @@ -94,21 +92,31 @@ contract BaseRedeemablesTest is RedeemablesErrors, BaseOrderTest { "", "" ); + erc721ShipyardRedeemableWithoutInternalBurn = new ERC721ShipyardRedeemableOwnerMintableWithoutInternalBurn( + "", + "" + ); + // Not using internal burn needs approval for the contract itself to transfer tokens on users' behalf. + erc721ShipyardRedeemableWithoutInternalBurn.setApprovalForAll( + address(erc721ShipyardRedeemableWithoutInternalBurn), true + ); erc721SeaDropRedeemable.setMaxSupply(10); erc1155SeaDropRedeemable.setMaxSupply(1, 10); erc1155SeaDropRedeemable.setMaxSupply(2, 10); erc1155SeaDropRedeemable.setMaxSupply(3, 10); - erc7498Tokens = new address[](4); + erc7498Tokens = new address[](5); erc7498Tokens[0] = address(erc721ShipyardRedeemable); erc7498Tokens[1] = address(erc721SeaDropRedeemable); erc7498Tokens[2] = address(erc1155ShipyardRedeemable); erc7498Tokens[3] = address(erc1155SeaDropRedeemable); + erc7498Tokens[4] = address(erc721ShipyardRedeemableWithoutInternalBurn); vm.label(erc7498Tokens[0], "erc721ShipyardRedeemable"); vm.label(erc7498Tokens[1], "erc721SeaDropRedeemable"); vm.label(erc7498Tokens[2], "erc1155ShipyardRedeemable"); vm.label(erc7498Tokens[3], "erc1155SeaDropRedeemable"); + vm.label(erc7498Tokens[4], "erc721ShipyardRedeemableWithoutInternalBurn"); receiveToken721 = new ERC721ShipyardRedeemableMintable("", ""); receiveToken1155 = new ERC1155ShipyardRedeemableMintable("", ""); @@ -140,13 +148,6 @@ contract BaseRedeemablesTest is RedeemablesErrors, BaseOrderTest { fn(context); } - function _campaignParamsMap() private pure returns (mapping(string => CampaignParams) storage campaignParamsMap) { - bytes32 position = CAMPAIGN_PARAMS_MAP_POSITION; - assembly { - campaignParamsMap.slot := position - } - } - function _setApprovals(address _owner) internal virtual override { vm.startPrank(_owner); for (uint256 i = 0; i < erc20s.length; ++i) {