diff --git a/src/tokens/ERC1155/utility/sale/ERC1155Sale.sol b/src/tokens/ERC1155/utility/sale/ERC1155Sale.sol index 526baa1..841d06f 100644 --- a/src/tokens/ERC1155/utility/sale/ERC1155Sale.sol +++ b/src/tokens/ERC1155/utility/sale/ERC1155Sale.sol @@ -259,6 +259,52 @@ contract ERC1155Sale is IERC1155Sale, WithdrawControlled, MerkleProofSingleUse { emit TokenSaleDetailsUpdated(tokenId, cost, supplyCap, startTime, endTime, merkleRoot); } + /** + * Set the sale details for a batch of tokens. + * @param tokenIds The token IDs to set the sale details for. + * @param costs The amount of payment tokens to accept for each token minted. + * @param supplyCaps The maximum number of tokens that can be minted. + * @param startTimes The start time of the sale. Tokens cannot be minted before this time. + * @param endTimes The end time of the sale. Tokens cannot be minted after this time. + * @param merkleRoots The merkle root for allowlist minting. + * @dev A zero end time indicates an inactive sale. + * @notice The payment token is set globally. + * @dev tokenIds must be sorted ascending without duplicates. + */ + function setTokenSaleDetailsBatch( + uint256[] calldata tokenIds, + uint256[] calldata costs, + uint256[] calldata supplyCaps, + uint64[] calldata startTimes, + uint64[] calldata endTimes, + bytes32[] calldata merkleRoots + ) public onlyRole(MINT_ADMIN_ROLE) { + if ( + tokenIds.length != costs.length || tokenIds.length != supplyCaps.length + || tokenIds.length != startTimes.length || tokenIds.length != endTimes.length + || tokenIds.length != merkleRoots.length + ) { + revert InvalidSaleDetails(); + } + + uint256 lastTokenId; + for (uint256 i = 0; i < tokenIds.length; i++) { + uint256 tokenId = tokenIds[i]; + if (i != 0 && lastTokenId >= tokenId) { + revert InvalidTokenIds(); + } + lastTokenId = tokenId; + + // solhint-disable-next-line not-rely-on-time + if (endTimes[i] < startTimes[i] || endTimes[i] <= block.timestamp) { + revert InvalidSaleDetails(); + } + _tokenSaleDetails[tokenId] = + SaleDetails(costs[i], supplyCaps[i], startTimes[i], endTimes[i], merkleRoots[i]); + emit TokenSaleDetailsUpdated(tokenId, costs[i], supplyCaps[i], startTimes[i], endTimes[i], merkleRoots[i]); + } + } + // // Views // diff --git a/test/tokens/ERC1155/utility/sale/ERC1155SaleBase.t.sol b/test/tokens/ERC1155/utility/sale/ERC1155SaleBase.t.sol index 55d8f67..02ac2e4 100644 --- a/test/tokens/ERC1155/utility/sale/ERC1155SaleBase.t.sol +++ b/test/tokens/ERC1155/utility/sale/ERC1155SaleBase.t.sol @@ -22,7 +22,7 @@ import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.so // solhint-disable not-rely-on-time -contract ERC1155SaleTest is TestHelper, IERC1155SaleSignals, IERC1155SupplySignals { +contract ERC1155SaleBaseTest is TestHelper, IERC1155SaleSignals, IERC1155SupplySignals { // Redeclare events event TransferSingle( @@ -85,8 +85,10 @@ contract ERC1155SaleTest is TestHelper, IERC1155SaleSignals, IERC1155SupplySigna checkSelectorCollision(0x97559600); // setGlobalSaleDetails(uint256,uint256,uint64,uint64,bytes32) checkSelectorCollision(0x6a326ab1); // setPaymentToken(address) checkSelectorCollision(0x4f651ccd); // setTokenSaleDetails(uint256,uint256,uint256,uint64,uint64,bytes32) + checkSelectorCollision(0xf07f04ff); // setTokenSaleDetailsBatch(uint256[],uint256[],uint256[],uint64[],uint64[],bytes32[]) checkSelectorCollision(0x01ffc9a7); // supportsInterface(bytes4) checkSelectorCollision(0x0869678c); // tokenSaleDetails(uint256) + checkSelectorCollision(0xff81434e); // tokenSaleDetailsBatch(uint256[]) checkSelectorCollision(0x44004cc1); // withdrawERC20(address,address,uint256) checkSelectorCollision(0x4782f779); // withdrawETH(address,uint256) } @@ -100,6 +102,121 @@ contract ERC1155SaleTest is TestHelper, IERC1155SaleSignals, IERC1155SupplySigna assertEq(deployedAddr, predictedAddr); } + // + // Setter and getter + // + function testGlobalSaleDetails( + uint256 cost, + uint256 supplyCap, + uint64 startTime, + uint64 endTime, + bytes32 merkleRoot + ) public { + endTime = uint64(bound(endTime, block.timestamp + 1, type(uint64).max)); + endTime = uint64(bound(endTime, startTime, type(uint64).max)); + + // Setter + vm.expectEmit(true, true, true, true, address(sale)); + emit GlobalSaleDetailsUpdated(cost, supplyCap, startTime, endTime, merkleRoot); + sale.setGlobalSaleDetails(cost, supplyCap, startTime, endTime, merkleRoot); + + // Getter + IERC1155SaleFunctions.SaleDetails memory _saleDetails = sale.globalSaleDetails(); + assertEq(cost, _saleDetails.cost); + assertEq(supplyCap, _saleDetails.supplyCap); + assertEq(startTime, _saleDetails.startTime); + assertEq(endTime, _saleDetails.endTime); + assertEq(merkleRoot, _saleDetails.merkleRoot); + } + + function testTokenSaleDetails( + uint256 tokenId, + uint256 cost, + uint256 supplyCap, + uint64 startTime, + uint64 endTime, + bytes32 merkleRoot + ) public { + endTime = uint64(bound(endTime, block.timestamp + 1, type(uint64).max)); + endTime = uint64(bound(endTime, startTime, type(uint64).max)); + + // Setter + vm.expectEmit(true, true, true, true, address(sale)); + emit TokenSaleDetailsUpdated(tokenId, cost, supplyCap, startTime, endTime, merkleRoot); + sale.setTokenSaleDetails(tokenId, cost, supplyCap, startTime, endTime, merkleRoot); + + // Getter + IERC1155SaleFunctions.SaleDetails memory _saleDetails = sale.tokenSaleDetails(tokenId); + assertEq(cost, _saleDetails.cost); + assertEq(supplyCap, _saleDetails.supplyCap); + assertEq(startTime, _saleDetails.startTime); + assertEq(endTime, _saleDetails.endTime); + assertEq(merkleRoot, _saleDetails.merkleRoot); + } + + function testTokenSaleDetailsBatch( + uint256[] memory tokenIds, + uint256[] memory costs, + uint256[] memory supplyCaps, + uint64[] memory startTimes, + uint64[] memory endTimes, + bytes32[] memory merkleRoots + ) public { + uint256 minLength = tokenIds.length; + minLength = minLength > costs.length ? costs.length : minLength; + minLength = minLength > supplyCaps.length ? supplyCaps.length : minLength; + minLength = minLength > startTimes.length ? startTimes.length : minLength; + minLength = minLength > endTimes.length ? endTimes.length : minLength; + minLength = minLength > merkleRoots.length ? merkleRoots.length : minLength; + minLength = minLength > 5 ? 5 : minLength; // Max 5 + vm.assume(minLength > 0); + // solhint-disable-next-line no-inline-assembly + assembly { + mstore(tokenIds, minLength) + mstore(costs, minLength) + mstore(supplyCaps, minLength) + mstore(startTimes, minLength) + mstore(endTimes, minLength) + mstore(merkleRoots, minLength) + } + + // Sort tokenIds ascending and ensure no duplicates + for (uint256 i = 0; i < minLength; i++) { + for (uint256 j = i + 1; j < minLength; j++) { + if (tokenIds[i] > tokenIds[j]) { + (tokenIds[i], tokenIds[j]) = (tokenIds[j], tokenIds[i]); + } + } + } + for (uint256 i = 0; i < minLength - 1; i++) { + vm.assume(tokenIds[i] != tokenIds[i + 1]); + } + + for (uint256 i = 0; i < minLength; i++) { + endTimes[i] = uint64(bound(endTimes[i], block.timestamp + 1, type(uint64).max)); + endTimes[i] = uint64(bound(endTimes[i], startTimes[i], type(uint64).max)); + } + + // Setter + for (uint256 i = 0; i < minLength; i++) { + vm.expectEmit(true, true, true, true, address(sale)); + emit TokenSaleDetailsUpdated( + tokenIds[i], costs[i], supplyCaps[i], startTimes[i], endTimes[i], merkleRoots[i] + ); + } + sale.setTokenSaleDetailsBatch(tokenIds, costs, supplyCaps, startTimes, endTimes, merkleRoots); + + // Getter + IERC1155SaleFunctions.SaleDetails[] memory _saleDetails = sale.tokenSaleDetailsBatch(tokenIds); + for (uint256 i = 0; i < minLength; i++) { + assertEq(costs[i], _saleDetails[i].cost); + assertEq(supplyCaps[i], _saleDetails[i].supplyCap); + assertEq(startTimes[i], _saleDetails[i].startTime); + assertEq(endTimes[i], _saleDetails[i].endTime); + assertEq(merkleRoots[i], _saleDetails[i].merkleRoot); + } + } + // // Withdraw // diff --git a/test/tokens/ERC1155/utility/sale/ERC1155SaleMint.t.sol b/test/tokens/ERC1155/utility/sale/ERC1155SaleMint.t.sol index c8b6ac3..3777167 100644 --- a/test/tokens/ERC1155/utility/sale/ERC1155SaleMint.t.sol +++ b/test/tokens/ERC1155/utility/sale/ERC1155SaleMint.t.sol @@ -16,7 +16,7 @@ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; // solhint-disable not-rely-on-time -contract ERC1155SaleTest is TestHelper, IERC1155SaleSignals, IERC1155SupplySignals, IMerkleProofSingleUseSignals { +contract ERC1155SaleMintTest is TestHelper, IERC1155SaleSignals, IERC1155SupplySignals, IMerkleProofSingleUseSignals { // Redeclare events event TransferSingle( @@ -105,6 +105,9 @@ contract ERC1155SaleTest is TestHelper, IERC1155SaleSignals, IERC1155SupplySigna uint64 startTime, uint64 endTime ) public withFactory(useFactory) { + startTime = uint64(bound(startTime, 0, type(uint64).max - 1)); + endTime = uint64(bound(endTime, 0, type(uint64).max - 1)); + (tokenId, amount) = assumeSafe(mintTo, tokenId, amount); if (startTime > endTime) { uint64 temp = startTime; @@ -142,6 +145,9 @@ contract ERC1155SaleTest is TestHelper, IERC1155SaleSignals, IERC1155SupplySigna uint64 startTime, uint64 endTime ) public withFactory(useFactory) { + startTime = uint64(bound(startTime, 0, type(uint64).max - 1)); + endTime = uint64(bound(endTime, 0, type(uint64).max - 1)); + (tokenId, amount) = assumeSafe(mintTo, tokenId, amount); if (startTime > endTime) { uint64 temp = startTime; @@ -444,8 +450,9 @@ contract ERC1155SaleTest is TestHelper, IERC1155SaleSignals, IERC1155SupplySigna // Must be ordered (tokenIds[1], tokenIds[0]) = (tokenIds[0], tokenIds[1]); } + + // solhint-disable-next-line no-inline-assembly assembly { - // solhint-disable-line no-inline-assembly mstore(tokenIds, 2) // Exactly 2 unique tokenIds } uint256[] memory amounts = new uint256[](2); diff --git a/test/tokens/ERC721/utility/sale/ERC721SaleBase.t.sol b/test/tokens/ERC721/utility/sale/ERC721SaleBase.t.sol index 7784a8c..3c04812 100644 --- a/test/tokens/ERC721/utility/sale/ERC721SaleBase.t.sol +++ b/test/tokens/ERC721/utility/sale/ERC721SaleBase.t.sol @@ -22,7 +22,7 @@ import { IERC721AQueryable } from "erc721a/contracts/interfaces/IERC721AQueryabl // solhint-disable not-rely-on-time -contract ERC721SaleTest is TestHelper, IERC721SaleSignals { +contract ERC721SaleBaseTest is TestHelper, IERC721SaleSignals { // Redeclare events event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); diff --git a/test/tokens/ERC721/utility/sale/ERC721SaleMint.t.sol b/test/tokens/ERC721/utility/sale/ERC721SaleMint.t.sol index 7b9743f..f0c8d3e 100644 --- a/test/tokens/ERC721/utility/sale/ERC721SaleMint.t.sol +++ b/test/tokens/ERC721/utility/sale/ERC721SaleMint.t.sol @@ -14,7 +14,7 @@ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; // solhint-disable not-rely-on-time -contract ERC721SaleTest is TestHelper, IERC721SaleSignals, IMerkleProofSingleUseSignals { +contract ERC721SaleMintTest is TestHelper, IERC721SaleSignals, IMerkleProofSingleUseSignals { // Redeclare events event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); @@ -69,6 +69,9 @@ contract ERC721SaleTest is TestHelper, IERC721SaleSignals, IMerkleProofSingleUse uint64 startTime, uint64 endTime ) public assumeSafe(mintTo, amount) withFactory(useFactory) { + startTime = uint64(bound(startTime, 0, type(uint64).max - 1)); + endTime = uint64(bound(endTime, 0, type(uint64).max - 1)); + if (startTime > endTime) { uint64 temp = startTime; startTime = endTime;