diff --git a/src/callback/UpdateTokenIdERC1155.sol b/src/callback/UpdateTokenIdERC1155.sol new file mode 100644 index 00000000..ff1e947a --- /dev/null +++ b/src/callback/UpdateTokenIdERC1155.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +contract UpdateTokenIdCallbackERC1155 { + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error UpdateTokenIdCallbackERC1155NotImplemented(); + + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice The updateTokenIdERC1155 hook that is called by a core token before minting tokens. + * + * @dev If the tokenId is type(uint256).max, the next tokenId will be set to the current next tokenId + amount. + * + * @param _tokenId The tokenId to mint. + * @return result tokenId to mint. + */ + function updateTokenIdERC1155(uint256 _tokenId) external payable virtual returns (uint256) { + revert UpdateTokenIdCallbackERC1155NotImplemented(); + } + +} diff --git a/src/core/token/ERC1155Core.sol b/src/core/token/ERC1155Core.sol index e00271c6..6685858c 100644 --- a/src/core/token/ERC1155Core.sol +++ b/src/core/token/ERC1155Core.sol @@ -15,6 +15,7 @@ import {BeforeMintCallbackERC1155} from "../../callback/BeforeMintCallbackERC115 import {BeforeMintWithSignatureCallbackERC1155} from "../../callback/BeforeMintWithSignatureCallbackERC1155.sol"; import {BeforeTransferCallbackERC1155} from "../../callback/BeforeTransferCallbackERC1155.sol"; import {UpdateMetadataCallbackERC1155} from "../../callback/UpdateMetadataCallbackERC1155.sol"; +import {UpdateTokenIdCallbackERC1155} from "../../callback/UpdateTokenIdERC1155.sol"; import {OnTokenURICallback} from "../../callback/OnTokenURICallback.sol"; @@ -142,7 +143,7 @@ contract ERC1155Core is ERC1155, Core, Multicallable, EIP712 { override returns (SupportedCallbackFunction[] memory supportedCallbackFunctions) { - supportedCallbackFunctions = new SupportedCallbackFunction[](8); + supportedCallbackFunctions = new SupportedCallbackFunction[](9); supportedCallbackFunctions[0] = SupportedCallbackFunction({ selector: BeforeMintCallbackERC1155.beforeMintERC1155.selector, mode: CallbackMode.REQUIRED @@ -173,6 +174,10 @@ contract ERC1155Core is ERC1155, Core, Multicallable, EIP712 { selector: UpdateMetadataCallbackERC1155.updateMetadataERC1155.selector, mode: CallbackMode.REQUIRED }); + supportedCallbackFunctions[8] = SupportedCallbackFunction({ + selector: UpdateTokenIdCallbackERC1155.updateTokenIdERC1155.selector, + mode: CallbackMode.OPTIONAL + }); } /*////////////////////////////////////////////////////////////// @@ -201,13 +206,14 @@ contract ERC1155Core is ERC1155, Core, Multicallable, EIP712 { external payable { + uint256 tokenIdToMint = _updateTokenId(tokenId); if (bytes(baseURI).length > 0) { - _updateMetadata(to, tokenId, amount, baseURI); + _updateMetadata(to, tokenIdToMint, amount, baseURI); } - _beforeMint(to, tokenId, amount, data); + _beforeMint(to, tokenIdToMint, amount, data); - _totalSupply[tokenId] += amount; - _mint(to, tokenId, amount, ""); + _totalSupply[tokenIdToMint] += amount; + _mint(to, tokenIdToMint, amount, ""); } /** @@ -236,13 +242,14 @@ contract ERC1155Core is ERC1155, Core, Multicallable, EIP712 { ) ).recover(signature); + uint256 tokenIdToMint = _updateTokenId(tokenId); if (bytes(baseURI).length > 0) { - _updateMetadata(to, tokenId, amount, baseURI); + _updateMetadata(to, tokenIdToMint, amount, baseURI); } - _beforeMintWithSignature(to, tokenId, amount, data, signer); + _beforeMintWithSignature(to, tokenIdToMint, amount, data, signer); - _totalSupply[tokenId] += amount; - _mint(to, tokenId, amount, ""); + _totalSupply[tokenIdToMint] += amount; + _mint(to, tokenIdToMint, amount, ""); } /** @@ -393,6 +400,20 @@ contract ERC1155Core is ERC1155, Core, Multicallable, EIP712 { ); } + /// @dev Calls the updateTokenId hook, if installed. + function _updateTokenId(uint256 tokenId) internal virtual returns (uint256 tokenIdToMint) { + (bool success, bytes memory returndata) = _executeCallbackFunction( + UpdateTokenIdCallbackERC1155.updateTokenIdERC1155.selector, + abi.encodeCall(UpdateTokenIdCallbackERC1155.updateTokenIdERC1155, (tokenId)) + ); + if (success) { + tokenIdToMint = abi.decode(returndata, (uint256)); + } else { + // this will only occur when the callback is not implemented + tokenIdToMint = tokenId; + } + } + /// @dev Returns the domain name and version for EIP712. function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) { name = "ERC1155Core"; diff --git a/src/core/token/ERC1155CoreInitializable.sol b/src/core/token/ERC1155CoreInitializable.sol index 399439b6..09691f79 100644 --- a/src/core/token/ERC1155CoreInitializable.sol +++ b/src/core/token/ERC1155CoreInitializable.sol @@ -17,6 +17,7 @@ import {BeforeMintCallbackERC1155} from "../../callback/BeforeMintCallbackERC115 import {BeforeMintWithSignatureCallbackERC1155} from "../../callback/BeforeMintWithSignatureCallbackERC1155.sol"; import {BeforeTransferCallbackERC1155} from "../../callback/BeforeTransferCallbackERC1155.sol"; import {UpdateMetadataCallbackERC1155} from "../../callback/UpdateMetadataCallbackERC1155.sol"; +import {UpdateTokenIdCallbackERC1155} from "../../callback/UpdateTokenIdERC1155.sol"; import {OnTokenURICallback} from "../../callback/OnTokenURICallback.sol"; @@ -142,7 +143,7 @@ contract ERC1155CoreInitializable is ERC1155, Core, Multicallable, Initializable override returns (SupportedCallbackFunction[] memory supportedCallbackFunctions) { - supportedCallbackFunctions = new SupportedCallbackFunction[](8); + supportedCallbackFunctions = new SupportedCallbackFunction[](9); supportedCallbackFunctions[0] = SupportedCallbackFunction({ selector: BeforeMintCallbackERC1155.beforeMintERC1155.selector, mode: CallbackMode.REQUIRED @@ -173,6 +174,10 @@ contract ERC1155CoreInitializable is ERC1155, Core, Multicallable, Initializable selector: UpdateMetadataCallbackERC1155.updateMetadataERC1155.selector, mode: CallbackMode.REQUIRED }); + supportedCallbackFunctions[8] = SupportedCallbackFunction({ + selector: UpdateTokenIdCallbackERC1155.updateTokenIdERC1155.selector, + mode: CallbackMode.OPTIONAL + }); } /*////////////////////////////////////////////////////////////// diff --git a/src/module/token/tokenId/sequentialTokenIdERC1155.sol b/src/module/token/tokenId/sequentialTokenIdERC1155.sol new file mode 100644 index 00000000..a96676bf --- /dev/null +++ b/src/module/token/tokenId/sequentialTokenIdERC1155.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {Module} from "../../../Module.sol"; + +import {Role} from "../../../Role.sol"; + +import {UpdateTokenIdCallbackERC1155} from "../../../callback/UpdateTokenIdERC1155.sol"; +import {IInstallationCallback} from "../../../interface/IInstallationCallback.sol"; + +library SequentialTokenIdStorage { + + /// @custom:storage-location erc7201:token.minting.tokenId + bytes32 public constant SEQUENTIAL_TOKEN_ID_STORAGE_POSITION = + keccak256(abi.encode(uint256(keccak256("token.tokenId.erc1155")) - 1)) & ~bytes32(uint256(0xff)); + + struct Data { + uint256 nextTokenId; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = SEQUENTIAL_TOKEN_ID_STORAGE_POSITION; + assembly { + data_.slot := position + } + } + +} + +contract SequentialTokenIdERC1155 is Module, UpdateTokenIdCallbackERC1155 { + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @dev Emitted when the tokenId is invalid. + error SequentialTokenIdInvalidTokenId(); + + /*////////////////////////////////////////////////////////////// + MODULE CONFIG + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns all implemented callback and fallback 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.updateTokenIdERC1155.selector); + + config.fallbackFunctions[0] = FallbackFunction({selector: this.getNextTokenId.selector, permissionBits: 0}); + + config.requiredInterfaces = new bytes4[](1); + config.requiredInterfaces[0] = 0xd9b67a26; // ERC1155 + } + + /*////////////////////////////////////////////////////////////// + CALLBACK FUNCTION + //////////////////////////////////////////////////////////////*/ + + function updateTokenIdERC1155(uint256 _tokenId) external payable override returns (uint256) { + uint256 _nextTokenId = _tokenIdStorage().nextTokenId; + + if (_tokenId == type(uint256).max) { + _tokenIdStorage().nextTokenId = _nextTokenId + 1; + + return _nextTokenId; + } + + if (_tokenId > _nextTokenId) { + revert SequentialTokenIdInvalidTokenId(); + } + + return _tokenId; + } + + /*////////////////////////////////////////////////////////////// + FALLBACK FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns the sale configuration for a token. + function getNextTokenId() external view returns (uint256) { + return _tokenIdStorage().nextTokenId; + } + + function _tokenIdStorage() internal pure returns (SequentialTokenIdStorage.Data storage) { + return SequentialTokenIdStorage.data(); + } + +} diff --git a/test/module/minting/ClaimableERC1155.t.sol b/test/module/minting/ClaimableERC1155.t.sol index aca34a30..aa1e5f90 100644 --- a/test/module/minting/ClaimableERC1155.t.sol +++ b/test/module/minting/ClaimableERC1155.t.sol @@ -115,9 +115,8 @@ contract ClaimableERC1155Test is Test { core.installModule(address(claimableModule), encodedInstallParams); // install module - bytes memory encodedBatchMetadataInstallParams = ""; vm.prank(owner); - core.installModule(address(batchMetadataModule), encodedBatchMetadataInstallParams); + core.installModule(address(batchMetadataModule), ""); // Setup signature vars typehashClaimSignatureParams = diff --git a/test/module/minting/MintableERC1155.t.sol b/test/module/minting/MintableERC1155.t.sol index 04db1f62..fa9bc62a 100644 --- a/test/module/minting/MintableERC1155.t.sol +++ b/test/module/minting/MintableERC1155.t.sol @@ -113,9 +113,8 @@ contract MintableERC1155Test is Test { vm.prank(owner); core.installModule(address(mintableModule), encodedInstallParams); - bytes memory encodedBatchMetadataInstallParams = ""; vm.prank(owner); - core.installModule(address(batchMetadataModule), encodedBatchMetadataInstallParams); + core.installModule(address(batchMetadataModule), ""); // Setup signature vars typehashMintSignatureParams = diff --git a/test/module/tokenId/sequentialTokenIdERC1155.t.sol b/test/module/tokenId/sequentialTokenIdERC1155.t.sol new file mode 100644 index 00000000..0637fd25 --- /dev/null +++ b/test/module/tokenId/sequentialTokenIdERC1155.t.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "lib/forge-std/src/console.sol"; + +import {OwnableRoles} from "@solady/auth/OwnableRoles.sol"; +import {ERC20} from "@solady/tokens/ERC20.sol"; +import {Test} from "forge-std/Test.sol"; + +// Target contract + +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"; +import {SequentialTokenIdERC1155} from "src/module/token/tokenId/sequentialTokenIdERC1155.sol"; + +contract MockCurrency is ERC20 { + + function mintTo(address _recipient, uint256 _amount) public { + _mint(_recipient, _amount); + } + + /// @dev Returns the name of the token. + function name() public view virtual override returns (string memory) { + return "MockCurrency"; + } + + /// @dev Returns the symbol of the token. + function symbol() public view virtual override returns (string memory) { + return "MOCK"; + } + +} + +contract MintableERC1155Test is Test { + + ERC1155Core public core; + + MintableERC1155 public mintableModule; + BatchMetadataERC1155 public batchMetadataModule; + SequentialTokenIdERC1155 public sequentialTokenIdModule; + + uint256 ownerPrivateKey = 1; + address public owner; + + address tokenRecipient = address(0x123); + + MintableERC1155.MintSignatureParamsERC1155 public mintRequest; + + // Constants + address private constant NATIVE_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + function setUp() public { + owner = vm.addr(ownerPrivateKey); + + address[] memory modules; + bytes[] memory moduleData; + + core = new ERC1155Core("test", "TEST", "", owner, modules, moduleData); + mintableModule = new MintableERC1155(); + batchMetadataModule = new BatchMetadataERC1155(); + sequentialTokenIdModule = new SequentialTokenIdERC1155(); + + // install module + bytes memory encodedInstallParams = abi.encode(owner); + vm.prank(owner); + core.installModule(address(mintableModule), encodedInstallParams); + + vm.prank(owner); + core.installModule(address(batchMetadataModule), ""); + + vm.prank(owner); + core.installModule(address(sequentialTokenIdModule), ""); + + // Give permissioned actor minter role + vm.prank(owner); + core.grantRoles(owner, Role._MINTER_ROLE); + } + + /*////////////////////////////////////////////////////////////// + Tests: beforeMintERC1155 + //////////////////////////////////////////////////////////////*/ + + function test_state_updateTokenId() public { + assertEq(SequentialTokenIdERC1155(address(core)).getNextTokenId(), 0); + + // increments the tokenId + vm.prank(owner); + core.mint(owner, type(uint256).max, 10, "", ""); + + assertEq(SequentialTokenIdERC1155(address(core)).getNextTokenId(), 1); + + // does not increment the tokenId + vm.prank(owner); + core.mint(owner, 1, 10, "", ""); + + assertEq(SequentialTokenIdERC1155(address(core)).getNextTokenId(), 1); + } + + function test_revert_updateTokenId() public { + vm.expectRevert(SequentialTokenIdERC1155.SequentialTokenIdInvalidTokenId.selector); + vm.prank(owner); + core.mint(owner, 2, 1, "", ""); + } + +} diff --git a/test/module/transferable/TransferableERC1155.t.sol b/test/module/transferable/TransferableERC1155.t.sol index 9d8e4cfb..6e4ba990 100644 --- a/test/module/transferable/TransferableERC1155.t.sol +++ b/test/module/transferable/TransferableERC1155.t.sol @@ -27,10 +27,14 @@ contract Core is ERC1155Core { bytes[] memory moduleInstallData ) ERC1155Core(name, symbol, contractURI, owner, modules, moduleInstallData) {} - // disable mint and approve callbacks for these tests + // disable mint, approve and tokenId callbacks for these tests function _beforeMint(address to, uint256 tokenId, uint256 value, bytes memory data) internal override {} function _beforeApproveForAll(address from, address to, bool approved) internal override {} + function _updateTokenId(uint256 tokenId) internal override returns (uint256) { + return tokenId; + } + } contract TransferableERC1155Test is Test {