Skip to content

Commit 2337863

Browse files
Updatable/Freezable Metadata: BatchMintMetada Extension (#513)
* initial edits to BatchMintMetadata.sol * additional updates to batchmint metadata and droperc721 * edits to batchmintmetadata and drop prebuilts * fix freeze function in upgradeable + add event * nit: natspec * relint * update dynamic-contract dependency --------- Co-authored-by: nkrishang <[email protected]>
1 parent 77f4594 commit 2337863

File tree

7 files changed

+319
-10
lines changed

7 files changed

+319
-10
lines changed

contracts/extension/BatchMintMetadata.sol

+41-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@ contract BatchMintMetadata {
1717
/// @dev Mapping from id of a batch of tokens => to base URI for the respective batch of tokens.
1818
mapping(uint256 => string) private baseURI;
1919

20+
/// @dev Mapping from id of a batch of tokens => to whether the base URI for the respective batch of tokens is frozen.
21+
mapping(uint256 => bool) public batchFrozen;
22+
23+
/// @dev This event emits when the metadata of all tokens are frozen.
24+
/// While not currently supported by marketplaces, this event allows
25+
/// future indexing if desired.
26+
event MetadataFrozen();
27+
28+
// @dev This event emits when the metadata of a range of tokens is updated.
29+
/// So that the third-party platforms such as NFT market could
30+
/// timely update the images and related attributes of the NFTs.
31+
event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId);
32+
2033
/**
2134
* @notice Returns the count of batches of NFTs.
2235
* @dev Each batch of tokens has an in ID and an associated `baseURI`.
@@ -27,9 +40,9 @@ contract BatchMintMetadata {
2740
}
2841

2942
/**
30-
* @notice Returns the ID for the batch of tokens the given tokenId belongs to.
43+
* @notice Returns the ID for the batch of tokens at the given index.
3144
* @dev See {getBaseURICount}.
32-
* @param _index ID of a token.
45+
* @param _index Index of the desired batch in batchIds array.
3346
*/
3447
function getBatchIdAtIndex(uint256 _index) public view returns (uint256) {
3548
if (_index >= getBaseURICount()) {
@@ -68,9 +81,35 @@ contract BatchMintMetadata {
6881
revert("Invalid tokenId");
6982
}
7083

84+
/// @dev returns the starting tokenId of a given batchId.
85+
function _getBatchStartId(uint256 _batchID) internal view returns (uint256) {
86+
uint256 numOfTokenBatches = getBaseURICount();
87+
uint256[] memory indices = batchIds;
88+
89+
for (uint256 i = 0; i < numOfTokenBatches; i++) {
90+
if (_batchID == indices[i]) {
91+
if (i > 0) {
92+
return indices[i - 1];
93+
}
94+
return 0;
95+
}
96+
}
97+
revert("Invalid batchId");
98+
}
99+
71100
/// @dev Sets the base URI for the batch of tokens with the given batchId.
72101
function _setBaseURI(uint256 _batchId, string memory _baseURI) internal {
102+
require(!batchFrozen[_batchId], "Batch frozen");
73103
baseURI[_batchId] = _baseURI;
104+
emit BatchMetadataUpdate(_getBatchStartId(_batchId), _batchId);
105+
}
106+
107+
/// @dev Freezes the base URI for the batch of tokens with the given batchId.
108+
function _freezeBaseURI(uint256 _batchId) internal {
109+
string memory baseURIForBatch = baseURI[_batchId];
110+
require(bytes(baseURIForBatch).length > 0, "Invalid batch");
111+
batchFrozen[_batchId] = true;
112+
emit MetadataFrozen();
74113
}
75114

76115
/// @dev Mints a batch of tokenIds and associates a common baseURI to all those Ids.

contracts/extension/upgradeable/BatchMintMetadata.sol

+38
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ library BatchMintMetadataStorage {
1111
uint256[] batchIds;
1212
/// @dev Mapping from id of a batch of tokens => to base URI for the respective batch of tokens.
1313
mapping(uint256 => string) baseURI;
14+
/// @dev Mapping from id of a batch of tokens => to whether the base URI for the respective batch of tokens is frozen.
15+
mapping(uint256 => bool) batchFrozen;
1416
}
1517

1618
function data() internal pure returns (Data storage data_) {
@@ -29,6 +31,16 @@ library BatchMintMetadataStorage {
2931
*/
3032

3133
contract BatchMintMetadata {
34+
/// @dev This event emits when the metadata of all tokens are frozen.
35+
/// While not currently supported by marketplaces, this event allows
36+
/// future indexing if desired.
37+
event MetadataFrozen();
38+
39+
// @dev This event emits when the metadata of a range of tokens is updated.
40+
/// So that the third-party platforms such as NFT market could
41+
/// timely update the images and related attributes of the NFTs.
42+
event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId);
43+
3244
/**
3345
* @notice Returns the count of batches of NFTs.
3446
* @dev Each batch of tokens has an in ID and an associated `baseURI`.
@@ -80,9 +92,35 @@ contract BatchMintMetadata {
8092
revert("Invalid tokenId");
8193
}
8294

95+
/// @dev returns the starting tokenId of a given batchId.
96+
function _getBatchStartId(uint256 _batchID) internal view returns (uint256) {
97+
uint256 numOfTokenBatches = getBaseURICount();
98+
uint256[] memory indices = _batchMintMetadataStorage().batchIds;
99+
100+
for (uint256 i = 0; i < numOfTokenBatches; i++) {
101+
if (_batchID == indices[i]) {
102+
if (i > 0) {
103+
return indices[i - 1];
104+
}
105+
return 0;
106+
}
107+
}
108+
revert("Invalid batchId");
109+
}
110+
83111
/// @dev Sets the base URI for the batch of tokens with the given batchId.
84112
function _setBaseURI(uint256 _batchId, string memory _baseURI) internal {
113+
require(!_batchMintMetadataStorage().batchFrozen[_batchId], "Batch frozen");
85114
_batchMintMetadataStorage().baseURI[_batchId] = _baseURI;
115+
emit BatchMetadataUpdate(_getBatchStartId(_batchId), _batchId);
116+
}
117+
118+
/// @dev Freezes the base URI for the batch of tokens with the given batchId.
119+
function _freezeBaseURI(uint256 _batchId) internal {
120+
string memory baseURIForBatch = _batchMintMetadataStorage().baseURI[_batchId];
121+
require(bytes(baseURIForBatch).length > 0, "Invalid batch");
122+
_batchMintMetadataStorage().batchFrozen[_batchId] = true;
123+
emit MetadataFrozen();
86124
}
87125

88126
/// @dev Mints a batch of tokenIds and associates a common baseURI to all those Ids.

contracts/prebuilts/drop/DropERC1155.sol

+27
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ contract DropERC1155 is
6666
bytes32 private transferRole;
6767
/// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s and lazy mint tokens.
6868
bytes32 private minterRole;
69+
/// @dev Only METADATA_ROLE holders can reveal the URI for a batch of delayed reveal NFTs, and update or freeze batch metadata.
70+
bytes32 private metadataRole;
6971

7072
/// @dev Max bps in the thirdweb system.
7173
uint256 private constant MAX_BPS = 10_000;
@@ -114,6 +116,7 @@ contract DropERC1155 is
114116
) external initializer {
115117
bytes32 _transferRole = keccak256("TRANSFER_ROLE");
116118
bytes32 _minterRole = keccak256("MINTER_ROLE");
119+
bytes32 _metadataRole = keccak256("METADATA_ROLE");
117120

118121
// Initialize inherited contracts, most base-like -> most derived.
119122
__ERC2771Context_init(_trustedForwarders);
@@ -127,13 +130,16 @@ contract DropERC1155 is
127130
_setupRole(_minterRole, _defaultAdmin);
128131
_setupRole(_transferRole, _defaultAdmin);
129132
_setupRole(_transferRole, address(0));
133+
_setupRole(_metadataRole, _defaultAdmin);
134+
_setRoleAdmin(_metadataRole, _metadataRole);
130135

131136
_setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps);
132137
_setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps);
133138
_setupPrimarySaleRecipient(_saleRecipient);
134139

135140
transferRole = _transferRole;
136141
minterRole = _minterRole;
142+
metadataRole = _metadataRole;
137143
name = _name;
138144
symbol = _symbol;
139145
}
@@ -187,6 +193,27 @@ contract DropERC1155 is
187193
emit SaleRecipientForTokenUpdated(_tokenId, _saleRecipient);
188194
}
189195

196+
/**
197+
* @notice Updates the base URI for a batch of tokens.
198+
*
199+
* @param _index Index of the desired batch in batchIds array.
200+
* @param _uri the new base URI for the batch.
201+
*/
202+
function updateBatchBaseURI(uint256 _index, string calldata _uri) external onlyRole(metadataRole) {
203+
uint256 batchId = getBatchIdAtIndex(_index);
204+
_setBaseURI(batchId, _uri);
205+
}
206+
207+
/**
208+
* @notice Freezes the base URI for a batch of tokens.
209+
*
210+
* @param _index Index of the desired batch in batchIds array.
211+
*/
212+
function freezeBatchBaseURI(uint256 _index) external onlyRole(metadataRole) {
213+
uint256 batchId = getBatchIdAtIndex(_index);
214+
_freezeBaseURI(batchId);
215+
}
216+
190217
/*///////////////////////////////////////////////////////////////
191218
Internal functions
192219
//////////////////////////////////////////////////////////////*/

contracts/prebuilts/drop/DropERC721.sol

+34-5
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ contract DropERC721 is
6262
bytes32 private transferRole;
6363
/// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s and lazy mint tokens.
6464
bytes32 private minterRole;
65+
/// @dev Only METADATA_ROLE holders can reveal the URI for a batch of delayed reveal NFTs, and update or freeze batch metadata.
66+
bytes32 private metadataRole;
6567

6668
/// @dev Max bps in the thirdweb system.
6769
uint256 private constant MAX_BPS = 10_000;
@@ -93,6 +95,7 @@ contract DropERC721 is
9395
) external initializer {
9496
bytes32 _transferRole = keccak256("TRANSFER_ROLE");
9597
bytes32 _minterRole = keccak256("MINTER_ROLE");
98+
bytes32 _metadataRole = keccak256("METADATA_ROLE");
9699

97100
// Initialize inherited contracts, most base-like -> most derived.
98101
__ERC2771Context_init(_trustedForwarders);
@@ -105,13 +108,16 @@ contract DropERC721 is
105108
_setupRole(_minterRole, _defaultAdmin);
106109
_setupRole(_transferRole, _defaultAdmin);
107110
_setupRole(_transferRole, address(0));
111+
_setupRole(_metadataRole, _defaultAdmin);
112+
_setRoleAdmin(_metadataRole, _metadataRole);
108113

109114
_setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps);
110115
_setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps);
111116
_setupPrimarySaleRecipient(_saleRecipient);
112117

113118
transferRole = _transferRole;
114119
minterRole = _minterRole;
120+
metadataRole = _metadataRole;
115121
}
116122

117123
/*///////////////////////////////////////////////////////////////
@@ -176,10 +182,12 @@ contract DropERC721 is
176182
return super.lazyMint(_amount, _baseURIForTokens, _data);
177183
}
178184

179-
/// @dev Lets an account with `MINTER_ROLE` reveal the URI for a batch of 'delayed-reveal' NFTs.
185+
/// @dev Lets an account with `METADATA_ROLE` reveal the URI for a batch of 'delayed-reveal' NFTs.
186+
/// @param _index the ID of a token with the desired batch.
187+
/// @param _key the key to decrypt the batch's URI.
180188
function reveal(uint256 _index, bytes calldata _key)
181189
external
182-
onlyRole(minterRole)
190+
onlyRole(metadataRole)
183191
returns (string memory revealedURI)
184192
{
185193
uint256 batchId = getBatchIdAtIndex(_index);
@@ -191,6 +199,29 @@ contract DropERC721 is
191199
emit TokenURIRevealed(_index, revealedURI);
192200
}
193201

202+
/**
203+
* @notice Updates the base URI for a batch of tokens. Can only be called if the batch has been revealed/is not encrypted.
204+
*
205+
* @param _index Index of the desired batch in batchIds array
206+
* @param _uri the new base URI for the batch.
207+
*/
208+
function updateBatchBaseURI(uint256 _index, string calldata _uri) external onlyRole(metadataRole) {
209+
require(!isEncryptedBatch(getBatchIdAtIndex(_index)), "Encrypted batch");
210+
uint256 batchId = getBatchIdAtIndex(_index);
211+
_setBaseURI(batchId, _uri);
212+
}
213+
214+
/**
215+
* @notice Freezes the base URI for a batch of tokens.
216+
*
217+
* @param _index Index of the desired batch in batchIds array.
218+
*/
219+
function freezeBatchBaseURI(uint256 _index) external onlyRole(metadataRole) {
220+
require(!isEncryptedBatch(getBatchIdAtIndex(_index)), "Encrypted batch");
221+
uint256 batchId = getBatchIdAtIndex(_index);
222+
_freezeBaseURI(batchId);
223+
}
224+
194225
/*///////////////////////////////////////////////////////////////
195226
Setter functions
196227
//////////////////////////////////////////////////////////////*/
@@ -302,9 +333,7 @@ contract DropERC721 is
302333
* Returns the total amount of tokens minted in the contract.
303334
*/
304335
function totalMinted() external view returns (uint256) {
305-
unchecked {
306-
return _currentIndex - _startTokenId();
307-
}
336+
return _totalMinted();
308337
}
309338

310339
/// @dev The tokenId of the next NFT that will be minted / lazy minted.

src/test/drop/DropERC1155.t.sol

+61
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ contract DropERC1155Test is BaseTest {
1515

1616
event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI);
1717
event TokenURIRevealed(uint256 indexed index, string revealedURI);
18+
event MaxTotalSupplyUpdated(uint256 tokenId, uint256 maxTotalSupply);
1819

1920
DropERC1155 public drop;
2021

@@ -909,6 +910,66 @@ contract DropERC1155Test is BaseTest {
909910
assertEq(drop.getActiveClaimConditionId(_tokenId), 2);
910911
}
911912

913+
/*///////////////////////////////////////////////////////////////
914+
Unit Test: updateBatchBaseURI
915+
//////////////////////////////////////////////////////////////*/
916+
917+
function test_state_updateBatchBaseURI() public {
918+
string memory initURI = "ipfs://init";
919+
string memory newURI = "ipfs://new";
920+
921+
vm.startPrank(deployer);
922+
drop.lazyMint(100, initURI, "");
923+
924+
string memory initTokenURI = drop.uri(0);
925+
926+
assertEq(initTokenURI, string(abi.encodePacked(initURI, "0")));
927+
928+
drop.updateBatchBaseURI(0, newURI);
929+
930+
string memory newTokenURI = drop.uri(0);
931+
932+
assertEq(newTokenURI, string(abi.encodePacked(newURI, "0")));
933+
}
934+
935+
/*///////////////////////////////////////////////////////////////
936+
Unit Test: freezeBatchBaseURI
937+
//////////////////////////////////////////////////////////////*/
938+
939+
function test_state_freezeBatchBaseURI() public {
940+
string memory initURI = "ipfs://init";
941+
942+
vm.startPrank(deployer);
943+
drop.lazyMint(100, initURI, "");
944+
945+
string memory initTokenURI = drop.uri(0);
946+
947+
assertEq(initTokenURI, string(abi.encodePacked(initURI, "0")));
948+
949+
drop.freezeBatchBaseURI(0);
950+
951+
assertEq(drop.batchFrozen(100), true);
952+
}
953+
954+
/*///////////////////////////////////////////////////////////////
955+
Unit Test: setMaxTotalSupply
956+
//////////////////////////////////////////////////////////////*/
957+
958+
function test_state_setMaxTotalSupply() public {
959+
vm.startPrank(deployer);
960+
drop.setMaxTotalSupply(1, 100);
961+
962+
assertEq(drop.maxTotalSupply(1), 100);
963+
}
964+
965+
function test_event_setMaxTotalSupply_MaxTotalSupplyUpdated() public {
966+
vm.startPrank(deployer);
967+
968+
vm.expectEmit(true, false, false, true);
969+
emit MaxTotalSupplyUpdated(1, 100);
970+
drop.setMaxTotalSupply(1, 100);
971+
}
972+
912973
/*///////////////////////////////////////////////////////////////
913974
Miscellaneous
914975
//////////////////////////////////////////////////////////////*/

0 commit comments

Comments
 (0)