From c324d4b399a3acbafea5e59392aa57b69a2de8b0 Mon Sep 17 00:00:00 2001 From: Stanley Date: Tue, 3 Sep 2024 23:16:41 -0400 Subject: [PATCH 1/3] implmented delayed functionality into batchMetadata --- .../token/metadata/BatchMetadataERC721.sol | 45 ++- .../DelayedRevealBatchMetadataERC721.sol | 285 ------------------ .../token/metadata/SimpleMetadataERC1155.sol | 25 -- .../token/metadata/SimpleMetadataERC721.sol | 85 ------ 4 files changed, 44 insertions(+), 396 deletions(-) delete mode 100644 src/module/token/metadata/DelayedRevealBatchMetadataERC721.sol delete mode 100644 src/module/token/metadata/SimpleMetadataERC1155.sol delete mode 100644 src/module/token/metadata/SimpleMetadataERC721.sol diff --git a/src/module/token/metadata/BatchMetadataERC721.sol b/src/module/token/metadata/BatchMetadataERC721.sol index 290cdc71..46f77c77 100644 --- a/src/module/token/metadata/BatchMetadataERC721.sol +++ b/src/module/token/metadata/BatchMetadataERC721.sol @@ -86,8 +86,12 @@ contract BatchMetadataERC721 is Module, UpdateMetadataCallbackERC721 { config.fallbackFunctions[0] = FallbackFunction({selector: this.uploadMetadata.selector, permissionBits: Role._MINTER_ROLE}); config.fallbackFunctions[1] = + FallbackFunction({selector: this.setBaseURI.selector, permissionBits: Role._MANAGER_ROLE}); + config.fallbackFunctions[2] = FallbackFunction({selector: this.getAllMetadataBatches.selector, permissionBits: 0}); - config.fallbackFunctions[2] = FallbackFunction({selector: this.nextTokenIdToMint.selector, permissionBits: 0}); + config.fallbackFunctions[3] = FallbackFunction({selector: this.nextTokenIdToMint.selector, permissionBits: 0}); + config.fallbackFunctions[4] = FallbackFunction({selector: this.getBatchId.selector, permissionBits: 0}); + config.fallbackFunctions[5] = FallbackFunction({selector: this.getBatchStartId.selector, permissionBits: 0}); config.requiredInterfaces = new bytes4[](1); config.requiredInterfaces[0] = 0x80ac58cd; // ERC721. @@ -153,6 +157,45 @@ contract BatchMetadataERC721 is Module, UpdateMetadataCallbackERC721 { return _batchMetadataStorage().nextTokenIdRangeStart; } + /// @dev Returns the id for the batch of tokens the given tokenId belongs to. + function getBatchId(uint256 _tokenId) public view virtual returns (uint256 batchId, uint256 index) { + uint256[] memory rangeEnds = _batchMetadataStorage().tokenIdRangeEnd; + uint256 numOfBatches = rangeEnds.length; + + for (uint256 i = 0; i < numOfBatches; i += 1) { + if (_tokenId < rangeEnds[i]) { + index = i; + batchId = rangeEnds[i]; + + return (batchId, index); + } + } + revert BatchMetadataNoMetadataForTokenId(); + } + + /// @dev returns the starting tokenId of a given batchId. + function getBatchStartId(uint256 _batchID) public view returns (uint256) { + uint256[] memory rangeEnds = _batchMetadataStorage().tokenIdRangeEnd; + uint256 numOfBatches = rangeEnds.length; + + for (uint256 i = 0; i < numOfBatches; i += 1) { + if (_batchID == rangeEnds[i]) { + if (i > 0) { + return rangeEnds[i - 1]; + } + return 0; + } + } + + revert BatchMetadataNoMetadataForTokenId(); + } + + /// @dev Sets the base URI for the batch of tokens with the given batchId. + function setBaseURI(uint256 _batchId, string memory _baseURI) external virtual { + _batchMetadataStorage().baseURIOfTokenIdRange[_batchId] = _baseURI; + emit BatchMetadataUpdate(getBatchStartId(_batchId), _batchId); + } + /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ diff --git a/src/module/token/metadata/DelayedRevealBatchMetadataERC721.sol b/src/module/token/metadata/DelayedRevealBatchMetadataERC721.sol deleted file mode 100644 index 2eb31ac5..00000000 --- a/src/module/token/metadata/DelayedRevealBatchMetadataERC721.sol +++ /dev/null @@ -1,285 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.20; - -import {Module} from "../../../Module.sol"; -import {Role} from "../../../Role.sol"; -import {LibString} from "@solady/utils/LibString.sol"; - -library DelayedRevealBatchMetadataStorage { - - /// @custom:storage-location erc7201:token.metadata.batch.delayed.reveal - bytes32 public constant DELAYED_REVEAL_BATCH_METADATA_STORAGE_POSITION = - keccak256(abi.encode(uint256(keccak256("token.metadata.batch.delayed.reveal")) - 1)) & ~bytes32(uint256(0xff)); - - struct Data { - // tokenId range end - uint256[] tokenIdRangeEnd; - // next tokenId as range start - uint256 nextTokenIdRangeStart; - // tokenId range end => baseURI of range - mapping(uint256 => string) baseURIOfTokenIdRange; - // tokenId range end => encrypted data for that range - mapping(uint256 => bytes) encryptedData; - } - - function data() internal pure returns (Data storage data_) { - bytes32 position = DELAYED_REVEAL_BATCH_METADATA_STORAGE_POSITION; - assembly { - data_.slot := position - } - } - -} - -contract DelayedRevealBatchMetadataERC721 is Module { - - using LibString for uint256; - - /*////////////////////////////////////////////////////////////// - STRUCTS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice MetadataBatch struct to store metadata for a range of tokenIds. - * @param startTokenIdInclusive The first tokenId in the range. - * @param endTokenIdNonInclusive The last tokenId in the range. - * @param baseURI The base URI for the range. - */ - struct DelayedRevealMetadataBatch { - uint256 startTokenIdInclusive; - uint256 endTokenIdInclusive; - string baseURI; - bytes encryptedData; - } - - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - /// @dev Emitted when uploading metadata for zero tokens. - error BatchMetadataZeroAmount(); - - /// @dev Emitted when trying to fetch metadata for a token that has no metadata. - error BatchMetadataNoMetadataForTokenId(); - - /// @dev The contract doesn't have any url to be delayed revealed - error DelayedRevealNothingToReveal(); - - /// @dev The result of the returned an incorrect hash - error DelayedRevealIncorrectDecryptionKey(bytes32 expected, bytes32 actual); - - /*////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ - - /// @dev ERC-4906 Metadata Update. - event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); - - /// @dev Emitted when tokens are revealed. - event TokenURIRevealed(uint256 indexed index, string revealedURI); - - /*////////////////////////////////////////////////////////////// - MODULE CONFIG - //////////////////////////////////////////////////////////////*/ - - /// @notice Returns all implemented callback and module functions. - function getModuleConfig() external pure virtual override returns (ModuleConfig memory config) { - config.callbackFunctions = new CallbackFunction[](1); - config.fallbackFunctions = new FallbackFunction[](3); - - config.callbackFunctions[0] = CallbackFunction(this.onTokenURI.selector); - config.fallbackFunctions[0] = - FallbackFunction({selector: this.uploadMetadata.selector, permissionBits: Role._MINTER_ROLE}); - config.fallbackFunctions[1] = - FallbackFunction({selector: this.getAllMetadataBatches.selector, permissionBits: 0}); - config.fallbackFunctions[2] = - FallbackFunction({selector: this.reveal.selector, permissionBits: Role._MINTER_ROLE}); - - config.requiredInterfaces = new bytes4[](1); - config.requiredInterfaces[0] = 0x80ac58cd; // ERC721. - - config.supportedInterfaces = new bytes4[](1); - config.supportedInterfaces[0] = 0x49064906; // ERC4906. - } - - /*////////////////////////////////////////////////////////////// - CALLBACK FUNCTION - //////////////////////////////////////////////////////////////*/ - - /// @notice Callback function for ERC721Metadata.tokenURI - function onTokenURI(uint256 _id) public view returns (string memory) { - (string memory batchUri, bool isEncrypted) = _getBaseURI(_id); - - if (isEncrypted) { - return string(abi.encodePacked(batchUri, "0")); - } else { - return string(abi.encodePacked(batchUri, _id.toString())); - } - } - - /*////////////////////////////////////////////////////////////// - FALLBACK FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /// @notice Returns all metadata batches for a token. - function getAllMetadataBatches() external view returns (DelayedRevealMetadataBatch[] memory) { - uint256[] memory rangeEnds = _delayedRevealBatchMetadataStorage().tokenIdRangeEnd; - uint256 numOfBatches = rangeEnds.length; - - DelayedRevealMetadataBatch[] memory batches = new DelayedRevealMetadataBatch[](rangeEnds.length); - - uint256 rangeStart = 0; - for (uint256 i = 0; i < numOfBatches; i += 1) { - batches[i] = DelayedRevealMetadataBatch({ - startTokenIdInclusive: rangeStart, - endTokenIdInclusive: rangeEnds[i] - 1, - baseURI: _delayedRevealBatchMetadataStorage().baseURIOfTokenIdRange[rangeEnds[i]], - encryptedData: _delayedRevealBatchMetadataStorage().encryptedData[rangeEnds[i]] - }); - rangeStart = rangeEnds[i]; - } - - return batches; - } - - /// @notice Uploads metadata for a range of tokenIds. - function uploadMetadata(uint256 _amount, string calldata _baseURI, bytes memory _data) public virtual { - if (_amount == 0) { - revert BatchMetadataZeroAmount(); - } - - uint256 rangeStart = _delayedRevealBatchMetadataStorage().nextTokenIdRangeStart; - uint256 rangeEndNonInclusive = rangeStart + _amount; - - if (_data.length > 0) { - (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(_data, (bytes, bytes32)); - if (encryptedURI.length != 0 && provenanceHash != "") { - _delayedRevealBatchMetadataStorage().encryptedData[rangeEndNonInclusive] = _data; - } - } - - _delayedRevealBatchMetadataStorage().nextTokenIdRangeStart = rangeEndNonInclusive; - _delayedRevealBatchMetadataStorage().tokenIdRangeEnd.push(rangeEndNonInclusive); - _delayedRevealBatchMetadataStorage().baseURIOfTokenIdRange[rangeEndNonInclusive] = _baseURI; - - emit BatchMetadataUpdate(rangeStart, rangeEndNonInclusive - 1); - } - - /// @notice reveals the URI for a range of 'delayed-reveal' tokens. - function reveal(uint256 _index, bytes calldata _key) public returns (string memory revealedURI) { - uint256 _rangeEndNonInclusive = _delayedRevealBatchMetadataStorage().tokenIdRangeEnd[_index]; - revealedURI = _getRevealURI(_rangeEndNonInclusive, _key); - - _delayedRevealBatchMetadataStorage().encryptedData[_rangeEndNonInclusive] = ""; - _delayedRevealBatchMetadataStorage().baseURIOfTokenIdRange[_rangeEndNonInclusive] = revealedURI; - - emit TokenURIRevealed(_index, revealedURI); - - uint256 rangeStart = _index == 0 ? 0 : _delayedRevealBatchMetadataStorage().tokenIdRangeEnd[_index - 1]; - emit BatchMetadataUpdate(rangeStart, _rangeEndNonInclusive - 1); - } - - /*////////////////////////////////////////////////////////////// - Encode `uploadMetadata` - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns bytes encoded metadata, to be used in `uploadedMetadata` fallback function - function encodeBytesUploadMetadata(bytes memory encryptedURI, bytes32 provenanceHash) - external - pure - returns (bytes memory) - { - return abi.encode(encryptedURI, provenanceHash); - } - - /*////////////////////////////////////////////////////////////// - INTERNAL FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Encrypt/decrypt data on chain. - * @dev Encrypt/decrypt given `data` with `key`. Uses inline assembly. - * See: https://ethereum.stackexchange.com/questions/69825/decrypt-message-on-chain - */ - function _encryptDecrypt(bytes memory data, bytes calldata key) internal pure returns (bytes memory result) { - // Store data length on stack for later use - uint256 length = data.length; - - // solhint-disable-next-line no-inline-assembly - assembly { - // Set result to free memory pointer - result := mload(0x40) - // Increase free memory pointer by lenght + 32 - mstore(0x40, add(add(result, length), 32)) - // Set result length - mstore(result, length) - } - - // Iterate over the data stepping by 32 bytes - for (uint256 i = 0; i < length; i += 32) { - // Generate hash of the key and offset - bytes32 hash = keccak256(abi.encodePacked(key, i)); - - bytes32 chunk; - // solhint-disable-next-line no-inline-assembly - assembly { - // Read 32-bytes data chunk - chunk := mload(add(data, add(i, 32))) - } - // XOR the chunk with hash - chunk ^= hash; - // solhint-disable-next-line no-inline-assembly - assembly { - // Write 32-byte encrypted chunk - mstore(add(result, add(i, 32)), chunk) - } - } - } - - /// @dev Returns the baseURI for a token. The intended metadata URI for the token is baseURI + tokenId. - function _getBaseURI(uint256 _tokenId) internal view returns (string memory, bool) { - uint256[] memory rangeEnds = _delayedRevealBatchMetadataStorage().tokenIdRangeEnd; - uint256 numOfBatches = rangeEnds.length; - - for (uint256 i = 0; i < numOfBatches; i += 1) { - if (_tokenId < rangeEnds[i]) { - bytes memory encryptedData = _delayedRevealBatchMetadataStorage().encryptedData[rangeEnds[i]]; - bool isEncrypted = encryptedData.length > 0; - - return (_delayedRevealBatchMetadataStorage().baseURIOfTokenIdRange[rangeEnds[i]], isEncrypted); - } - } - revert BatchMetadataNoMetadataForTokenId(); - } - - /// @notice unencrypted URI for a range of 'delayed-reveal' tokens. - function _getRevealURI(uint256 _rangeEndNonInclusive, bytes calldata _key) - internal - view - returns (string memory revealedURI) - { - bytes memory data = _delayedRevealBatchMetadataStorage().encryptedData[_rangeEndNonInclusive]; - if (data.length == 0) { - revert DelayedRevealNothingToReveal(); - } - - (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(data, (bytes, bytes32)); - - revealedURI = string(_encryptDecrypt(encryptedURI, _key)); - - if (keccak256(abi.encodePacked(revealedURI, _key, block.chainid)) != provenanceHash) { - revert DelayedRevealIncorrectDecryptionKey( - provenanceHash, keccak256(abi.encodePacked(revealedURI, _key, block.chainid)) - ); - } - } - - function _delayedRevealBatchMetadataStorage() - internal - pure - returns (DelayedRevealBatchMetadataStorage.Data storage) - { - return DelayedRevealBatchMetadataStorage.data(); - } - -} diff --git a/src/module/token/metadata/SimpleMetadataERC1155.sol b/src/module/token/metadata/SimpleMetadataERC1155.sol deleted file mode 100644 index 215e9ea4..00000000 --- a/src/module/token/metadata/SimpleMetadataERC1155.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.20; - -import {Role} from "../../../Role.sol"; -import {SimpleMetadataERC721} from "./SimpleMetadataERC721.sol"; - -contract SimpleMetadataERC1155 is SimpleMetadataERC721 { - - /// @notice Returns all implemented callback and module functions. - function getModuleConfig() external pure override returns (ModuleConfig memory config) { - config.callbackFunctions = new CallbackFunction[](1); - config.fallbackFunctions = new FallbackFunction[](1); - - config.callbackFunctions[0] = CallbackFunction(this.onTokenURI.selector); - config.fallbackFunctions[0] = - FallbackFunction({selector: this.setTokenURI.selector, permissionBits: Role._MINTER_ROLE}); - - config.requiredInterfaces = new bytes4[](1); - config.requiredInterfaces[0] = 0xd9b67a26; // ERC1155 - - config.supportedInterfaces = new bytes4[](1); - config.supportedInterfaces[0] = 0x49064906; // ERC4906. - } - -} diff --git a/src/module/token/metadata/SimpleMetadataERC721.sol b/src/module/token/metadata/SimpleMetadataERC721.sol deleted file mode 100644 index c53ed237..00000000 --- a/src/module/token/metadata/SimpleMetadataERC721.sol +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.20; - -import {Module} from "../../../Module.sol"; -import {Role} from "../../../Role.sol"; -import {LibString} from "@solady/utils/LibString.sol"; - -library SimpleMetadataStorage { - - /// @custom:storage-location erc7201:token.metadata.simple - bytes32 public constant SIMPLE_METADATA_STORAGE_POSITION = - keccak256(abi.encode(uint256(keccak256("token.metadata.simple")) - 1)) & ~bytes32(uint256(0xff)); - - struct Data { - // base URI - mapping(uint256 => string) uris; - } - - function data() internal pure returns (Data storage data_) { - bytes32 position = SIMPLE_METADATA_STORAGE_POSITION; - assembly { - data_.slot := position - } - } - -} - -contract SimpleMetadataERC721 is Module { - - using LibString for uint256; - - /*////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ - - /// @notice Emitted when the metadata URI for a token is updated. - event MetadataUpdate(uint256 id); - - /// @notice Emitted when the metadata URI is queried for non-existent token. - error MetadataNoMetadataForTokenId(); - - /*////////////////////////////////////////////////////////////// - MODULE CONFIG - //////////////////////////////////////////////////////////////*/ - - /// @notice Returns all implemented callback and module functions. - function getModuleConfig() external pure virtual override returns (ModuleConfig memory config) { - config.callbackFunctions = new CallbackFunction[](1); - config.fallbackFunctions = new FallbackFunction[](1); - - config.callbackFunctions[0] = CallbackFunction(this.onTokenURI.selector); - config.fallbackFunctions[0] = - FallbackFunction({selector: this.setTokenURI.selector, permissionBits: Role._MINTER_ROLE}); - - config.requiredInterfaces = new bytes4[](1); - config.requiredInterfaces[0] = 0x80ac58cd; // ERC721. - - config.supportedInterfaces = new bytes4[](1); - config.supportedInterfaces[0] = 0x49064906; // ERC4906. - } - - /*////////////////////////////////////////////////////////////// - CALLBACK FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /// @notice Callback function for ERC721Metadata.tokenURI - function onTokenURI(uint256 _id) public view returns (string memory uri) { - uri = SimpleMetadataStorage.data().uris[_id]; - if (bytes(uri).length == 0) { - revert MetadataNoMetadataForTokenId(); - } - return uri; - } - - /*////////////////////////////////////////////////////////////// - FALLBACK FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /// @notice Sets the metadata URI for a token. - function setTokenURI(uint256 _id, string calldata _uri) external { - SimpleMetadataStorage.data().uris[_id] = _uri; - emit MetadataUpdate(_id); - } - -} From df4bfe28d8a123978eb360791a1a104dc7e4fa37 Mon Sep 17 00:00:00 2001 From: Stanley Date: Wed, 4 Sep 2024 08:59:29 -0400 Subject: [PATCH 2/3] created tests for BatchMetadata --- .../token/metadata/BatchMetadataERC721.sol | 2 +- .../module/metadata/BatchMetadataERC721.t.sol | 73 +++++++- .../DelayedRevealBatchMetadataERC721.t.sol | 177 ------------------ .../metadata/SimpleMetadataERC1155.t.sol | 72 ------- .../metadata/SimpleMetadataERC721.t.sol | 66 ------- 5 files changed, 73 insertions(+), 317 deletions(-) delete mode 100644 test/module/metadata/DelayedRevealBatchMetadataERC721.t.sol delete mode 100644 test/module/metadata/SimpleMetadataERC1155.t.sol delete mode 100644 test/module/metadata/SimpleMetadataERC721.t.sol diff --git a/src/module/token/metadata/BatchMetadataERC721.sol b/src/module/token/metadata/BatchMetadataERC721.sol index 46f77c77..c549f01b 100644 --- a/src/module/token/metadata/BatchMetadataERC721.sol +++ b/src/module/token/metadata/BatchMetadataERC721.sol @@ -78,7 +78,7 @@ contract BatchMetadataERC721 is Module, UpdateMetadataCallbackERC721 { /// @notice Returns all implemented callback and module functions. function getModuleConfig() external pure virtual override returns (ModuleConfig memory config) { config.callbackFunctions = new CallbackFunction[](2); - config.fallbackFunctions = new FallbackFunction[](3); + config.fallbackFunctions = new FallbackFunction[](6); config.callbackFunctions[0] = CallbackFunction(this.onTokenURI.selector); config.callbackFunctions[1] = CallbackFunction(this.updateMetadataERC721.selector); diff --git a/test/module/metadata/BatchMetadataERC721.t.sol b/test/module/metadata/BatchMetadataERC721.t.sol index d9925bca..4bffffa2 100644 --- a/test/module/metadata/BatchMetadataERC721.t.sol +++ b/test/module/metadata/BatchMetadataERC721.t.sol @@ -51,7 +51,78 @@ contract BatchMetadataERC721Test is Test { } /*/////////////////////////////////////////////////////////////// - Unit tests: `uploadMetadata` + Unit tests: `getBatchId` + //////////////////////////////////////////////////////////////*/ + + function test_state_getBatchId() public { + vm.prank(owner); + BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); + (uint256 batchId, uint256 index) = BatchMetadataExt(address(core)).getBatchId(0); + + assertEq(batchId, 100); + assertEq(index, 0); + } + + function test_revert_getBatchId() public { + vm.prank(owner); + BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); + + vm.expectRevert(BatchMetadataERC721.BatchMetadataNoMetadataForTokenId.selector); + (uint256 batchId, uint256 index) = BatchMetadataExt(address(core)).getBatchId(101); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `getBatchStartId` + //////////////////////////////////////////////////////////////*/ + + function test_state_getBatchStartId() public { + vm.startPrank(owner); + BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); + BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); + vm.stopPrank(); + + uint256 batchId1 = BatchMetadataExt(address(core)).getBatchStartId(100); + uint256 batchId2 = BatchMetadataExt(address(core)).getBatchStartId(200); + + assertEq(batchId1, 0); + assertEq(batchId2, 100); + } + + function test_revert_getBatchStartId() public { + vm.prank(owner); + BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); + + vm.expectRevert(BatchMetadataERC721.BatchMetadataNoMetadataForTokenId.selector); + vm.prank(owner); + uint256 batchId = BatchMetadataExt(address(core)).getBatchStartId(101); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `setBaseURI` + //////////////////////////////////////////////////////////////*/ + + function test_state_setBaseURI() public { + vm.prank(owner); + BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); + + // get metadata batches + BatchMetadataExt.MetadataBatch[] memory batches = BatchMetadataExt(address(core)).getAllMetadataBatches(); + + assertEq(batches.length, 1); + assertEq(batches[0].baseURI, "ipfs://base/"); + + vm.prank(owner); + BatchMetadataExt(address(core)).setBaseURI(100, "ipfs://base2/"); + + // get metadata batches + BatchMetadataExt.MetadataBatch[] memory batches2 = BatchMetadataExt(address(core)).getAllMetadataBatches(); + + assertEq(batches2.length, 1); + assertEq(batches2[0].baseURI, "ipfs://base2/"); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `updateMetadata` //////////////////////////////////////////////////////////////*/ function test_state_updateMetadata() public { diff --git a/test/module/metadata/DelayedRevealBatchMetadataERC721.t.sol b/test/module/metadata/DelayedRevealBatchMetadataERC721.t.sol deleted file mode 100644 index 473ca31f..00000000 --- a/test/module/metadata/DelayedRevealBatchMetadataERC721.t.sol +++ /dev/null @@ -1,177 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; - -import "lib/forge-std/src/console.sol"; - -import {Test} from "forge-std/Test.sol"; - -// Target contract - -import {Core} from "src/Core.sol"; -import {Module} from "src/Module.sol"; -import {ERC721Core} from "src/core/token/ERC721Core.sol"; - -import {ICore} from "src/interface/ICore.sol"; -import {IModuleConfig} from "src/interface/IModuleConfig.sol"; -import {DelayedRevealBatchMetadataERC721} from "src/module/token/metadata/DelayedRevealBatchMetadataERC721.sol"; - -contract DelayedRevealExt is DelayedRevealBatchMetadataERC721 {} - -contract DelayedRevealBatchMetadataERC721Test is Test { - - ERC721Core public core; - - DelayedRevealExt public moduleImplementation; - - address public owner = address(0x1); - address public permissionedActor = address(0x2); - address public unpermissionedActor = address(0x3); - - function setUp() public { - address[] memory modules; - bytes[] memory moduleData; - - core = new ERC721Core("test", "TEST", "", owner, modules, moduleData); - moduleImplementation = new DelayedRevealExt(); - - // install module - vm.prank(owner); - core.installModule(address(moduleImplementation), ""); - } - - /*/////////////////////////////////////////////////////////////// - Helper functions - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Encrypt/decrypt data on chain. - * @dev Encrypt/decrypt given `data` with `key`. Uses inline assembly. - * See: https://ethereum.stackexchange.com/questions/69825/decrypt-message-on-chain - */ - function _encryptDecrypt(bytes memory data, bytes memory key) internal pure returns (bytes memory result) { - // Store data length on stack for later use - uint256 length = data.length; - - // solhint-disable-next-line no-inline-assembly - assembly { - // Set result to free memory pointer - result := mload(0x40) - // Increase free memory pointer by lenght + 32 - mstore(0x40, add(add(result, length), 32)) - // Set result length - mstore(result, length) - } - - // Iterate over the data stepping by 32 bytes - for (uint256 i = 0; i < length; i += 32) { - // Generate hash of the key and offset - bytes32 hash = keccak256(abi.encodePacked(key, i)); - - bytes32 chunk; - // solhint-disable-next-line no-inline-assembly - assembly { - // Read 32-bytes data chunk - chunk := mload(add(data, add(i, 32))) - } - // XOR the chunk with hash - chunk ^= hash; - // solhint-disable-next-line no-inline-assembly - assembly { - // Write 32-byte encrypted chunk - mstore(add(result, add(i, 32)), chunk) - } - } - } - - /*/////////////////////////////////////////////////////////////// - Unit tests: `uploadMetadata` - //////////////////////////////////////////////////////////////*/ - - function test_state_uploadMetadata() public { - vm.prank(owner); - DelayedRevealExt(address(core)).uploadMetadata(100, "ipfs://base/", ""); - - // read state from core - assertEq(core.tokenURI(1), "ipfs://base/1"); - assertEq(core.tokenURI(99), "ipfs://base/99"); - - // upload another batch - vm.prank(owner); - DelayedRevealExt(address(core)).uploadMetadata(100, "ipfs://base2/", ""); - - // read state from core - assertEq(core.tokenURI(1), "ipfs://base/1"); - assertEq(core.tokenURI(99), "ipfs://base/99"); - assertEq(core.tokenURI(100), "ipfs://base2/100"); - assertEq(core.tokenURI(199), "ipfs://base2/199"); - } - - function test_state_uploadMetadata_encrypted() public { - string memory originalURI = "ipfs://original/"; - string memory tempURI = "ipfs://temp/"; - bytes memory encryptionKey = "key123"; - - bytes32 provenanceHash = keccak256(abi.encodePacked(originalURI, encryptionKey, block.chainid)); - bytes memory encryptedURI = _encryptDecrypt(bytes(originalURI), encryptionKey); - bytes memory encryptedData = abi.encode(encryptedURI, provenanceHash); - - vm.prank(owner); - DelayedRevealExt(address(core)).uploadMetadata(100, tempURI, encryptedData); - - // read state from core - assertEq(core.tokenURI(1), "ipfs://temp/0"); - assertEq(core.tokenURI(99), "ipfs://temp/0"); - } - - function test_state_reveal() public { - string memory originalURI = "ipfs://original/"; - string memory tempURI = "ipfs://temp/"; - bytes memory encryptionKey = "key123"; - - bytes32 provenanceHash = keccak256(abi.encodePacked(originalURI, encryptionKey, block.chainid)); - bytes memory encryptedURI = _encryptDecrypt(bytes(originalURI), encryptionKey); - bytes memory encryptedData = abi.encode(encryptedURI, provenanceHash); - - vm.prank(owner); - DelayedRevealExt(address(core)).uploadMetadata(100, tempURI, encryptedData); - - // reveal - vm.prank(owner); - DelayedRevealExt(address(core)).reveal(0, encryptionKey); - - // read state from core - assertEq(core.tokenURI(1), "ipfs://original/1"); - assertEq(core.tokenURI(99), "ipfs://original/99"); - } - - function test_getRevealURI() public { - string memory originalURI = "ipfs://original/"; - string memory tempURI = "ipfs://temp/"; - bytes memory encryptionKey = "key123"; - - bytes32 provenanceHash = keccak256(abi.encodePacked(originalURI, encryptionKey, block.chainid)); - bytes memory encryptedURI = _encryptDecrypt(bytes(originalURI), encryptionKey); - bytes memory encryptedData = abi.encode(encryptedURI, provenanceHash); - - vm.prank(owner); - DelayedRevealExt(address(core)).uploadMetadata(100, tempURI, encryptedData); - - // get reveal URI - uint256 index = 0; - - DelayedRevealBatchMetadataERC721.DelayedRevealMetadataBatch[] memory batches = - DelayedRevealExt(address(core)).getAllMetadataBatches(); - - bytes memory encryptedDataStored = batches[index].encryptedData; - (bytes memory encryptedURIStored,) = abi.decode(encryptedDataStored, (bytes, bytes32)); - - string memory revealURI = string(_encryptDecrypt(encryptedURIStored, encryptionKey)); - - assertEq(revealURI, originalURI); - - // state unchanged - assertEq(core.tokenURI(1), "ipfs://temp/0"); - assertEq(core.tokenURI(99), "ipfs://temp/0"); - } - -} diff --git a/test/module/metadata/SimpleMetadataERC1155.t.sol b/test/module/metadata/SimpleMetadataERC1155.t.sol deleted file mode 100644 index c01614cb..00000000 --- a/test/module/metadata/SimpleMetadataERC1155.t.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; - -import "lib/forge-std/src/console.sol"; - -import {Test} from "forge-std/Test.sol"; - -// Target contract - -import {Core} from "src/Core.sol"; -import {Module} from "src/Module.sol"; -import {ERC1155Core} from "src/core/token/ERC1155Core.sol"; - -import {ICore} from "src/interface/ICore.sol"; -import {IModuleConfig} from "src/interface/IModuleConfig.sol"; -import {SimpleMetadataERC1155} from "src/module/token/metadata/SimpleMetadataERC1155.sol"; -import {SimpleMetadataERC721, SimpleMetadataStorage} from "src/module/token/metadata/SimpleMetadataERC721.sol"; - -contract SimpleMetadataExt is SimpleMetadataERC1155 {} - -contract SimpleMetadataERC1155Test is Test { - - ERC1155Core public core; - - SimpleMetadataExt public moduleImplementation; - - address public owner = address(0x1); - address public permissionedActor = address(0x2); - address public unpermissionedActor = address(0x3); - - function setUp() public { - address[] memory modules; - bytes[] memory moduleData; - - core = new ERC1155Core("test", "TEST", "", owner, modules, moduleData); - moduleImplementation = new SimpleMetadataExt(); - - // install module - vm.prank(owner); - core.installModule(address(moduleImplementation), ""); - } - - /*/////////////////////////////////////////////////////////////// - Unit tests: `setTokenURI` - //////////////////////////////////////////////////////////////*/ - - function test_state_setTokenURI() public { - vm.prank(owner); - SimpleMetadataExt(address(core)).setTokenURI(1, "ipfs://base/1"); - - vm.prank(owner); - SimpleMetadataExt(address(core)).setTokenURI(2, "ipfs://base/2"); - - // read state from core - assertEq(core.uri(1), "ipfs://base/1"); - assertEq(core.uri(2), "ipfs://base/2"); - - vm.expectRevert(abi.encodeWithSelector(SimpleMetadataERC721.MetadataNoMetadataForTokenId.selector)); - core.uri(3); - } - - function test_revert_setTokenURI() public { - vm.expectRevert(0x82b42900); // `Unauthorized()` - SimpleMetadataExt(address(core)).setTokenURI(1, "ipfs://base/"); - } - - function test_audit_does_not_set_name_and_symbol() public { - assertEq(core.name(), "test"); - assertEq(core.symbol(), "TEST"); - } - -} diff --git a/test/module/metadata/SimpleMetadataERC721.t.sol b/test/module/metadata/SimpleMetadataERC721.t.sol deleted file mode 100644 index 69705950..00000000 --- a/test/module/metadata/SimpleMetadataERC721.t.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; - -import "lib/forge-std/src/console.sol"; - -import {Test} from "forge-std/Test.sol"; - -// Target contract - -import {Core} from "src/Core.sol"; -import {Module} from "src/Module.sol"; -import {ERC721Core} from "src/core/token/ERC721Core.sol"; - -import {ICore} from "src/interface/ICore.sol"; -import {IModuleConfig} from "src/interface/IModuleConfig.sol"; -import {SimpleMetadataERC721, SimpleMetadataStorage} from "src/module/token/metadata/SimpleMetadataERC721.sol"; - -contract SimpleMetadataExt is SimpleMetadataERC721 {} - -contract SimpleMetadataERC721Test is Test { - - ERC721Core public core; - - SimpleMetadataExt public moduleImplementation; - - address public owner = address(0x1); - address public permissionedActor = address(0x2); - address public unpermissionedActor = address(0x3); - - function setUp() public { - address[] memory modules; - bytes[] memory moduleData; - - core = new ERC721Core("test", "TEST", "", owner, modules, moduleData); - moduleImplementation = new SimpleMetadataExt(); - - // install module - vm.prank(owner); - core.installModule(address(moduleImplementation), ""); - } - - /*/////////////////////////////////////////////////////////////// - Unit tests: `setTokenURI` - //////////////////////////////////////////////////////////////*/ - - function test_state_setTokenURI() public { - vm.prank(owner); - SimpleMetadataExt(address(core)).setTokenURI(1, "ipfs://base/1"); - - vm.prank(owner); - SimpleMetadataExt(address(core)).setTokenURI(2, "ipfs://base/2"); - - // read state from core - assertEq(core.tokenURI(1), "ipfs://base/1"); - assertEq(core.tokenURI(2), "ipfs://base/2"); - - vm.expectRevert(abi.encodeWithSelector(SimpleMetadataERC721.MetadataNoMetadataForTokenId.selector)); - core.tokenURI(3); - } - - function test_revert_setTokenURI() public { - vm.expectRevert(0x82b42900); // `Unauthorized()` - SimpleMetadataExt(address(core)).setTokenURI(1, "ipfs://base/"); - } - -} From a2593404b61a8244cd9b30e309df5d749a6dc2f7 Mon Sep 17 00:00:00 2001 From: Stanley Date: Wed, 4 Sep 2024 10:03:26 -0400 Subject: [PATCH 3/3] updated ERC1155 tests and updated from batchStartId to batchRange --- .../token/metadata/BatchMetadataERC1155.sol | 8 +- .../token/metadata/BatchMetadataERC721.sol | 11 +-- .../metadata/BatchMetadataERC1155.t.sol | 86 +++++++++++++++++++ .../module/metadata/BatchMetadataERC721.t.sol | 16 ++-- 4 files changed, 107 insertions(+), 14 deletions(-) diff --git a/src/module/token/metadata/BatchMetadataERC1155.sol b/src/module/token/metadata/BatchMetadataERC1155.sol index 740d0981..656af62a 100644 --- a/src/module/token/metadata/BatchMetadataERC1155.sol +++ b/src/module/token/metadata/BatchMetadataERC1155.sol @@ -11,7 +11,7 @@ contract BatchMetadataERC1155 is BatchMetadataERC721, UpdateMetadataCallbackERC1 /// @notice Returns all implemented callback and module functions. function getModuleConfig() external pure virtual override returns (ModuleConfig memory config) { config.callbackFunctions = new CallbackFunction[](2); - config.fallbackFunctions = new FallbackFunction[](3); + config.fallbackFunctions = new FallbackFunction[](6); config.callbackFunctions[0] = CallbackFunction(this.onTokenURI.selector); config.callbackFunctions[1] = CallbackFunction(this.updateMetadataERC1155.selector); @@ -19,8 +19,12 @@ contract BatchMetadataERC1155 is BatchMetadataERC721, UpdateMetadataCallbackERC1 config.fallbackFunctions[0] = FallbackFunction({selector: this.uploadMetadata.selector, permissionBits: Role._MINTER_ROLE}); config.fallbackFunctions[1] = + FallbackFunction({selector: this.setBaseURI.selector, permissionBits: Role._MANAGER_ROLE}); + config.fallbackFunctions[2] = FallbackFunction({selector: this.getAllMetadataBatches.selector, permissionBits: 0}); - config.fallbackFunctions[2] = FallbackFunction({selector: this.nextTokenIdToMint.selector, permissionBits: 0}); + config.fallbackFunctions[3] = FallbackFunction({selector: this.nextTokenIdToMint.selector, permissionBits: 0}); + config.fallbackFunctions[4] = FallbackFunction({selector: this.getBatchId.selector, permissionBits: 0}); + config.fallbackFunctions[5] = FallbackFunction({selector: this.getBatchRange.selector, permissionBits: 0}); config.requiredInterfaces = new bytes4[](1); config.requiredInterfaces[0] = 0xd9b67a26; // ERC1155 diff --git a/src/module/token/metadata/BatchMetadataERC721.sol b/src/module/token/metadata/BatchMetadataERC721.sol index c549f01b..bf47dcea 100644 --- a/src/module/token/metadata/BatchMetadataERC721.sol +++ b/src/module/token/metadata/BatchMetadataERC721.sol @@ -91,7 +91,7 @@ contract BatchMetadataERC721 is Module, UpdateMetadataCallbackERC721 { FallbackFunction({selector: this.getAllMetadataBatches.selector, permissionBits: 0}); config.fallbackFunctions[3] = FallbackFunction({selector: this.nextTokenIdToMint.selector, permissionBits: 0}); config.fallbackFunctions[4] = FallbackFunction({selector: this.getBatchId.selector, permissionBits: 0}); - config.fallbackFunctions[5] = FallbackFunction({selector: this.getBatchStartId.selector, permissionBits: 0}); + config.fallbackFunctions[5] = FallbackFunction({selector: this.getBatchRange.selector, permissionBits: 0}); config.requiredInterfaces = new bytes4[](1); config.requiredInterfaces[0] = 0x80ac58cd; // ERC721. @@ -174,16 +174,16 @@ contract BatchMetadataERC721 is Module, UpdateMetadataCallbackERC721 { } /// @dev returns the starting tokenId of a given batchId. - function getBatchStartId(uint256 _batchID) public view returns (uint256) { + function getBatchRange(uint256 _batchID) public view returns (uint256, uint256) { uint256[] memory rangeEnds = _batchMetadataStorage().tokenIdRangeEnd; uint256 numOfBatches = rangeEnds.length; for (uint256 i = 0; i < numOfBatches; i += 1) { if (_batchID == rangeEnds[i]) { if (i > 0) { - return rangeEnds[i - 1]; + return (rangeEnds[i - 1], rangeEnds[i] - 1); } - return 0; + return (0, rangeEnds[i] - 1); } } @@ -193,7 +193,8 @@ contract BatchMetadataERC721 is Module, UpdateMetadataCallbackERC721 { /// @dev Sets the base URI for the batch of tokens with the given batchId. function setBaseURI(uint256 _batchId, string memory _baseURI) external virtual { _batchMetadataStorage().baseURIOfTokenIdRange[_batchId] = _baseURI; - emit BatchMetadataUpdate(getBatchStartId(_batchId), _batchId); + (uint256 startTokenId,) = getBatchRange(_batchId); + emit BatchMetadataUpdate(startTokenId, _batchId); } /*////////////////////////////////////////////////////////////// diff --git a/test/module/metadata/BatchMetadataERC1155.t.sol b/test/module/metadata/BatchMetadataERC1155.t.sol index 6812d496..dfcdb7db 100644 --- a/test/module/metadata/BatchMetadataERC1155.t.sol +++ b/test/module/metadata/BatchMetadataERC1155.t.sol @@ -9,11 +9,15 @@ import {Test} from "forge-std/Test.sol"; import {Core} from "src/Core.sol"; import {Module} from "src/Module.sol"; +import {Role} from "src/Role.sol"; import {ERC1155Core} from "src/core/token/ERC1155Core.sol"; import {ICore} from "src/interface/ICore.sol"; import {IModuleConfig} from "src/interface/IModuleConfig.sol"; + import {BatchMetadataERC1155} from "src/module/token/metadata/BatchMetadataERC1155.sol"; +import {BatchMetadataERC721} from "src/module/token/metadata/BatchMetadataERC721.sol"; +import {MintableERC1155} from "src/module/token/minting/MintableERC1155.sol"; contract BatchMetadataExt is BatchMetadataERC1155 {} @@ -22,6 +26,7 @@ contract BatchMetadataERC1155Test is Test { ERC1155Core public core; BatchMetadataExt public moduleImplementation; + MintableERC1155 public mintableModule; address public owner = address(0x1); address public permissionedActor = address(0x2); @@ -33,10 +38,91 @@ contract BatchMetadataERC1155Test is Test { core = new ERC1155Core("test", "TEST", "", owner, modules, moduleData); moduleImplementation = new BatchMetadataExt(); + mintableModule = new MintableERC1155(); // install module vm.prank(owner); core.installModule(address(moduleImplementation), ""); + + bytes memory encodedInstallParams = abi.encode(owner); + vm.prank(owner); + core.installModule(address(mintableModule), encodedInstallParams); + + vm.prank(owner); + core.grantRoles(owner, Role._MINTER_ROLE); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `getBatchId` + //////////////////////////////////////////////////////////////*/ + + function test_state_getBatchId() public { + vm.prank(owner); + BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); + (uint256 batchId, uint256 index) = BatchMetadataExt(address(core)).getBatchId(0); + + assertEq(batchId, 100); + assertEq(index, 0); + } + + function test_revert_getBatchId() public { + vm.prank(owner); + BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); + + vm.expectRevert(BatchMetadataERC721.BatchMetadataNoMetadataForTokenId.selector); + (uint256 batchId, uint256 index) = BatchMetadataExt(address(core)).getBatchId(101); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `getBatchRange` + //////////////////////////////////////////////////////////////*/ + + function test_state_getBatchRange() public { + vm.startPrank(owner); + BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); + BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); + vm.stopPrank(); + + (uint256 startTokenId1, uint256 endTokenId1) = BatchMetadataExt(address(core)).getBatchRange(100); + (uint256 startTokenId2, uint256 endTokenId2) = BatchMetadataExt(address(core)).getBatchRange(200); + + assertEq(startTokenId1, 0); + assertEq(endTokenId1, 99); + assertEq(startTokenId2, 100); + assertEq(endTokenId2, 199); + } + + function test_revert_getBatchRange() public { + vm.prank(owner); + BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); + + vm.expectRevert(BatchMetadataERC721.BatchMetadataNoMetadataForTokenId.selector); + vm.prank(owner); + BatchMetadataExt(address(core)).getBatchRange(101); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `setBaseURI` + //////////////////////////////////////////////////////////////*/ + + function test_state_setBaseURI() public { + vm.prank(owner); + BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); + + // get metadata batches + BatchMetadataExt.MetadataBatch[] memory batches = BatchMetadataExt(address(core)).getAllMetadataBatches(); + + assertEq(batches.length, 1); + assertEq(batches[0].baseURI, "ipfs://base/"); + + vm.prank(owner); + BatchMetadataExt(address(core)).setBaseURI(100, "ipfs://base2/"); + + // get metadata batches + BatchMetadataExt.MetadataBatch[] memory batches2 = BatchMetadataExt(address(core)).getAllMetadataBatches(); + + assertEq(batches2.length, 1); + assertEq(batches2[0].baseURI, "ipfs://base2/"); } /*/////////////////////////////////////////////////////////////// diff --git a/test/module/metadata/BatchMetadataERC721.t.sol b/test/module/metadata/BatchMetadataERC721.t.sol index 4bffffa2..c2bd3a27 100644 --- a/test/module/metadata/BatchMetadataERC721.t.sol +++ b/test/module/metadata/BatchMetadataERC721.t.sol @@ -72,20 +72,22 @@ contract BatchMetadataERC721Test is Test { } /*/////////////////////////////////////////////////////////////// - Unit tests: `getBatchStartId` + Unit tests: `getBatchRange` //////////////////////////////////////////////////////////////*/ - function test_state_getBatchStartId() public { + function test_state_getBatchRange() public { vm.startPrank(owner); BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); BatchMetadataExt(address(core)).uploadMetadata(100, "ipfs://base/"); vm.stopPrank(); - uint256 batchId1 = BatchMetadataExt(address(core)).getBatchStartId(100); - uint256 batchId2 = BatchMetadataExt(address(core)).getBatchStartId(200); + (uint256 startTokenId1, uint256 endTokenId1) = BatchMetadataExt(address(core)).getBatchRange(100); + (uint256 startTokenId2, uint256 endTokenId2) = BatchMetadataExt(address(core)).getBatchRange(200); - assertEq(batchId1, 0); - assertEq(batchId2, 100); + assertEq(startTokenId1, 0); + assertEq(endTokenId1, 99); + assertEq(startTokenId2, 100); + assertEq(endTokenId2, 199); } function test_revert_getBatchStartId() public { @@ -94,7 +96,7 @@ contract BatchMetadataERC721Test is Test { vm.expectRevert(BatchMetadataERC721.BatchMetadataNoMetadataForTokenId.selector); vm.prank(owner); - uint256 batchId = BatchMetadataExt(address(core)).getBatchStartId(101); + BatchMetadataExt(address(core)).getBatchRange(101); } /*///////////////////////////////////////////////////////////////