Skip to content
This repository was archived by the owner on Aug 20, 2025. It is now read-only.

Commit 22e1e6b

Browse files
authored
Merge pull request #15 from ProjectOpenSea/stephan/dynamic-traits
add trait redemptions to ERC7498Redeemables
2 parents 432be56 + 6860365 commit 22e1e6b

24 files changed

+704
-333
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
- name: Run Forge build
3131
run: |
3232
forge --version
33-
forge build --sizes
33+
forge build
3434
id: build
3535

3636
- name: Run Forge tests

script/DeployAndConfigure1155Receive.s.sol

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@ import {Test} from "forge-std/Test.sol";
66
import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol";
77
import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol";
88
import {CampaignParams, CampaignRequirements} from "../src/lib/RedeemablesStructs.sol";
9+
import {BURN_ADDRESS} from "../src/lib/RedeemablesConstants.sol";
910
import {ERC721RedemptionMintable} from "../src/extensions/ERC721RedemptionMintable.sol";
1011
import {ERC721OwnerMintable} from "../src/test/ERC721OwnerMintable.sol";
1112
import {ERC1155ShipyardRedeemableMintable} from "../src/extensions/ERC1155ShipyardRedeemableMintable.sol";
1213

1314
contract DeployAndConfigure1155Receive is Script, Test {
14-
address constant _BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD;
15-
1615
function run() external {
1716
vm.startBroadcast();
1817

@@ -51,7 +50,7 @@ contract DeployAndConfigure1155Receive is Script, Test {
5150
identifierOrCriteria: 0,
5251
startAmount: 1,
5352
endAmount: 1,
54-
recipient: payable(_BURN_ADDRESS)
53+
recipient: payable(BURN_ADDRESS)
5554
});
5655

5756
CampaignRequirements[] memory requirements = new CampaignRequirements[](
@@ -68,8 +67,7 @@ contract DeployAndConfigure1155Receive is Script, Test {
6867
maxCampaignRedemptions: 1_000,
6968
manager: msg.sender
7069
});
71-
uint256 campaignId =
72-
receiveToken.createCampaign(params, "ipfs://QmQjubc6guHReNW5Es5ZrgDtJRwXk2Aia7BkVoLJGaCRqP");
70+
receiveToken.createCampaign(params, "ipfs://QmQjubc6guHReNW5Es5ZrgDtJRwXk2Aia7BkVoLJGaCRqP");
7371

7472
// To test updateCampaign, update to proper start/end times.
7573
params.startTime = uint32(block.timestamp);

script/DeployAndConfigureExampleCampaign.s.sol.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol";
77
import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol";
88
import {RedeemableContractOfferer} from "../src/RedeemableContractOfferer.sol";
99
import {CampaignParams} from "../src/lib/RedeemablesStructs.sol";
10+
import {BURN_ADDRESS} from "../src/lib/RedeemablesConstants.sol";
1011
import {ERC721RedemptionMintable} from "../src/extensions/ERC721RedemptionMintable.sol";
1112
import {TestERC721} from "../test/utils/mocks/TestERC721.sol";
1213

@@ -16,8 +17,6 @@ contract DeployAndConfigureExampleCampaign is Script {
1617
address conduit = 0x1E0049783F008A0085193E00003D00cd54003c71;
1718
bytes32 conduitKey = 0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000;
1819

19-
address constant _BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD;
20-
2120
function run() external {
2221
vm.startBroadcast();
2322

@@ -49,7 +48,7 @@ contract DeployAndConfigureExampleCampaign is Script {
4948
identifierOrCriteria: 0,
5049
startAmount: 1,
5150
endAmount: 1,
52-
recipient: payable(_BURN_ADDRESS)
51+
recipient: payable(BURN_ADDRESS)
5352
});
5453

5554
CampaignParams memory params = CampaignParams({

script/DeployAndRedeemTokens-CampaignOnReceiveToken.s.sol

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@ import {Test} from "forge-std/Test.sol";
66
import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol";
77
import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol";
88
import {CampaignParams, CampaignRequirements} from "../src/lib/RedeemablesStructs.sol";
9+
import {BURN_ADDRESS} from "../src/lib/RedeemablesConstants.sol";
910
import {ERC721RedemptionMintable} from "../src/extensions/ERC721RedemptionMintable.sol";
1011
import {ERC721OwnerMintable} from "../src/test/ERC721OwnerMintable.sol";
1112
import {ERC721ShipyardRedeemableMintable} from "../src/extensions/ERC721ShipyardRedeemableMintable.sol";
1213

1314
contract DeployAndRedeemTokens_CampaignOnReceiveToken is Script, Test {
14-
address constant _BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD;
15-
1615
function run() external {
1716
vm.startBroadcast();
1817

@@ -37,7 +36,7 @@ contract DeployAndRedeemTokens_CampaignOnReceiveToken is Script, Test {
3736
identifierOrCriteria: 0,
3837
startAmount: 1,
3938
endAmount: 1,
40-
recipient: payable(_BURN_ADDRESS)
39+
recipient: payable(BURN_ADDRESS)
4140
});
4241

4342
CampaignRequirements[] memory requirements = new CampaignRequirements[](
@@ -62,8 +61,10 @@ contract DeployAndRedeemTokens_CampaignOnReceiveToken is Script, Test {
6261

6362
// Let's redeem them!
6463
uint256 requirementsIndex = 0;
65-
bytes32 redemptionHash = bytes32(0);
66-
bytes memory data = abi.encode(campaignId, requirementsIndex, redemptionHash);
64+
bytes32 redemptionHash;
65+
uint256 salt;
66+
bytes memory signature;
67+
bytes memory data = abi.encode(campaignId, requirementsIndex, redemptionHash, salt, signature);
6768

6869
uint256[] memory tokenIds = new uint256[](1);
6970
tokenIds[0] = 1;

script/DeployAndRedeemTokens.s.sol

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@ import {Test} from "forge-std/Test.sol";
66
import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol";
77
import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol";
88
import {CampaignParams, CampaignRequirements} from "../src/lib/RedeemablesStructs.sol";
9+
import {BURN_ADDRESS} from "../src/lib/RedeemablesConstants.sol";
910
import {ERC721RedemptionMintable} from "../src/extensions/ERC721RedemptionMintable.sol";
1011
import {ERC721ShipyardRedeemableOwnerMintable} from "../src/test/ERC721ShipyardRedeemableOwnerMintable.sol";
1112

1213
contract DeployAndRedeemTokens is Script, Test {
13-
address constant _BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD;
14-
1514
function run() external {
1615
vm.startBroadcast();
1716

@@ -44,7 +43,7 @@ contract DeployAndRedeemTokens is Script, Test {
4443
identifierOrCriteria: 0,
4544
startAmount: 1,
4645
endAmount: 1,
47-
recipient: payable(_BURN_ADDRESS)
46+
recipient: payable(BURN_ADDRESS)
4847
});
4948

5049
CampaignRequirements[] memory requirements = new CampaignRequirements[](
@@ -69,8 +68,10 @@ contract DeployAndRedeemTokens is Script, Test {
6968
// Let's redeem them!
7069
uint256 campaignId = 1;
7170
uint256 requirementsIndex = 0;
72-
bytes32 redemptionHash = bytes32(0);
73-
bytes memory data = abi.encode(campaignId, requirementsIndex, redemptionHash);
71+
bytes32 redemptionHash;
72+
uint256 salt;
73+
bytes memory signature;
74+
bytes memory data = abi.encode(campaignId, requirementsIndex, redemptionHash, salt, signature);
7475

7576
uint256[] memory tokenIds = new uint256[](1);
7677
tokenIds[0] = 1;

script/DeployAndRedeemTrait.s.sol

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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 {CampaignParams, CampaignRequirements, TraitRedemption} from "../src/lib/RedeemablesStructs.sol";
10+
import {ERC721RedemptionMintable} from "../src/extensions/ERC721RedemptionMintable.sol";
11+
import {ERC721ShipyardRedeemableMintable} from "../src/extensions/ERC721ShipyardRedeemableMintable.sol";
12+
import {ERC721ShipyardRedeemablePreapprovedTraitSetters} from
13+
"../src/test/ERC721ShipyardRedeemablePreapprovedTraitSetters.sol";
14+
15+
contract DeployAndRedeemTrait is Script, Test {
16+
function run() external {
17+
vm.startBroadcast();
18+
19+
// deploy the receive token first
20+
ERC721ShipyardRedeemableMintable receiveToken = new ERC721ShipyardRedeemableMintable(
21+
"TestRedeemablesRecieveToken",
22+
"TEST"
23+
);
24+
25+
// add the receive token address to allowed trait setters array
26+
address[] memory allowedTraitSetters = new address[](1);
27+
allowedTraitSetters[0] = address(receiveToken);
28+
29+
// deploy the redeem token with the receive token as an allowed trait setter
30+
ERC721ShipyardRedeemablePreapprovedTraitSetters redeemToken =
31+
new ERC721ShipyardRedeemablePreapprovedTraitSetters(
32+
"DynamicTraitsRedeemToken",
33+
"TEST",
34+
allowedTraitSetters
35+
);
36+
37+
// configure the campaign.
38+
OfferItem[] memory offer = new OfferItem[](1);
39+
40+
// offer is receive token
41+
offer[0] = OfferItem({
42+
itemType: ItemType.ERC721_WITH_CRITERIA,
43+
token: address(receiveToken),
44+
identifierOrCriteria: 0,
45+
startAmount: 1,
46+
endAmount: 1
47+
});
48+
49+
// consideration is empty
50+
ConsiderationItem[] memory consideration = new ConsiderationItem[](0);
51+
52+
TraitRedemption[] memory traitRedemptions = new TraitRedemption[](1);
53+
54+
// trait key is "hasRedeemed"
55+
bytes32 traitKey = bytes32("hasRedeemed");
56+
57+
// previous trait value (`substandardValue`) should be 0
58+
bytes32 substandardValue = bytes32(uint256(0));
59+
60+
// new trait value should be 1
61+
bytes32 traitValue = bytes32(uint256(1));
62+
63+
traitRedemptions[0] = TraitRedemption({
64+
substandard: 1,
65+
token: address(redeemToken),
66+
traitKey: traitKey,
67+
traitValue: traitValue,
68+
substandardValue: substandardValue
69+
});
70+
71+
CampaignRequirements[] memory requirements = new CampaignRequirements[](
72+
1
73+
);
74+
requirements[0].offer = offer;
75+
requirements[0].consideration = consideration;
76+
requirements[0].traitRedemptions = traitRedemptions;
77+
78+
CampaignParams memory params = CampaignParams({
79+
requirements: requirements,
80+
signer: address(0),
81+
startTime: uint32(block.timestamp),
82+
endTime: uint32(block.timestamp + 1_000_000),
83+
maxCampaignRedemptions: 1_000,
84+
manager: msg.sender
85+
});
86+
receiveToken.createCampaign(params, "");
87+
88+
// Mint token 1 to redeem for token 1.
89+
redeemToken.mint(msg.sender, 1);
90+
91+
// Let's redeem them!
92+
uint256[] memory traitRedemptionTokenIds = new uint256[](1);
93+
traitRedemptionTokenIds[0] = 1;
94+
95+
bytes memory data = abi.encode(
96+
1, // campaignId
97+
0, // requirementsIndex
98+
bytes32(0), // redemptionHash
99+
traitRedemptionTokenIds,
100+
uint256(0), // salt
101+
bytes("") // signature
102+
);
103+
104+
receiveToken.redeem(new uint256[](0), msg.sender, data);
105+
106+
// Assert new trait has been set and redemption token is minted.
107+
bytes32 actualTraitValue = IERC7496(address(redeemToken)).getTraitValue(1, traitKey);
108+
// "hasRedeemed" should be 1 (true)
109+
assertEq(bytes32(uint256(1)), actualTraitValue);
110+
assertEq(receiveToken.ownerOf(1), msg.sender);
111+
}
112+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 {BURN_ADDRESS} from "../src/lib/RedeemablesConstants.sol";
10+
import {ERC721RedemptionMintable} from "../src/extensions/ERC721RedemptionMintable.sol";
11+
import {ERC721OwnerMintable} from "../src/test/ERC721OwnerMintable.sol";
12+
import {ERC721ShipyardRedeemableMintable} from "../src/extensions/ERC721ShipyardRedeemableMintable.sol";
13+
14+
contract DeployERC721ReceiveTokenWithPredeployedSeaDropRedeemToken is Script, Test {
15+
function run() external {
16+
vm.startBroadcast();
17+
18+
ERC721ShipyardRedeemableMintable redeemToken =
19+
ERC721ShipyardRedeemableMintable(0xa1783E74857736b2AEE610A36b537B31CC333048);
20+
ERC721ShipyardRedeemableMintable receiveToken =
21+
ERC721ShipyardRedeemableMintable(0x343B9aEC7fAB02d07c6747Bace112920822334B4);
22+
23+
// Configure the campaign.
24+
OfferItem[] memory offer = new OfferItem[](1);
25+
offer[0] = OfferItem({
26+
itemType: ItemType.ERC721_WITH_CRITERIA,
27+
token: address(receiveToken),
28+
identifierOrCriteria: 0,
29+
startAmount: 1,
30+
endAmount: 1
31+
});
32+
33+
ConsiderationItem[] memory consideration = new ConsiderationItem[](1);
34+
consideration[0] = ConsiderationItem({
35+
itemType: ItemType.ERC721_WITH_CRITERIA,
36+
token: address(redeemToken),
37+
identifierOrCriteria: 0,
38+
startAmount: 1,
39+
endAmount: 1,
40+
recipient: payable(BURN_ADDRESS)
41+
});
42+
43+
CampaignRequirements[] memory requirements = new CampaignRequirements[](
44+
1
45+
);
46+
requirements[0].offer = offer;
47+
requirements[0].consideration = consideration;
48+
49+
CampaignParams memory params = CampaignParams({
50+
requirements: requirements,
51+
signer: address(0),
52+
startTime: uint32(block.timestamp),
53+
endTime: uint32(block.timestamp + 1_000_000),
54+
maxCampaignRedemptions: 1_000,
55+
manager: msg.sender
56+
});
57+
uint256 campaignId =
58+
receiveToken.createCampaign(params, "ipfs://QmQKc93y2Ev5k9Kz54mCw48ZM487bwGDktZYPLtrjJ3r1d");
59+
60+
// redeemToken.setBaseURI(
61+
// "ipfs://QmYTSupCtriDLBHgPBBhZ98wYdp6N9S8jTL5sKSZwbASeT"
62+
// );
63+
64+
// receiveToken.setBaseURI(
65+
// "ipfs://QmWxgnz8T9wsMBmpCY4Cvanj3RR1obFD2hqDKPZhKN5Tsq/"
66+
// );
67+
68+
// Let's redeem them!
69+
uint256 requirementsIndex = 0;
70+
bytes32 redemptionHash;
71+
uint256 salt;
72+
bytes memory signature;
73+
bytes memory data = abi.encode(campaignId, requirementsIndex, redemptionHash, salt, signature);
74+
75+
uint256[] memory tokenIds = new uint256[](1);
76+
tokenIds[0] = 1;
77+
78+
redeemToken.setApprovalForAll(address(receiveToken), true);
79+
80+
receiveToken.redeem(tokenIds, msg.sender, data);
81+
82+
// Assert redeemable token is burned and redemption token is minted.
83+
assertEq(redeemToken.balanceOf(msg.sender), 2);
84+
assertEq(receiveToken.ownerOf(1), msg.sender);
85+
}
86+
}

script/RedeemTokens.s.sol

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@ contract RedeemTokens is Script, Test {
1414
function run() external {
1515
vm.startBroadcast();
1616

17-
address redeemToken = 0x1eCC76De3f9E4e9f8378f6ade61A02A10f976c45;
17+
// address redeemToken = 0x1eCC76De3f9E4e9f8378f6ade61A02A10f976c45;
1818
ERC1155ShipyardRedeemableMintable receiveToken =
1919
ERC1155ShipyardRedeemableMintable(0x3D0fa2a8D07dfe357905a4cB4ed51b0Aea8385B9);
2020

2121
// Let's redeem them!
2222
uint256 campaignId = 1;
2323
uint256 requirementsIndex = 0;
24-
bytes32 redemptionHash = bytes32(0);
25-
bytes memory data = abi.encode(campaignId, requirementsIndex, redemptionHash);
24+
bytes32 redemptionHash;
25+
uint256 salt;
26+
bytes memory signature;
27+
bytes memory data = abi.encode(campaignId, requirementsIndex, redemptionHash, salt, signature);
2628

2729
uint256[] memory redeemTokenIds = new uint256[](1);
2830
redeemTokenIds[0] = 1;

src/ERC721ShipyardRedeemable.sol

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ contract ERC721ShipyardRedeemable is ERC721ShipyardContractMetadata, ERC7498NFTR
4444
return true;
4545
}
4646

47-
function _internalBurn(address, /* from */ uint256 id, uint256 /* amount */ ) internal virtual override {
47+
function _internalBurn(
48+
address,
49+
/* from */
50+
uint256 id,
51+
uint256 /* amount */
52+
) internal virtual override {
4853
_burn(id);
4954
}
5055

src/extensions/ERC1155RedemptionMintable.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ contract ERC1155RedemptionMintable is ERC1155ShipyardContractMetadata, IRedempti
2727
function mintRedemption(
2828
uint256, /* campaignId */
2929
address recipient,
30-
ConsiderationItem[] calldata consideration,
30+
ConsiderationItem[] calldata, /* consideration */
3131
TraitRedemption[] calldata /* traitRedemptions */
3232
) external {
3333
bool validSender;

0 commit comments

Comments
 (0)