diff --git a/src/tokens/ERC1155/README.md b/src/tokens/ERC1155/README.md index bacc4257..36b13d8a 100644 --- a/src/tokens/ERC1155/README.md +++ b/src/tokens/ERC1155/README.md @@ -18,16 +18,29 @@ This folder contains contracts that are pre-configured for specific use cases. The `ERC1155Items` contract is a preset that configures the `ERC1155BaseToken` contract to allow minting of tokens. It adds a `MINTER_ROLE` and a `mint(address to, uint256 amount)` function that can only be called by accounts with the `MINTER_ROLE`. +## Utilities + +This folder contains contracts that work in conjunction with other ERC1155 contracts. + ### Sale -The `ERC1155Sale` contract is a preset that configures the `ERC1155BaseToken` contract to allow for the sale of tokens. It adds a `mint(address to, , uint256[] memory tokenIds, uint256[] memory amounts, bytes memory data, bytes32[] calldata proof)` function allows for the minting of tokens under various conditions. +The `ERC1155Sale` contract is a utility contract that provides sale functionality for ERC-1155 tokens. It works in conjunction with an `ERC1155Items` contract to handle the minting and sale of tokens under various conditions. + +The contract supports multiple sale configurations through a sale details system. Each sale configuration includes: + +- Token ID range (minTokenId to maxTokenId) +- Cost per token +- Payment token (ETH or ERC20) +- Supply limit per token ID +- Sale time window (startTime to endTime) +- Optional merkle root for allowlist minting -Conditions may be set by the contract owner. Set the payment token with `setPaymentToken(address paymentTokenAddr)`, then use either the `setTokenSaleDetails(uint256 tokenId, uint256 cost, uint256 remainingSupply, uint64 startTime, uint64 endTime, bytes32 merkleRoot)` function for single token settings or the `setGlobalSaleDetails(uint256 cost, uint256 remainingSupply, uint64 startTime, uint64 endTime, bytes32 merkleRoot)` function for global settings. These functions can only be called by accounts with the `MINT_ADMIN_ROLE`. +Conditions may be set by the contract owner using the `addSaleDetails(SaleDetails calldata details)` function for new configurations or `updateSaleDetails(uint256 saleIndex, SaleDetails calldata details)` for existing ones. These functions can only be called by accounts with the `MINT_ADMIN_ROLE`. When using a merkle proof, each caller may only use each root once. To prevent collisions ensure the same root is not used for multiple sale details. -Leaves are defined as `keccak256(abi.encodePacked(caller, tokenId))`. The `caller` is the message sender, who will also receive the tokens. The `tokenId` is the id of the token that will be minted, (for global sales `type(uint256).max` is used). +Leaves are defined as `keccak256(abi.encodePacked(caller, tokenId))`. The `caller` is the message sender, who will also receive the tokens. The `tokenId` is the id of the token that will be minted. -For information about the function parameters, please refer to the function specification in `presets/sale/IERC1155Sale.sol`. +For information about the function parameters, please refer to the function specification in `utility/sale/IERC1155Sale.sol`. ## Usage diff --git a/src/tokens/ERC1155/utility/sale/ERC1155Sale.sol b/src/tokens/ERC1155/utility/sale/ERC1155Sale.sol index 1b616712..e8caeab7 100644 --- a/src/tokens/ERC1155/utility/sale/ERC1155Sale.sol +++ b/src/tokens/ERC1155/utility/sale/ERC1155Sale.sol @@ -5,7 +5,7 @@ import { MerkleProofSingleUse } from "../../../common/MerkleProofSingleUse.sol"; import { SignalsImplicitModeControlled } from "../../../common/SignalsImplicitModeControlled.sol"; import { AccessControlEnumerable, IERC20, SafeERC20, WithdrawControlled } from "../../../common/WithdrawControlled.sol"; import { IERC1155ItemsFunctions } from "../../presets/items/IERC1155Items.sol"; -import { IERC1155Sale, IERC1155SaleFunctions } from "./IERC1155Sale.sol"; +import { IERC1155Sale } from "./IERC1155Sale.sol"; contract ERC1155Sale is IERC1155Sale, WithdrawControlled, MerkleProofSingleUse, SignalsImplicitModeControlled { @@ -14,11 +14,10 @@ contract ERC1155Sale is IERC1155Sale, WithdrawControlled, MerkleProofSingleUse, bool private _initialized; address private _items; - // ERC20 token address for payment. address(0) indicated payment in ETH. - address private _paymentToken; - - SaleDetails private _globalSaleDetails; - mapping(uint256 => SaleDetails) private _tokenSaleDetails; + // Sales details indexed by sale index. + SaleDetails[] private _saleDetails; + // tokenId => saleIndex => quantity minted + mapping(uint256 => mapping(uint256 => uint256)) private _tokensMintedPerSale; /** * Initialize the contract. @@ -49,86 +48,76 @@ contract ERC1155Sale is IERC1155Sale, WithdrawControlled, MerkleProofSingleUse, _initialized = true; } - /** - * Checks if the current block.timestamp is out of the give timestamp range. - * @param _startTime Earliest acceptable timestamp (inclusive). - * @param _endTime Latest acceptable timestamp (exclusive). - * @dev A zero endTime value is always considered out of bounds. - */ - function _blockTimeOutOfBounds(uint256 _startTime, uint256 _endTime) private view returns (bool) { - // 0 end time indicates inactive sale. - return _endTime == 0 || block.timestamp < _startTime || block.timestamp >= _endTime; // solhint-disable-line not-rely-on-time - } - /** * Checks the sale is active, valid and takes payment. * @param _tokenIds Token IDs to mint. * @param _amounts Amounts of tokens to mint. + * @param _saleIndexes Sale indexes for each token. * @param _expectedPaymentToken ERC20 token address to accept payment in. address(0) indicates ETH. * @param _maxTotal Maximum amount of payment tokens. - * @param _proof Merkle proof for allowlist minting. + * @param _proofs Merkle proofs for allowlist minting. */ function _validateMint( - uint256[] memory _tokenIds, - uint256[] memory _amounts, + uint256[] calldata _tokenIds, + uint256[] calldata _amounts, + uint256[] calldata _saleIndexes, address _expectedPaymentToken, uint256 _maxTotal, - bytes32[] calldata _proof + bytes32[][] calldata _proofs ) private { - uint256 lastTokenId; uint256 totalCost; - uint256 totalAmount; - SaleDetails memory gSaleDetails = _globalSaleDetails; - bool globalSaleInactive = _blockTimeOutOfBounds(gSaleDetails.startTime, gSaleDetails.endTime); - bool globalMerkleCheckRequired = false; - for (uint256 i; i < _tokenIds.length; i++) { + // Validate input arrays have matching lengths + uint256 length = _tokenIds.length; + if (length != _amounts.length || length != _saleIndexes.length || length != _proofs.length) { + revert InvalidArrayLengths(); + } + + for (uint256 i; i < length; i++) { uint256 tokenId = _tokenIds[i]; - // Test tokenIds ordering - if (i != 0 && lastTokenId >= tokenId) { - revert InvalidTokenIds(); + uint256 saleIndex = _saleIndexes[i]; + + // Find the sale details for the token + if (saleIndex >= _saleDetails.length) { + revert SaleDetailsNotFound(saleIndex); + } + SaleDetails memory details = _saleDetails[saleIndex]; + + // Check if token is within the sale range + if (tokenId < details.minTokenId || tokenId > details.maxTokenId) { + revert InvalidSaleDetails(); + } + + // Check if sale is active + // solhint-disable-next-line not-rely-on-time + if (block.timestamp < details.startTime || block.timestamp > details.endTime) { + revert SaleInactive(); + } + + // Validate payment token matches expected + if (details.paymentToken != _expectedPaymentToken) { + revert PaymentTokenMismatch(); } - lastTokenId = tokenId; uint256 amount = _amounts[i]; + if (amount == 0) { + revert InvalidAmount(); + } - // Active sale test - SaleDetails memory saleDetails = _tokenSaleDetails[tokenId]; - bool tokenSaleInactive = _blockTimeOutOfBounds(saleDetails.startTime, saleDetails.endTime); - if (tokenSaleInactive) { - // Prefer token sale - if (globalSaleInactive) { - // Both sales inactive - revert SaleInactive(tokenId); - } - // Use global sale details - if (_globalSaleDetails.remainingSupply < amount) { - revert InsufficientSupply(_globalSaleDetails.remainingSupply, amount); - } - globalMerkleCheckRequired = true; - totalCost += gSaleDetails.cost * amount; - _globalSaleDetails.remainingSupply -= amount; - } else { - // Use token sale details - if (saleDetails.remainingSupply < amount) { - revert InsufficientSupply(saleDetails.remainingSupply, amount); - } - requireMerkleProof(saleDetails.merkleRoot, _proof, msg.sender, bytes32(tokenId)); - totalCost += saleDetails.cost * amount; - _tokenSaleDetails[tokenId].remainingSupply -= amount; + // Check supply + uint256 minted = _tokensMintedPerSale[tokenId][saleIndex]; + if (amount > details.supply - minted) { + revert InsufficientSupply(details.supply - minted, amount); } - totalAmount += amount; - } - if (globalMerkleCheckRequired) { - // Check it once outside the loop only when required - requireMerkleProof(gSaleDetails.merkleRoot, _proof, msg.sender, bytes32(type(uint256).max)); - } + // Check merkle proof + requireMerkleProof(details.merkleRoot, _proofs[i], msg.sender, bytes32(tokenId)); - if (_expectedPaymentToken != _paymentToken) { - // Caller expected different payment token - revert InsufficientPayment(_paymentToken, totalCost, 0); + // Update supply and calculate cost + _tokensMintedPerSale[tokenId][saleIndex] = minted + amount; + totalCost += details.cost * amount; } + if (_maxTotal < totalCost) { // Caller expected to pay less revert InsufficientPayment(_expectedPaymentToken, totalCost, _maxTotal); @@ -152,211 +141,100 @@ contract ERC1155Sale is IERC1155Sale, WithdrawControlled, MerkleProofSingleUse, // Minting // - /** - * Mint tokens. - * @param to Address to mint tokens to. - * @param tokenIds Token IDs to mint. - * @param amounts Amounts of tokens to mint. - * @param data Data to pass if receiver is contract. - * @param expectedPaymentToken ERC20 token address to accept payment in. address(0) indicates ETH. - * @param maxTotal Maximum amount of payment tokens. - * @param proof Merkle proof for allowlist minting. - * @notice Sale must be active for all tokens. - * @dev tokenIds must be sorted ascending without duplicates. - * @dev An empty proof is supplied when no proof is required. - */ + /// @inheritdoc IERC1155Sale + /// @notice Sale must be active for all tokens. + /// @dev All sales must use the same payment token. + /// @dev An empty proof is supplied when no proof is required. function mint( address to, - uint256[] memory tokenIds, - uint256[] memory amounts, - bytes memory data, + uint256[] calldata tokenIds, + uint256[] calldata amounts, + bytes calldata data, + uint256[] calldata saleIndexes, address expectedPaymentToken, uint256 maxTotal, - bytes32[] calldata proof + bytes32[][] calldata proofs ) public payable { - if (tokenIds.length != amounts.length) { - revert InvalidTokenIds(); - } - _validateMint(tokenIds, amounts, expectedPaymentToken, maxTotal, proof); + _validateMint(tokenIds, amounts, saleIndexes, expectedPaymentToken, maxTotal, proofs); IERC1155ItemsFunctions(_items).batchMint(to, tokenIds, amounts, data); - emit ItemsMinted(to, tokenIds, amounts); + emit ItemsMinted(to, tokenIds, amounts, saleIndexes); } // // Admin // - /** - * Set the payment token. - * @param paymentTokenAddr The ERC20 token address to accept payment in. address(0) indicates ETH. - * @dev This should be set before the sale starts. - */ - function setPaymentToken( - address paymentTokenAddr - ) public onlyRole(MINT_ADMIN_ROLE) { - _paymentToken = paymentTokenAddr; + /// @inheritdoc IERC1155Sale + function addSaleDetails( + SaleDetails calldata details + ) public onlyRole(MINT_ADMIN_ROLE) returns (uint256 saleIndex) { + _validateSaleDetails(details); + + saleIndex = _saleDetails.length; + _saleDetails.push(details); + + emit SaleDetailsAdded(saleIndex, details); } - /** - * Set the global sale details. - * @param cost The amount of payment tokens to accept for each token minted. - * @param remainingSupply The maximum number of tokens that can be minted by the items contract. - * @param startTime The start time of the sale. Tokens cannot be minted before this time. - * @param endTime The end time of the sale. Tokens cannot be minted after this time. - * @param merkleRoot The merkle root for allowlist minting. - * @dev A zero end time indicates an inactive sale. - * @notice The payment token is set globally. - */ - function setGlobalSaleDetails( - uint256 cost, - uint256 remainingSupply, - uint64 startTime, - uint64 endTime, - bytes32 merkleRoot - ) public onlyRole(MINT_ADMIN_ROLE) { - // solhint-disable-next-line not-rely-on-time - if (endTime < startTime || endTime <= block.timestamp) { - revert InvalidSaleDetails(); + /// @inheritdoc IERC1155Sale + function updateSaleDetails(uint256 saleIndex, SaleDetails calldata details) public onlyRole(MINT_ADMIN_ROLE) { + if (saleIndex >= _saleDetails.length) { + revert SaleDetailsNotFound(saleIndex); } - if (remainingSupply == 0) { - revert InvalidSaleDetails(); - } - _globalSaleDetails = SaleDetails(cost, remainingSupply, startTime, endTime, merkleRoot); - emit GlobalSaleDetailsUpdated(cost, remainingSupply, startTime, endTime, merkleRoot); + _validateSaleDetails(details); + + _saleDetails[saleIndex] = details; + + emit SaleDetailsUpdated(saleIndex, details); } - /** - * Set the sale details for an individual token. - * @param tokenId The token ID to set the sale details for. - * @param cost The amount of payment tokens to accept for each token minted. - * @param remainingSupply The maximum number of tokens that can be minted by this contract. - * @param startTime The start time of the sale. Tokens cannot be minted before this time. - * @param endTime The end time of the sale. Tokens cannot be minted after this time. - * @param merkleRoot The merkle root for allowlist minting. - * @dev A zero end time indicates an inactive sale. - * @notice The payment token is set globally. - */ - function setTokenSaleDetails( - uint256 tokenId, - uint256 cost, - uint256 remainingSupply, - uint64 startTime, - uint64 endTime, - bytes32 merkleRoot - ) public onlyRole(MINT_ADMIN_ROLE) { - // solhint-disable-next-line not-rely-on-time - if (endTime < startTime || endTime <= block.timestamp) { + function _validateSaleDetails( + SaleDetails calldata details + ) private pure { + if (details.maxTokenId < details.minTokenId) { revert InvalidSaleDetails(); } - if (remainingSupply == 0) { + if (details.supply == 0) { revert InvalidSaleDetails(); } - _tokenSaleDetails[tokenId] = SaleDetails(cost, remainingSupply, startTime, endTime, merkleRoot); - emit TokenSaleDetailsUpdated(tokenId, cost, remainingSupply, 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 remainingSupplies The maximum number of tokens that can be minted by this contract. - * @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 remainingSupplies, - uint64[] calldata startTimes, - uint64[] calldata endTimes, - bytes32[] calldata merkleRoots - ) public onlyRole(MINT_ADMIN_ROLE) { - if ( - tokenIds.length != costs.length || tokenIds.length != remainingSupplies.length - || tokenIds.length != startTimes.length || tokenIds.length != endTimes.length - || tokenIds.length != merkleRoots.length - ) { + if (details.endTime < details.startTime) { 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(); - } - if (remainingSupplies[i] == 0) { - revert InvalidSaleDetails(); - } - _tokenSaleDetails[tokenId] = - SaleDetails(costs[i], remainingSupplies[i], startTimes[i], endTimes[i], merkleRoots[i]); - emit TokenSaleDetailsUpdated( - tokenId, costs[i], remainingSupplies[i], startTimes[i], endTimes[i], merkleRoots[i] - ); - } } // // Views // - /** - * Get global sales details. - * @return Sale details. - * @notice Global sales details apply to all tokens. - * @notice Global sales details are overriden when token sale is active. - */ - function globalSaleDetails() external view returns (SaleDetails memory) { - return _globalSaleDetails; + /// @inheritdoc IERC1155Sale + function saleDetailsCount() external view returns (uint256) { + return _saleDetails.length; } - /** - * Get token sale details. - * @param tokenId Token ID to get sale details for. - * @return Sale details. - * @notice Token sale details override global sale details. - */ - function tokenSaleDetails( - uint256 tokenId + /// @inheritdoc IERC1155Sale + function saleDetails( + uint256 saleIndex ) external view returns (SaleDetails memory) { - return _tokenSaleDetails[tokenId]; + if (saleIndex >= _saleDetails.length) { + revert SaleDetailsNotFound(saleIndex); + } + return _saleDetails[saleIndex]; } - /** - * Get sale details for multiple tokens. - * @param tokenIds Array of token IDs to retrieve sale details for. - * @return Array of sale details corresponding to each token ID. - * @notice Each token's sale details override the global sale details if set. - */ - function tokenSaleDetailsBatch( - uint256[] calldata tokenIds + /// @inheritdoc IERC1155Sale + function saleDetailsBatch( + uint256[] calldata saleIndexes ) external view returns (SaleDetails[] memory) { - SaleDetails[] memory details = new SaleDetails[](tokenIds.length); - for (uint256 i = 0; i < tokenIds.length; i++) { - details[i] = _tokenSaleDetails[tokenIds[i]]; + SaleDetails[] memory details = new SaleDetails[](saleIndexes.length); + for (uint256 i = 0; i < saleIndexes.length; i++) { + if (saleIndexes[i] >= _saleDetails.length) { + revert SaleDetailsNotFound(saleIndexes[i]); + } + details[i] = _saleDetails[saleIndexes[i]]; } return details; } - /** - * Get payment token. - * @return Payment token address. - * @notice address(0) indicates payment in ETH. - */ - function paymentToken() external view returns (address) { - return _paymentToken; - } - /** * Check interface support. * @param interfaceId Interface id @@ -365,8 +243,7 @@ contract ERC1155Sale is IERC1155Sale, WithdrawControlled, MerkleProofSingleUse, function supportsInterface( bytes4 interfaceId ) public view virtual override(WithdrawControlled, SignalsImplicitModeControlled) returns (bool) { - return type(IERC1155SaleFunctions).interfaceId == interfaceId - || WithdrawControlled.supportsInterface(interfaceId) + return type(IERC1155Sale).interfaceId == interfaceId || WithdrawControlled.supportsInterface(interfaceId) || SignalsImplicitModeControlled.supportsInterface(interfaceId); } diff --git a/src/tokens/ERC1155/utility/sale/ERC1155SaleFactory.sol b/src/tokens/ERC1155/utility/sale/ERC1155SaleFactory.sol index 25c7f8ab..876dbce0 100644 --- a/src/tokens/ERC1155/utility/sale/ERC1155SaleFactory.sol +++ b/src/tokens/ERC1155/utility/sale/ERC1155SaleFactory.sol @@ -23,13 +23,14 @@ contract ERC1155SaleFactory is IERC1155SaleFactory, SequenceProxyFactory { /// @inheritdoc IERC1155SaleFactoryFunctions function deploy( + uint256 nonce, address proxyOwner, address tokenOwner, address items, address implicitModeValidator, bytes32 implicitModeProjectId ) external returns (address proxyAddr) { - bytes32 salt = keccak256(abi.encode(tokenOwner, items, implicitModeValidator, implicitModeProjectId)); + bytes32 salt = keccak256(abi.encode(nonce, tokenOwner, items, implicitModeValidator, implicitModeProjectId)); proxyAddr = _createProxy(salt, proxyOwner, ""); ERC1155Sale(proxyAddr).initialize(tokenOwner, items, implicitModeValidator, implicitModeProjectId); emit ERC1155SaleDeployed(proxyAddr); @@ -38,13 +39,14 @@ contract ERC1155SaleFactory is IERC1155SaleFactory, SequenceProxyFactory { /// @inheritdoc IERC1155SaleFactoryFunctions function determineAddress( + uint256 nonce, address proxyOwner, address tokenOwner, address items, address implicitModeValidator, bytes32 implicitModeProjectId ) external view returns (address proxyAddr) { - bytes32 salt = keccak256(abi.encode(tokenOwner, items, implicitModeValidator, implicitModeProjectId)); + bytes32 salt = keccak256(abi.encode(nonce, tokenOwner, items, implicitModeValidator, implicitModeProjectId)); return _computeProxyAddress(salt, proxyOwner, ""); } diff --git a/src/tokens/ERC1155/utility/sale/IERC1155Sale.sol b/src/tokens/ERC1155/utility/sale/IERC1155Sale.sol index 9436cc70..1df19b18 100644 --- a/src/tokens/ERC1155/utility/sale/IERC1155Sale.sol +++ b/src/tokens/ERC1155/utility/sale/IERC1155Sale.sol @@ -1,50 +1,69 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.19; -interface IERC1155SaleFunctions { +interface IERC1155Sale { + /** + * Sale details. + * @param minTokenId Minimum token ID for this sale. (Inclusive) + * @param maxTokenId Maximum token ID for this sale. (Inclusive) + * @param cost Cost per token. + * @param paymentToken ERC20 token address to accept payment in. address(0) indicates payment in ETH. + * @param supply Maximum number of tokens that can be minted (per token ID). + * @param startTime Start time of the sale. (Inclusive) + * @param endTime End time of the sale. (Inclusive) + * @param merkleRoot Merkle root for allowlist minting. 0 indicates no proof required. + */ struct SaleDetails { + uint256 minTokenId; + uint256 maxTokenId; uint256 cost; - uint256 remainingSupply; + address paymentToken; + uint256 supply; uint64 startTime; - uint64 endTime; // 0 end time indicates sale inactive - bytes32 merkleRoot; // Root of allowed addresses + uint64 endTime; + bytes32 merkleRoot; } /** - * Get global sales details. - * @return Sale details. - * @notice Global sales details apply to all tokens. - * @notice Global sales details are overriden when token sale is active. + * Get the total number of sale details. + * @return Total number of sale details. */ - function globalSaleDetails() external view returns (SaleDetails memory); + function saleDetailsCount() external view returns (uint256); /** - * Get token sale details. - * @param tokenId Token ID to get sale details for. - * @return Sale details. - * @notice Token sale details override global sale details. + * Get sale details. + * @param saleIndex Index of the sale details to get. + * @return details Sale details. */ - function tokenSaleDetails( - uint256 tokenId - ) external view returns (SaleDetails memory); + function saleDetails( + uint256 saleIndex + ) external view returns (SaleDetails memory details); /** - * Get sale details for multiple tokens. - * @param tokenIds Array of token IDs to retrieve sale details for. - * @return Array of sale details corresponding to each token ID. - * @notice Each token's sale details override the global sale details if set. + * Get sale details for multiple sale indexes. + * @param saleIndexes Array of sale indexes to retrieve sale details for. + * @return details Array of sale details corresponding to each sale index. */ - function tokenSaleDetailsBatch( - uint256[] calldata tokenIds - ) external view returns (SaleDetails[] memory); + function saleDetailsBatch( + uint256[] calldata saleIndexes + ) external view returns (SaleDetails[] memory details); /** - * Get payment token. - * @return Payment token address. - * @notice address(0) indicates payment in ETH. + * Add new sale details. + * @param details Sale details to add. + * @return saleIndex Index of the newly added sale details. */ - function paymentToken() external view returns (address); + function addSaleDetails( + SaleDetails calldata details + ) external returns (uint256 saleIndex); + + /** + * Update existing sale details. + * @param saleIndex Index of the sale details to update. + * @param details Sale details to update. + */ + function updateSaleDetails(uint256 saleIndex, SaleDetails calldata details) external; /** * Mint tokens. @@ -52,34 +71,47 @@ interface IERC1155SaleFunctions { * @param tokenIds Token IDs to mint. * @param amounts Amounts of tokens to mint. * @param data Data to pass if receiver is contract. - * @param paymentToken ERC20 token address to accept payment in. address(0) indicates ETH. + * @param saleIndexes Sale indexes for each token. Must match tokenIds length. + * @param expectedPaymentToken ERC20 token address to accept payment in. address(0) indicates ETH. * @param maxTotal Maximum amount of payment tokens. - * @param proof Merkle proof for allowlist minting. + * @param proofs Merkle proofs for allowlist minting. Must match tokenIds length. * @notice Sale must be active for all tokens. - * @dev tokenIds must be sorted ascending without duplicates. + * @dev All sales must use the same payment token. * @dev An empty proof is supplied when no proof is required. */ function mint( address to, - uint256[] memory tokenIds, - uint256[] memory amounts, - bytes memory data, - address paymentToken, + uint256[] calldata tokenIds, + uint256[] calldata amounts, + bytes calldata data, + uint256[] calldata saleIndexes, + address expectedPaymentToken, uint256 maxTotal, - bytes32[] calldata proof + bytes32[][] calldata proofs ) external payable; -} + /** + * Emitted when sale details are added. + * @param saleIndex Index of the sale details that were added. + * @param details Sale details that were added. + */ + event SaleDetailsAdded(uint256 saleIndex, SaleDetails details); -interface IERC1155SaleSignals { + /** + * Emitted when sale details are updated. + * @param saleIndex Index of the sale details that were updated. + * @param details Sale details that were updated. + */ + event SaleDetailsUpdated(uint256 saleIndex, SaleDetails details); - event GlobalSaleDetailsUpdated( - uint256 cost, uint256 remainingSupply, uint64 startTime, uint64 endTime, bytes32 merkleRoot - ); - event TokenSaleDetailsUpdated( - uint256 tokenId, uint256 cost, uint256 remainingSupply, uint64 startTime, uint64 endTime, bytes32 merkleRoot - ); - event ItemsMinted(address to, uint256[] tokenIds, uint256[] amounts); + /** + * Emitted when tokens are minted. + * @param to Address that minted the tokens. + * @param tokenIds Token IDs that were minted. + * @param amounts Amounts of tokens that were minted. + * @param saleIndexes Sale indexes that were minted from. + */ + event ItemsMinted(address to, uint256[] tokenIds, uint256[] amounts, uint256[] saleIndexes); /** * Contract already initialized. @@ -92,15 +124,14 @@ interface IERC1155SaleSignals { error InvalidSaleDetails(); /** - * Sale is not active globally. + * Sale details index does not exist. */ - error GlobalSaleInactive(); + error SaleDetailsNotFound(uint256 index); /** * Sale is not active. - * @param tokenId Invalid Token ID. */ - error SaleInactive(uint256 tokenId); + error SaleInactive(); /** * Insufficient tokens for payment. @@ -110,11 +141,6 @@ interface IERC1155SaleSignals { */ error InsufficientPayment(address currency, uint256 expected, uint256 actual); - /** - * Invalid token IDs. - */ - error InvalidTokenIds(); - /** * Insufficient supply of tokens. * @param remainingSupply Remaining supply. @@ -122,6 +148,19 @@ interface IERC1155SaleSignals { */ error InsufficientSupply(uint256 remainingSupply, uint256 amount); -} + /** + * Invalid array lengths. + */ + error InvalidArrayLengths(); + + /** + * Invalid amount. + */ + error InvalidAmount(); + + /** + * Payment token mismatch between sales. + */ + error PaymentTokenMismatch(); -interface IERC1155Sale is IERC1155SaleFunctions, IERC1155SaleSignals { } +} diff --git a/src/tokens/ERC1155/utility/sale/IERC1155SaleFactory.sol b/src/tokens/ERC1155/utility/sale/IERC1155SaleFactory.sol index 693d13dd..a022fe4e 100644 --- a/src/tokens/ERC1155/utility/sale/IERC1155SaleFactory.sol +++ b/src/tokens/ERC1155/utility/sale/IERC1155SaleFactory.sol @@ -5,6 +5,7 @@ interface IERC1155SaleFactoryFunctions { /** * Creates an ERC-1155 Sale proxy contract + * @param nonce Extra salt to add to the salt used to compute the address * @param proxyOwner The owner of the ERC-1155 Sale proxy * @param tokenOwner The owner of the ERC-1155 Sale implementation * @param items The ERC-1155 Items contract address @@ -14,6 +15,7 @@ interface IERC1155SaleFactoryFunctions { * @notice The deployed contract must be granted the MINTER_ROLE on the ERC-1155 Items contract. */ function deploy( + uint256 nonce, address proxyOwner, address tokenOwner, address items, @@ -23,6 +25,7 @@ interface IERC1155SaleFactoryFunctions { /** * Computes the address of a proxy instance. + * @param nonce Extra salt to add to the salt used to compute the address * @param proxyOwner The owner of the ERC-1155 Sale proxy * @param tokenOwner The owner of the ERC-1155 Sale implementation * @param items The ERC-1155 Items contract address @@ -31,6 +34,7 @@ interface IERC1155SaleFactoryFunctions { * @return proxyAddr The address of the ERC-1155 Sale Proxy */ function determineAddress( + uint256 nonce, address proxyOwner, address tokenOwner, address items, diff --git a/src/tokens/common/MerkleProofSingleUse.sol b/src/tokens/common/MerkleProofSingleUse.sol index 323b10fd..4aeaa4c8 100644 --- a/src/tokens/common/MerkleProofSingleUse.sol +++ b/src/tokens/common/MerkleProofSingleUse.sol @@ -6,12 +6,12 @@ import { IMerkleProofSingleUse } from "./IMerkleProofSingleUse.sol"; import { MerkleProof } from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; /** - * Require single use merkle proofs per address. + * Require single use merkle proofs per root. */ abstract contract MerkleProofSingleUse is IMerkleProofSingleUse { - // Stores proofs used by an address - mapping(address => mapping(bytes32 => bool)) private _proofUsed; + // Stores proofs used per root + mapping(bytes32 => mapping(bytes32 => bool)) private _proofUsed; /** * Requires the given merkle proof to be valid. @@ -24,10 +24,11 @@ abstract contract MerkleProofSingleUse is IMerkleProofSingleUse { */ function requireMerkleProof(bytes32 root, bytes32[] calldata proof, address addr, bytes32 salt) internal { if (root != bytes32(0)) { - if (!checkMerkleProof(root, proof, addr, salt)) { + bytes32 leaf = _getLeaf(addr, salt); + if (!_checkMerkleProof(root, proof, leaf)) { revert MerkleProofInvalid(root, proof, addr, salt); } - _proofUsed[addr][root] = true; + _proofUsed[root][leaf] = true; } } @@ -45,7 +46,15 @@ abstract contract MerkleProofSingleUse is IMerkleProofSingleUse { address addr, bytes32 salt ) public view returns (bool) { - return !_proofUsed[addr][root] && MerkleProof.verify(proof, root, keccak256(abi.encodePacked(addr, salt))); + return _checkMerkleProof(root, proof, _getLeaf(addr, salt)); + } + + function _getLeaf(address addr, bytes32 salt) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(addr, salt)); + } + + function _checkMerkleProof(bytes32 root, bytes32[] calldata proof, bytes32 leaf) internal view returns (bool) { + return !_proofUsed[root][leaf] && MerkleProof.verify(proof, root, leaf); } } diff --git a/test/tokens/ERC1155/utility/sale/ERC1155SaleBase.t.sol b/test/tokens/ERC1155/utility/sale/ERC1155SaleBase.t.sol index fa9cfab8..481dd4cb 100644 --- a/test/tokens/ERC1155/utility/sale/ERC1155SaleBase.t.sol +++ b/test/tokens/ERC1155/utility/sale/ERC1155SaleBase.t.sol @@ -6,9 +6,8 @@ import { ERC20Mock } from "../../../../_mocks/ERC20Mock.sol"; import { IERC1155Supply, IERC1155SupplySignals } from "src/tokens/ERC1155/extensions/supply/IERC1155Supply.sol"; import { ERC1155Items } from "src/tokens/ERC1155/presets/items/ERC1155Items.sol"; -import { ERC1155Sale } from "src/tokens/ERC1155/utility/sale/ERC1155Sale.sol"; +import { ERC1155Sale, IERC1155Sale } from "src/tokens/ERC1155/utility/sale/ERC1155Sale.sol"; import { ERC1155SaleFactory } from "src/tokens/ERC1155/utility/sale/ERC1155SaleFactory.sol"; -import { IERC1155SaleFunctions, IERC1155SaleSignals } from "src/tokens/ERC1155/utility/sale/IERC1155Sale.sol"; import { IAccessControl } from "openzeppelin-contracts/contracts/access/IAccessControl.sol"; import { Strings } from "openzeppelin-contracts/contracts/utils/Strings.sol"; @@ -18,7 +17,7 @@ import { ISignalsImplicitMode } from "signals-implicit-mode/src/helper/SignalsIm // solhint-disable not-rely-on-time -contract ERC1155SaleBaseTest is TestHelper, IERC1155SaleSignals, IERC1155SupplySignals { +contract ERC1155SaleBaseTest is TestHelper, IERC1155SupplySignals { // Redeclare events event TransferSingle( @@ -50,14 +49,14 @@ contract ERC1155SaleBaseTest is TestHelper, IERC1155SaleSignals, IERC1155SupplyS function setUpFromFactory() public { ERC1155SaleFactory factory = new ERC1155SaleFactory(address(this)); - sale = ERC1155Sale(factory.deploy(proxyOwner, address(this), address(token), address(0), bytes32(0))); + sale = ERC1155Sale(factory.deploy(0, proxyOwner, address(this), address(token), address(0), bytes32(0))); token.grantRole(keccak256("MINTER_ROLE"), address(sale)); } function testSupportsInterface() public view { assertTrue(sale.supportsInterface(type(IERC165).interfaceId)); assertTrue(sale.supportsInterface(type(IAccessControl).interfaceId)); - assertTrue(sale.supportsInterface(type(IERC1155SaleFunctions).interfaceId)); + assertTrue(sale.supportsInterface(type(IERC1155Sale).interfaceId)); assertTrue(sale.supportsInterface(type(ISignalsImplicitMode).interfaceId)); } @@ -68,32 +67,30 @@ contract ERC1155SaleBaseTest is TestHelper, IERC1155SaleSignals, IERC1155SupplyS function testSelectorCollision() public pure { checkSelectorCollision(0xa217fddf); // DEFAULT_ADMIN_ROLE() checkSelectorCollision(0x9d043a66); // acceptImplicitRequest(address,(address,bytes4,bytes32,bytes32,bytes,(string,uint64)),(address,uint256,bytes,uint256,bool,bool,uint256)) + checkSelectorCollision(0x436013db); // addSaleDetails((uint256,uint256,uint256,address,uint256,uint64,uint64,bytes32)) checkSelectorCollision(0xbad43661); // checkMerkleProof(bytes32,bytes32[],address,bytes32) checkSelectorCollision(0x248a9ca3); // getRoleAdmin(bytes32) checkSelectorCollision(0x9010d07c); // getRoleMember(bytes32,uint256) checkSelectorCollision(0xca15c873); // getRoleMemberCount(bytes32) - checkSelectorCollision(0x119cd50c); // globalSaleDetails() checkSelectorCollision(0x2f2ff15d); // grantRole(bytes32,address) checkSelectorCollision(0x91d14854); // hasRole(bytes32,address) checkSelectorCollision(0x63acc14d); // initialize(address,address,address,bytes32) - checkSelectorCollision(0x60e606f6); // mint(address,uint256[],uint256[],bytes,address,uint256,bytes32[]) - checkSelectorCollision(0x3013ce29); // paymentToken() + checkSelectorCollision(0xddced6e7); // mint(address,uint256[],uint256[],bytes,uint256[],address,uint256,bytes32[][]) checkSelectorCollision(0x36568abe); // renounceRole(bytes32,address) checkSelectorCollision(0xd547741f); // revokeRole(bytes32,address) - checkSelectorCollision(0x97559600); // setGlobalSaleDetails(uint256,uint256,uint64,uint64,bytes32) + checkSelectorCollision(0x989d6ed1); // saleDetails(uint256) + checkSelectorCollision(0xce6bcda7); // saleDetailsBatch(uint256[]) + checkSelectorCollision(0xfc640a87); // saleDetailsCount() checkSelectorCollision(0xed4c2ac7); // setImplicitModeProjectId(bytes32) checkSelectorCollision(0x0bb310de); // setImplicitModeValidator(address) - 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(0x26f63107); // updateSaleDetails(uint256,(uint256,uint256,uint256,address,uint256,uint64,uint64,bytes32)) checkSelectorCollision(0x44004cc1); // withdrawERC20(address,address,uint256) checkSelectorCollision(0x4782f779); // withdrawETH(address,uint256) } function testFactoryDetermineAddress( + uint256 nonce, address _proxyOwner, address tokenOwner, address items, @@ -104,131 +101,159 @@ contract ERC1155SaleBaseTest is TestHelper, IERC1155SaleSignals, IERC1155SupplyS vm.assume(tokenOwner != address(0)); ERC1155SaleFactory factory = new ERC1155SaleFactory(address(this)); address deployedAddr = - factory.deploy(_proxyOwner, tokenOwner, items, implicitModeValidator, implicitModeProjectId); - address predictedAddr = - factory.determineAddress(_proxyOwner, tokenOwner, items, implicitModeValidator, implicitModeProjectId); + factory.deploy(nonce, _proxyOwner, tokenOwner, items, implicitModeValidator, implicitModeProjectId); + address predictedAddr = factory.determineAddress( + nonce, _proxyOwner, tokenOwner, items, implicitModeValidator, implicitModeProjectId + ); assertEq(deployedAddr, predictedAddr); } // - // Setter and getter + // Admin // - function testGlobalSaleDetails( - uint256 cost, - uint256 remainingSupply, - uint64 startTime, - uint64 endTime, - bytes32 merkleRoot + function test_addSaleDetails_success( + IERC1155Sale.SaleDetails[] memory beforeDetails, + IERC1155Sale.SaleDetails memory details ) public { - remainingSupply = bound(remainingSupply, 1, type(uint256).max); - endTime = uint64(bound(endTime, block.timestamp + 1, type(uint64).max)); - startTime = uint64(bound(startTime, 0, endTime)); - - // Setter - vm.expectEmit(true, true, true, true, address(sale)); - emit GlobalSaleDetailsUpdated(cost, remainingSupply, startTime, endTime, merkleRoot); - sale.setGlobalSaleDetails(cost, remainingSupply, startTime, endTime, merkleRoot); - - // Getter - IERC1155SaleFunctions.SaleDetails memory _saleDetails = sale.globalSaleDetails(); - assertEq(cost, _saleDetails.cost); - assertEq(remainingSupply, _saleDetails.remainingSupply); - assertEq(startTime, _saleDetails.startTime); - assertEq(endTime, _saleDetails.endTime); - assertEq(merkleRoot, _saleDetails.merkleRoot); + for (uint256 i = 0; i < beforeDetails.length; i++) { + sale.addSaleDetails(validSaleDetails(0, beforeDetails[i])); + } + + details = validSaleDetails(0, details); + vm.expectEmit(true, true, true, true); + emit IERC1155Sale.SaleDetailsAdded(beforeDetails.length, details); + uint256 saleIndex = sale.addSaleDetails(details); + + assertEq(sale.saleDetailsCount(), beforeDetails.length + 1); + IERC1155Sale.SaleDetails memory actual = sale.saleDetails(saleIndex); + _compareSaleDetails(actual, details); } - function testTokenSaleDetails( - uint256 tokenId, - uint256 cost, - uint256 remainingSupply, - uint64 startTime, - uint64 endTime, - bytes32 merkleRoot + function test_addSaleDetails_fail_invalidTokenId( + IERC1155Sale.SaleDetails memory details ) public { - remainingSupply = bound(remainingSupply, 1, type(uint256).max); - endTime = uint64(bound(endTime, block.timestamp + 1, type(uint64).max)); - startTime = uint64(bound(startTime, 0, endTime)); - - // Setter - vm.expectEmit(true, true, true, true, address(sale)); - emit TokenSaleDetailsUpdated(tokenId, cost, remainingSupply, startTime, endTime, merkleRoot); - sale.setTokenSaleDetails(tokenId, cost, remainingSupply, startTime, endTime, merkleRoot); - - // Getter - IERC1155SaleFunctions.SaleDetails memory _saleDetails = sale.tokenSaleDetails(tokenId); - assertEq(cost, _saleDetails.cost); - assertEq(remainingSupply, _saleDetails.remainingSupply); - assertEq(startTime, _saleDetails.startTime); - assertEq(endTime, _saleDetails.endTime); - assertEq(merkleRoot, _saleDetails.merkleRoot); + details = validSaleDetails(0, details); + details.minTokenId = bound(details.minTokenId, 1, type(uint256).max); + details.maxTokenId = bound(details.maxTokenId, 0, details.minTokenId - 1); + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.InvalidSaleDetails.selector)); + sale.addSaleDetails(details); } - function testTokenSaleDetailsBatch( - uint256[] memory tokenIds, - uint256[] memory costs, - uint256[] memory remainingSupplys, - uint64[] memory startTimes, - uint64[] memory endTimes, - bytes32[] memory merkleRoots + function test_addSaleDetails_fail_invalidSupply( + IERC1155Sale.SaleDetails memory details ) public { - uint256 minLength = tokenIds.length; - minLength = minLength > costs.length ? costs.length : minLength; - minLength = minLength > remainingSupplys.length ? remainingSupplys.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(remainingSupplys, minLength) - mstore(startTimes, minLength) - mstore(endTimes, minLength) - mstore(merkleRoots, minLength) - } + details = validSaleDetails(0, details); + details.supply = 0; + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.InvalidSaleDetails.selector)); + sale.addSaleDetails(details); + } - // 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]); - } + function test_addSaleDetails_fail_invalidStartTime( + IERC1155Sale.SaleDetails memory details + ) public { + details = validSaleDetails(0, details); + details.startTime = uint64(bound(details.startTime, 1, type(uint64).max)); + details.endTime = uint64(bound(details.endTime, 0, details.startTime - 1)); + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.InvalidSaleDetails.selector)); + sale.addSaleDetails(details); + } - // Bound values - for (uint256 i = 0; i < minLength; i++) { - remainingSupplys[i] = bound(remainingSupplys[i], 1, type(uint256).max); - endTimes[i] = uint64(bound(endTimes[i], block.timestamp + 1, type(uint64).max)); - startTimes[i] = uint64(bound(startTimes[i], 0, endTimes[i])); + function test_updateSaleDetails_success( + IERC1155Sale.SaleDetails[] memory beforeDetails, + IERC1155Sale.SaleDetails memory newDetails, + uint256 saleIndex + ) public { + vm.assume(beforeDetails.length > 0); + saleIndex = bound(saleIndex, 0, beforeDetails.length - 1); + for (uint256 i = 0; i < beforeDetails.length; i++) { + sale.addSaleDetails(validSaleDetails(0, beforeDetails[i])); } + uint256 beforeUpdateCount = sale.saleDetailsCount(); + + newDetails = validSaleDetails(0, newDetails); + + vm.expectEmit(true, true, true, true); + emit IERC1155Sale.SaleDetailsUpdated(saleIndex, newDetails); + sale.updateSaleDetails(saleIndex, newDetails); + + assertEq(sale.saleDetailsCount(), beforeUpdateCount); // Unchanged + IERC1155Sale.SaleDetails memory actual = sale.saleDetails(saleIndex); + _compareSaleDetails(actual, newDetails); + } - // Setter - for (uint256 i = 0; i < minLength; i++) { - vm.expectEmit(true, true, true, true, address(sale)); - emit TokenSaleDetailsUpdated( - tokenIds[i], costs[i], remainingSupplys[i], startTimes[i], endTimes[i], merkleRoots[i] - ); + function test_updateSaleDetails_fail_notFound( + IERC1155Sale.SaleDetails memory newDetails, + uint256 saleIndex + ) public { + vm.assume(saleIndex >= sale.saleDetailsCount()); + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.SaleDetailsNotFound.selector, saleIndex)); + sale.updateSaleDetails(saleIndex, newDetails); + } + + function test_updateSaleDetails_fail_invalidTokenId( + IERC1155Sale.SaleDetails memory details + ) public { + details = validSaleDetails(0, details); + uint256 saleIndex = sale.addSaleDetails(details); + details.minTokenId = bound(details.minTokenId, 1, type(uint256).max); + details.maxTokenId = bound(details.maxTokenId, 0, details.minTokenId - 1); + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.InvalidSaleDetails.selector)); + sale.updateSaleDetails(saleIndex, details); + } + + function test_updateSaleDetails_fail_invalidSupply( + IERC1155Sale.SaleDetails memory details + ) public { + details = validSaleDetails(0, details); + uint256 saleIndex = sale.addSaleDetails(details); + details.supply = 0; + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.InvalidSaleDetails.selector)); + sale.updateSaleDetails(saleIndex, details); + } + + function test_updateSaleDetails_fail_invalidStartTime( + IERC1155Sale.SaleDetails memory details + ) public { + details = validSaleDetails(0, details); + uint256 saleIndex = sale.addSaleDetails(details); + details.startTime = uint64(bound(details.startTime, 1, type(uint64).max)); + details.endTime = uint64(bound(details.endTime, 0, details.startTime - 1)); + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.InvalidSaleDetails.selector)); + sale.updateSaleDetails(saleIndex, details); + } + + function test_saleDetails_fail_notFound( + uint256 saleIndex + ) public { + vm.assume(saleIndex >= sale.saleDetailsCount()); + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.SaleDetailsNotFound.selector, saleIndex)); + sale.saleDetails(saleIndex); + } + + function test_saleDetailsBatch( + IERC1155Sale.SaleDetails[] memory details + ) public { + uint256[] memory saleIndexes = new uint256[](details.length); + for (uint256 i = 0; i < details.length; i++) { + details[i] = validSaleDetails(0, details[i]); + saleIndexes[i] = sale.addSaleDetails(details[i]); } - sale.setTokenSaleDetailsBatch(tokenIds, costs, remainingSupplys, 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(remainingSupplys[i], _saleDetails[i].remainingSupply); - assertEq(startTimes[i], _saleDetails[i].startTime); - assertEq(endTimes[i], _saleDetails[i].endTime); - assertEq(merkleRoots[i], _saleDetails[i].merkleRoot); + assertEq(sale.saleDetailsCount(), details.length); + IERC1155Sale.SaleDetails[] memory actual = sale.saleDetailsBatch(saleIndexes); + assertEq(actual.length, details.length); + for (uint256 i = 0; i < details.length; i++) { + _compareSaleDetails(actual[i], details[i]); } } + function test_saleDetailsBatch_fail_notFound( + uint256[] memory saleIndexes + ) public { + vm.assume(saleIndexes.length > 0); + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.SaleDetailsNotFound.selector, saleIndexes[0])); + sale.saleDetailsBatch(saleIndexes); + } + // // Withdraw // @@ -295,12 +320,33 @@ contract ERC1155SaleBaseTest is TestHelper, IERC1155SaleSignals, IERC1155SupplyS _; } - modifier assumeSafe(address nonContract, uint256 tokenId, uint256 amount) { - assumeSafeAddress(nonContract); - vm.assume(nonContract != proxyOwner); - vm.assume(tokenId < 100); - vm.assume(amount > 0 && amount < 20); - _; + function validSaleDetails( + uint256 validTokenId, + IERC1155Sale.SaleDetails memory saleDetails + ) public view returns (IERC1155Sale.SaleDetails memory) { + saleDetails.minTokenId = bound(saleDetails.minTokenId, 0, validTokenId); + saleDetails.maxTokenId = bound(saleDetails.maxTokenId, validTokenId, type(uint256).max); + saleDetails.supply = bound(saleDetails.supply, 1, type(uint256).max); + saleDetails.cost = bound(saleDetails.cost, 0, type(uint256).max / saleDetails.supply); + saleDetails.startTime = uint64(bound(saleDetails.startTime, 0, block.timestamp)); + saleDetails.endTime = uint64(bound(saleDetails.endTime, block.timestamp, type(uint64).max)); + saleDetails.paymentToken = address(0); + saleDetails.merkleRoot = bytes32(0); + return saleDetails; + } + + function _compareSaleDetails( + IERC1155Sale.SaleDetails memory actual, + IERC1155Sale.SaleDetails memory expected + ) internal pure { + assertEq(actual.minTokenId, expected.minTokenId); + assertEq(actual.maxTokenId, expected.maxTokenId); + assertEq(actual.cost, expected.cost); + assertEq(actual.paymentToken, expected.paymentToken); + assertEq(actual.supply, expected.supply); + assertEq(actual.startTime, expected.startTime); + assertEq(actual.endTime, expected.endTime); + assertEq(actual.merkleRoot, expected.merkleRoot); } } diff --git a/test/tokens/ERC1155/utility/sale/ERC1155SaleMint.t.sol b/test/tokens/ERC1155/utility/sale/ERC1155SaleMint.t.sol index 5790a7cf..8523bb2a 100644 --- a/test/tokens/ERC1155/utility/sale/ERC1155SaleMint.t.sol +++ b/test/tokens/ERC1155/utility/sale/ERC1155SaleMint.t.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.19; +import { console } from "forge-std/console.sol"; + import { TestHelper } from "../../../../TestHelper.sol"; import { ERC20Mock } from "../../../../_mocks/ERC20Mock.sol"; @@ -8,12 +10,12 @@ import { IERC1155SupplySignals } from "src/tokens/ERC1155/extensions/supply/IERC import { ERC1155Items } from "src/tokens/ERC1155/presets/items/ERC1155Items.sol"; import { ERC1155Sale } from "src/tokens/ERC1155/utility/sale/ERC1155Sale.sol"; import { ERC1155SaleFactory } from "src/tokens/ERC1155/utility/sale/ERC1155SaleFactory.sol"; -import { IERC1155SaleSignals } from "src/tokens/ERC1155/utility/sale/IERC1155Sale.sol"; +import { IERC1155Sale } from "src/tokens/ERC1155/utility/sale/IERC1155Sale.sol"; import { IMerkleProofSingleUseSignals } from "src/tokens/common/IMerkleProofSingleUse.sol"; // solhint-disable not-rely-on-time -contract ERC1155SaleMintTest is TestHelper, IERC1155SaleSignals, IERC1155SupplySignals, IMerkleProofSingleUseSignals { +contract ERC1155SaleMintTest is TestHelper, IERC1155SupplySignals, IMerkleProofSingleUseSignals { // Redeclare events event TransferSingle( @@ -26,13 +28,8 @@ contract ERC1155SaleMintTest is TestHelper, IERC1155SaleSignals, IERC1155SupplyS ERC1155Items private token; ERC1155Sale private sale; ERC20Mock private erc20; - uint256 private perTokenCost = 0.02 ether; - - address private proxyOwner; function setUp() public { - proxyOwner = makeAddr("proxyOwner"); - token = new ERC1155Items(); token.initialize(address(this), "test", "ipfs://", "ipfs://", address(this), 0, address(0), bytes32(0)); @@ -41,643 +38,764 @@ contract ERC1155SaleMintTest is TestHelper, IERC1155SaleSignals, IERC1155SupplyS token.grantRole(keccak256("MINTER_ROLE"), address(sale)); - vm.deal(address(this), 1e6 ether); + erc20 = new ERC20Mock(address(this)); } function setUpFromFactory() public { ERC1155SaleFactory factory = new ERC1155SaleFactory(address(this)); - sale = ERC1155Sale(factory.deploy(proxyOwner, address(this), address(token), address(0), bytes32(0))); + sale = ERC1155Sale(factory.deploy(0, address(this), address(this), address(token), address(0), bytes32(0))); token.grantRole(keccak256("MINTER_ROLE"), address(sale)); } // // Minting // + function test_mint_fail_invalidArrayLength(uint256[] memory array1, uint256[] memory array2) public { + vm.assume(array1.length != array2.length); + bytes32[][] memory proofs = new bytes32[][](array1.length); + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.InvalidArrayLengths.selector)); + sale.mint(address(0), array1, array1, "", array2, address(0), 0, proofs); + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.InvalidArrayLengths.selector)); + sale.mint(address(0), array1, array2, "", array1, address(0), 0, proofs); + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.InvalidArrayLengths.selector)); + sale.mint(address(0), array2, array1, "", array1, address(0), 0, proofs); + } + + function test_mint_fail_invalidProofsArrayLength(uint256[] memory array1, bytes32[][] memory proofs) public { + vm.assume(array1.length != proofs.length); + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.InvalidArrayLengths.selector)); + sale.mint(address(0), array1, array1, "", array1, address(0), 0, proofs); + } - // Minting denied when no sale active. - function testMintInactiveFail( + function test_mint_fail_noSale(uint256 tokenId, uint256 amount) public { + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + uint256[] memory saleIndexes = TestHelper.singleToArray(0); + bytes32[][] memory proofs = new bytes32[][](1); + proofs[0] = TestHelper.blankProof(); + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.SaleDetailsNotFound.selector, 0)); + sale.mint(address(0), tokenIds, amounts, "", saleIndexes, address(0), 0, proofs); + } + + function test_mint_success( bool useFactory, - address mintTo, + address recipient, + IERC1155Sale.SaleDetails memory details, uint256 tokenId, uint256 amount - ) public withFactory(useFactory) { - (tokenId, amount) = assumeSafe(mintTo, tokenId, amount); + ) public withFactory(useFactory) returns (uint256 saleIndex) { + assumeSafeAddress(recipient); + details = validSaleDetails(tokenId, details); + saleIndex = sale.addSaleDetails(details); + amount = bound(amount, 1, details.supply); uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); uint256[] memory amounts = TestHelper.singleToArray(amount); - uint256 cost = amount * perTokenCost; + uint256[] memory saleIndexes = TestHelper.singleToArray(saleIndex); + bytes32[][] memory proofs = new bytes32[][](1); + proofs[0] = TestHelper.blankProof(); - vm.expectRevert(abi.encodeWithSelector(SaleInactive.selector, tokenId)); - sale.mint{ value: cost }(mintTo, tokenIds, amounts, "", address(0), cost, TestHelper.blankProof()); + uint256 expectedCost = details.cost * amount; + vm.deal(address(this), expectedCost); + + vm.expectEmit(true, true, true, true, address(token)); + emit TransferBatch(address(sale), address(0), recipient, tokenIds, amounts); + vm.expectEmit(true, true, true, true, address(sale)); + emit IERC1155Sale.ItemsMinted(recipient, tokenIds, amounts, saleIndexes); + sale.mint{ value: expectedCost }( + recipient, tokenIds, amounts, "", saleIndexes, address(0), expectedCost, proofs + ); + + assertEq(address(sale).balance, expectedCost); + + return saleIndex; } - // Minting denied when sale is active but not for the token. - function testMintInactiveSingleFail( + function test_mint_successERC20( bool useFactory, - address mintTo, + address recipient, + IERC1155Sale.SaleDetails memory details, uint256 tokenId, uint256 amount - ) public withFactory(useFactory) { - (tokenId, amount) = assumeSafe(mintTo, tokenId, amount); - setTokenSaleActive(tokenId + 1); - uint256 cost = amount * perTokenCost; - - vm.expectRevert(abi.encodeWithSelector(SaleInactive.selector, tokenId)); - sale.mint{ value: cost }( - mintTo, - TestHelper.singleToArray(tokenId), - TestHelper.singleToArray(amount), - "", - address(0), - cost, - TestHelper.blankProof() - ); + ) public withFactory(useFactory) returns (uint256 saleIndex) { + assumeSafeAddress(recipient); + details = validSaleDetails(tokenId, details); + details.paymentToken = address(erc20); + saleIndex = sale.addSaleDetails(details); + amount = bound(amount, 1, details.supply); + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + uint256[] memory saleIndexes = TestHelper.singleToArray(saleIndex); + bytes32[][] memory proofs = new bytes32[][](1); + proofs[0] = TestHelper.blankProof(); + + uint256 expectedCost = details.cost * amount; + erc20.mint(address(this), expectedCost); + erc20.approve(address(sale), expectedCost); + + vm.expectEmit(true, true, true, true, address(token)); + emit TransferBatch(address(sale), address(0), recipient, tokenIds, amounts); + vm.expectEmit(true, true, true, true, address(sale)); + emit IERC1155Sale.ItemsMinted(recipient, tokenIds, amounts, saleIndexes); + sale.mint(recipient, tokenIds, amounts, "", saleIndexes, address(erc20), expectedCost, proofs); + + assertEq(erc20.balanceOf(address(this)), 0); + assertEq(erc20.balanceOf(address(sale)), expectedCost); + + return saleIndex; } - // Minting denied when token sale is expired. - function testMintExpiredSingleFail( + function test_mint_success_proof( bool useFactory, - address mintTo, + address recipient, + IERC1155Sale.SaleDetails memory details, uint256 tokenId, uint256 amount, - uint64 startTime, - uint64 endTime + address[] memory allowlist, + uint256 leafIndex ) 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; - startTime = endTime; - endTime = temp; - } - if (endTime == 0) { - endTime++; - } + assumeSafeAddress(recipient); + details = validSaleDetails(tokenId, details); + + vm.assume(allowlist.length > 1); + leafIndex = bound(leafIndex, 0, allowlist.length - 1); - vm.warp(uint256(endTime) - 1); - sale.setTokenSaleDetails(tokenId, perTokenCost, amount, startTime, endTime, ""); - vm.warp(uint256(endTime) + 1); - - uint256 cost = amount * perTokenCost; - - vm.expectRevert(abi.encodeWithSelector(SaleInactive.selector, tokenId)); - sale.mint{ value: cost }( - mintTo, - TestHelper.singleToArray(tokenId), - TestHelper.singleToArray(amount), - "", - address(0), - cost, - TestHelper.blankProof() + address sender = allowlist[leafIndex]; + + bytes32[][] memory proofs = new bytes32[][](1); + (details.merkleRoot, proofs[0]) = TestHelper.getMerkleParts(allowlist, tokenId, leafIndex); + + uint256 saleIndex = sale.addSaleDetails(details); + amount = bound(amount, 1, details.supply); + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + uint256[] memory saleIndexes = TestHelper.singleToArray(saleIndex); + + uint256 expectedCost = details.cost * amount; + vm.deal(sender, expectedCost); + + vm.expectEmit(true, true, true, true, address(token)); + emit TransferBatch(address(sale), address(0), recipient, tokenIds, amounts); + vm.expectEmit(true, true, true, true, address(sale)); + emit IERC1155Sale.ItemsMinted(recipient, tokenIds, amounts, saleIndexes); + vm.prank(sender); + sale.mint{ value: expectedCost }( + recipient, tokenIds, amounts, "", saleIndexes, address(0), expectedCost, proofs ); + + assertEq(address(sale).balance, expectedCost); } - // Minting denied when global sale is expired. - function testMintExpiredGlobalFail( + function test_mint_fail_invalidTokenId( bool useFactory, - address mintTo, + address recipient, + IERC1155Sale.SaleDetails memory details, uint256 tokenId, uint256 amount, - uint64 startTime, - uint64 endTime + bool tokenIdAboveMax ) 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; - startTime = endTime; - endTime = temp; - } - if (endTime == 0) { - endTime++; + details = validSaleDetails(tokenId, details); + if (tokenIdAboveMax) { + vm.assume(details.maxTokenId < type(uint256).max); + tokenId = bound(tokenId, details.maxTokenId + 1, type(uint256).max); + } else { + vm.assume(details.minTokenId > 0); + tokenId = bound(tokenId, 0, details.minTokenId - 1); } - vm.warp(uint256(endTime) - 1); - sale.setGlobalSaleDetails(perTokenCost, amount, startTime, endTime, ""); - vm.warp(uint256(endTime) + 1); - - uint256 cost = amount * perTokenCost; - - vm.expectRevert(abi.encodeWithSelector(SaleInactive.selector, tokenId)); - sale.mint{ value: cost }( - mintTo, - TestHelper.singleToArray(tokenId), - TestHelper.singleToArray(amount), - "", - address(0), - cost, - TestHelper.blankProof() + uint256 saleIndex = sale.addSaleDetails(details); + amount = bound(amount, 1, details.supply); + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + uint256[] memory saleIndexes = TestHelper.singleToArray(saleIndex); + bytes32[][] memory proofs = new bytes32[][](1); + proofs[0] = TestHelper.blankProof(); + + uint256 expectedCost = details.cost * amount; + vm.deal(address(this), expectedCost); + + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.InvalidSaleDetails.selector)); + sale.mint{ value: expectedCost }( + recipient, tokenIds, amounts, "", saleIndexes, address(0), expectedCost, proofs ); } - // Minting denied when sale is active but not for all tokens in the group. - function testMintInactiveInGroupFail( + function test_mint_successERC20_higherExpectedCost( bool useFactory, - address mintTo, + address recipient, + IERC1155Sale.SaleDetails memory details, uint256 tokenId, - uint256 amount - ) public withFactory(useFactory) { - (tokenId, amount) = assumeSafe(mintTo, tokenId, amount); - setTokenSaleActive(tokenId); - uint256[] memory tokenIds = new uint256[](2); - tokenIds[0] = tokenId; - tokenIds[1] = tokenId + 1; - uint256[] memory amounts = new uint256[](2); - amounts[0] = amount; - amounts[1] = amount; - uint256 cost = amount * perTokenCost * 2; + uint256 amount, + uint256 expectedCost + ) public withFactory(useFactory) returns (uint256 saleIndex) { + assumeSafeAddress(recipient); + details = validSaleDetails(tokenId, details); + details.paymentToken = address(erc20); + saleIndex = sale.addSaleDetails(details); + amount = bound(amount, 1, details.supply); + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + uint256[] memory saleIndexes = TestHelper.singleToArray(saleIndex); + bytes32[][] memory proofs = new bytes32[][](1); + proofs[0] = TestHelper.blankProof(); + + uint256 realExpectedCost = details.cost * amount; + expectedCost = bound(expectedCost, realExpectedCost, type(uint256).max); + erc20.mint(address(this), expectedCost); + erc20.approve(address(sale), expectedCost); - vm.expectRevert(abi.encodeWithSelector(SaleInactive.selector, tokenId + 1)); - sale.mint{ value: cost }(mintTo, tokenIds, amounts, "", address(0), cost, TestHelper.blankProof()); + vm.expectEmit(true, true, true, true, address(token)); + emit TransferBatch(address(sale), address(0), recipient, tokenIds, amounts); + vm.expectEmit(true, true, true, true, address(sale)); + emit IERC1155Sale.ItemsMinted(recipient, tokenIds, amounts, saleIndexes); + sale.mint(recipient, tokenIds, amounts, "", saleIndexes, address(erc20), expectedCost, proofs); + + assertEq(erc20.balanceOf(address(this)), expectedCost - realExpectedCost); + assertEq(erc20.balanceOf(address(sale)), realExpectedCost); + + return saleIndex; } - // Minting denied when global supply exceeded. - function testMintGlobalSupplyExceeded( + function test_mint_fail_beforeSale( bool useFactory, - address mintTo, + address recipient, + IERC1155Sale.SaleDetails memory details, uint256 tokenId, uint256 amount, - uint256 remainingSupply + uint256 blockTime ) public withFactory(useFactory) { - (tokenId, amount) = assumeSafe(mintTo, tokenId, amount); - amount = bound(amount, 2, 20); - remainingSupply = bound(remainingSupply, 1, amount - 1); - sale.setGlobalSaleDetails( - perTokenCost, remainingSupply, uint64(block.timestamp), uint64(block.timestamp + 1), "" - ); + details = validSaleDetails(tokenId, details); + blockTime = bound(blockTime, 0, type(uint64).max - 1); + details.startTime = uint64(bound(details.startTime, blockTime + 1, type(uint64).max)); + details.endTime = uint64(bound(details.endTime, details.startTime, type(uint64).max)); + vm.warp(blockTime); + + uint256 saleIndex = sale.addSaleDetails(details); + amount = bound(amount, 1, details.supply); + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + uint256[] memory saleIndexes = TestHelper.singleToArray(saleIndex); + bytes32[][] memory proofs = new bytes32[][](1); + proofs[0] = TestHelper.blankProof(); - uint256 cost = amount * perTokenCost; - - vm.expectRevert(abi.encodeWithSelector(InsufficientSupply.selector, remainingSupply, amount)); - sale.mint{ value: cost }( - mintTo, - TestHelper.singleToArray(tokenId), - TestHelper.singleToArray(amount), - "", - address(0), - cost, - TestHelper.blankProof() - ); + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.SaleInactive.selector)); + sale.mint(recipient, tokenIds, amounts, "", saleIndexes, address(0), 0, proofs); } - // Minting denied when token supply exceeded. - function testMintTokenSupplyExceeded( + function test_mint_fail_afterSale( bool useFactory, - address mintTo, + address recipient, + IERC1155Sale.SaleDetails memory details, uint256 tokenId, uint256 amount, - uint256 remainingSupply + uint256 blockTime ) public withFactory(useFactory) { - (tokenId, amount) = assumeSafe(mintTo, tokenId, amount); - amount = bound(amount, 2, 20); - remainingSupply = bound(remainingSupply, 1, amount - 1); - sale.setTokenSaleDetails( - tokenId, perTokenCost, remainingSupply, uint64(block.timestamp), uint64(block.timestamp + 1), "" - ); + details = validSaleDetails(tokenId, details); + blockTime = bound(blockTime, 1, type(uint64).max); + details.endTime = uint64(bound(details.endTime, 0, blockTime - 1)); + details.startTime = uint64(bound(details.startTime, 0, details.endTime)); + vm.warp(blockTime); + + uint256 saleIndex = sale.addSaleDetails(details); + amount = bound(amount, 1, details.supply); + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + uint256[] memory saleIndexes = TestHelper.singleToArray(saleIndex); + bytes32[][] memory proofs = new bytes32[][](1); + proofs[0] = TestHelper.blankProof(); - uint256 cost = amount * perTokenCost; - - vm.expectRevert(abi.encodeWithSelector(InsufficientSupply.selector, remainingSupply, amount)); - sale.mint{ value: cost }( - mintTo, - TestHelper.singleToArray(tokenId), - TestHelper.singleToArray(amount), - "", - address(0), - cost, - TestHelper.blankProof() - ); + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.SaleInactive.selector)); + sale.mint(recipient, tokenIds, amounts, "", saleIndexes, address(0), 0, proofs); } - // Minting allowed when sale is active globally. - function testMintGlobalSuccess( + function test_mint_fail_supplyExceeded( bool useFactory, - address mintTo, + address recipient, + IERC1155Sale.SaleDetails memory details, uint256 tokenId, uint256 amount - ) public withFactory(useFactory) withGlobalSaleActive { - (tokenId, amount) = assumeSafe(mintTo, tokenId, amount); - uint256 cost = amount * perTokenCost; - - uint256 count = token.balanceOf(mintTo, tokenId); - { - uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); - uint256[] memory amounts = TestHelper.singleToArray(amount); - vm.expectEmit(true, true, true, true, address(token)); - emit TransferBatch(address(sale), address(0), mintTo, tokenIds, amounts); - vm.expectEmit(true, true, true, true, address(sale)); - emit ItemsMinted(mintTo, tokenIds, amounts); - sale.mint{ value: cost }(mintTo, tokenIds, amounts, "", address(0), cost, TestHelper.blankProof()); - } - assertEq(count + amount, token.balanceOf(mintTo, tokenId)); + ) public withFactory(useFactory) { + details = validSaleDetails(tokenId, details); + details.supply = bound(details.supply, 1, type(uint256).max - 1); + + uint256 saleIndex = sale.addSaleDetails(details); + amount = bound(amount, details.supply + 1, type(uint256).max); + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + uint256[] memory saleIndexes = TestHelper.singleToArray(saleIndex); + bytes32[][] memory proofs = new bytes32[][](1); + proofs[0] = TestHelper.blankProof(); + + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.InsufficientSupply.selector, details.supply, amount)); + sale.mint(recipient, tokenIds, amounts, "", saleIndexes, address(0), 0, proofs); } - // Minting allowed when sale is active for the token. - function testMintSingleSuccess( + function test_mint_fail_supplyExceededOnSubsequentMint( bool useFactory, - address mintTo, + address recipient, + IERC1155Sale.SaleDetails memory details, uint256 tokenId, + uint256 minted, uint256 amount ) public withFactory(useFactory) { - (tokenId, amount) = assumeSafe(mintTo, tokenId, amount); - setTokenSaleActive(tokenId); - uint256 cost = amount * perTokenCost; - - uint256 count = token.balanceOf(mintTo, tokenId); - { - uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); - uint256[] memory amounts = TestHelper.singleToArray(amount); - vm.expectEmit(true, true, true, true, address(token)); - emit TransferBatch(address(sale), address(0), mintTo, tokenIds, amounts); - vm.expectEmit(true, true, true, true, address(sale)); - emit ItemsMinted(mintTo, tokenIds, amounts); - sale.mint{ value: cost }(mintTo, tokenIds, amounts, "", address(0), cost, TestHelper.blankProof()); - } - assertEq(count + amount, token.balanceOf(mintTo, tokenId)); + details = validSaleDetails(tokenId, details); + details.supply = bound(details.supply, 1, type(uint256).max - 1); + minted = bound(minted, 1, details.supply); + uint256 saleIndex = test_mint_success(useFactory, recipient, details, tokenId, minted); + + // New amount exceeds supply + amount = bound(amount, details.supply - minted + 1, type(uint256).max); + + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + uint256[] memory saleIndexes = TestHelper.singleToArray(saleIndex); + bytes32[][] memory proofs = new bytes32[][](1); + proofs[0] = TestHelper.blankProof(); + vm.expectRevert( + abi.encodeWithSelector(IERC1155Sale.InsufficientSupply.selector, details.supply - minted, amount) + ); + sale.mint(recipient, tokenIds, amounts, "", saleIndexes, address(0), 0, proofs); } - // Minting allowed when sale is active for both tokens individually. - function testMintGroupSuccess( + function test_mint_fail_incorrectPayment( bool useFactory, - address mintTo, + address recipient, + IERC1155Sale.SaleDetails memory details, uint256 tokenId, - uint256 amount + uint256 amount, + uint256 expectedCost ) public withFactory(useFactory) { - (tokenId, amount) = assumeSafe(mintTo, tokenId, amount); - setTokenSaleActive(tokenId); - setTokenSaleActive(tokenId + 1); - uint256[] memory tokenIds = new uint256[](2); - tokenIds[0] = tokenId; - tokenIds[1] = tokenId + 1; - uint256[] memory amounts = new uint256[](2); - amounts[0] = amount; - amounts[1] = amount; - uint256 cost = amount * perTokenCost * 2; - - uint256 count = token.balanceOf(mintTo, tokenId); - uint256 count2 = token.balanceOf(mintTo, tokenId + 1); - vm.expectEmit(true, true, true, true, address(token)); - emit TransferBatch(address(sale), address(0), mintTo, tokenIds, amounts); - vm.expectEmit(true, true, true, true, address(sale)); - emit ItemsMinted(mintTo, tokenIds, amounts); - sale.mint{ value: cost }(mintTo, tokenIds, amounts, "", address(0), cost, TestHelper.blankProof()); - assertEq(count + amount, token.balanceOf(mintTo, tokenId)); - assertEq(count2 + amount, token.balanceOf(mintTo, tokenId + 1)); + assumeSafeAddress(recipient); + details = validSaleDetails(tokenId, details); + if (details.cost == 0) { + details.cost = 1; + } + uint256 saleIndex = sale.addSaleDetails(details); + amount = bound(amount, 1, details.supply); + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + uint256[] memory saleIndexes = TestHelper.singleToArray(saleIndex); + bytes32[][] memory proofs = new bytes32[][](1); + proofs[0] = TestHelper.blankProof(); + + uint256 realExpectedCost = details.cost * amount; + expectedCost = bound(expectedCost, 0, realExpectedCost - 1); // Force different cost + vm.deal(address(this), expectedCost); + + vm.expectRevert( + abi.encodeWithSelector( + IERC1155Sale.InsufficientPayment.selector, details.paymentToken, realExpectedCost, expectedCost + ) + ); + sale.mint{ value: expectedCost }( + recipient, tokenIds, amounts, "", saleIndexes, address(0), expectedCost, proofs + ); } - // Minting allowed when global sale is free. - function testFreeGlobalMint( + function test_mint_fail_insufficientPaymentERC20( bool useFactory, - address mintTo, + address recipient, + IERC1155Sale.SaleDetails memory details, uint256 tokenId, - uint256 amount + uint256 amount, + uint256 expectedCost ) public withFactory(useFactory) { - (tokenId, amount) = assumeSafe(mintTo, tokenId, amount); - sale.setGlobalSaleDetails(0, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), ""); + assumeSafeAddress(recipient); + details = validSaleDetails(tokenId, details); + details.paymentToken = address(erc20); + if (details.cost == 0) { + details.cost = 1; + } + uint256 saleIndex = sale.addSaleDetails(details); + amount = bound(amount, 1, details.supply); uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); uint256[] memory amounts = TestHelper.singleToArray(amount); - - uint256 count = token.balanceOf(mintTo, tokenId); - vm.expectEmit(true, true, true, true, address(token)); - emit TransferBatch(address(sale), address(0), mintTo, tokenIds, amounts); - vm.expectEmit(true, true, true, true, address(sale)); - emit ItemsMinted(mintTo, tokenIds, amounts); - sale.mint(mintTo, tokenIds, amounts, "", address(0), 0, TestHelper.blankProof()); - assertEq(count + amount, token.balanceOf(mintTo, tokenId)); + uint256[] memory saleIndexes = TestHelper.singleToArray(saleIndex); + bytes32[][] memory proofs = new bytes32[][](1); + proofs[0] = TestHelper.blankProof(); + + uint256 realExpectedCost = details.cost * amount; + erc20.mint(address(this), realExpectedCost); + erc20.approve(address(sale), expectedCost); + expectedCost = bound(expectedCost, 0, realExpectedCost - 1); + + vm.expectRevert( + abi.encodeWithSelector( + IERC1155Sale.InsufficientPayment.selector, details.paymentToken, realExpectedCost, expectedCost + ) + ); + sale.mint(recipient, tokenIds, amounts, "", saleIndexes, address(erc20), expectedCost, proofs); } - // Minting allowed when token sale is free and global is not. - function testFreeTokenMint( + function test_mint_fail_invalidExpectedPaymentToken( bool useFactory, - address mintTo, + address recipient, + IERC1155Sale.SaleDetails memory details, uint256 tokenId, - uint256 amount - ) public withFactory(useFactory) withGlobalSaleActive { - (tokenId, amount) = assumeSafe(mintTo, tokenId, amount); - sale.setTokenSaleDetails( - tokenId, 0, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), "" - ); + uint256 amount, + address expectedPaymentToken + ) public withFactory(useFactory) { + assumeSafeAddress(recipient); + details = validSaleDetails(tokenId, details); + vm.assume(details.paymentToken != expectedPaymentToken); + uint256 saleIndex = sale.addSaleDetails(details); + amount = bound(amount, 1, details.supply); uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); uint256[] memory amounts = TestHelper.singleToArray(amount); + uint256[] memory saleIndexes = TestHelper.singleToArray(saleIndex); + bytes32[][] memory proofs = new bytes32[][](1); + proofs[0] = TestHelper.blankProof(); - uint256 count = token.balanceOf(mintTo, tokenId); - vm.expectEmit(true, true, true, true, address(token)); - emit TransferBatch(address(sale), address(0), mintTo, tokenIds, amounts); - vm.expectEmit(true, true, true, true, address(sale)); - emit ItemsMinted(mintTo, tokenIds, amounts); - sale.mint(mintTo, tokenIds, amounts, "", address(0), 0, TestHelper.blankProof()); - assertEq(count + amount, token.balanceOf(mintTo, tokenId)); + uint256 expectedCost = details.cost * amount; + vm.deal(address(this), expectedCost); + + vm.expectRevert( + abi.encodeWithSelector( + IERC1155Sale.PaymentTokenMismatch.selector, details.paymentToken, expectedPaymentToken + ) + ); + sale.mint{ value: expectedCost }( + recipient, tokenIds, amounts, "", saleIndexes, expectedPaymentToken, expectedCost, proofs + ); } - // Minting allowed when mint charged with ERC20. - function testERC20Mint( + function test_mint_fail_invalidExpectedCost( bool useFactory, - address mintTo, + address recipient, + IERC1155Sale.SaleDetails memory details, uint256 tokenId, - uint256 amount - ) public withFactory(useFactory) withERC20 { - (tokenId, amount) = assumeSafe(mintTo, tokenId, amount); - sale.setPaymentToken(address(erc20)); - sale.setGlobalSaleDetails( - perTokenCost, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), "" - ); + uint256 amount, + uint256 expectedCost + ) public withFactory(useFactory) { + assumeSafeAddress(recipient); + details = validSaleDetails(tokenId, details); + if (details.cost == 0) { + details.cost = 1; + } + uint256 saleIndex = sale.addSaleDetails(details); + amount = bound(amount, 1, details.supply); uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); uint256[] memory amounts = TestHelper.singleToArray(amount); - uint256 cost = amount * perTokenCost; - - uint256 balance = erc20.balanceOf(address(this)); - uint256 count = token.balanceOf(mintTo, tokenId); - vm.expectEmit(true, true, true, true, address(token)); - emit TransferBatch(address(sale), address(0), mintTo, tokenIds, amounts); - vm.expectEmit(true, true, true, true, address(sale)); - emit ItemsMinted(mintTo, tokenIds, amounts); - sale.mint(mintTo, tokenIds, amounts, "", address(erc20), cost, TestHelper.blankProof()); - assertEq(count + amount, token.balanceOf(mintTo, tokenId)); - assertEq(balance - cost, erc20.balanceOf(address(this))); - assertEq(cost, erc20.balanceOf(address(sale))); + uint256[] memory saleIndexes = TestHelper.singleToArray(saleIndex); + bytes32[][] memory proofs = new bytes32[][](1); + proofs[0] = TestHelper.blankProof(); + + uint256 realExpectedCost = details.cost * amount; + vm.deal(address(this), realExpectedCost); + expectedCost = bound(expectedCost, 0, realExpectedCost - 1); + + vm.expectRevert( + abi.encodeWithSelector( + IERC1155Sale.InsufficientPayment.selector, details.paymentToken, realExpectedCost, expectedCost + ) + ); + sale.mint{ value: realExpectedCost }( + recipient, tokenIds, amounts, "", saleIndexes, details.paymentToken, expectedCost, proofs + ); } - // Minting with merkle success. - function testMerkleSuccess( - address[] memory allowlist, - uint256 senderIndex, + function test_mint_fail_invalidExpectedCostERC20( + bool useFactory, + address recipient, + IERC1155Sale.SaleDetails memory details, uint256 tokenId, - bool globalActive - ) public returns (address sender, bytes32 root, bytes32[] memory proof) { - // Construct a merkle tree with the allowlist. - vm.assume(allowlist.length > 1); - senderIndex = bound(senderIndex, 0, allowlist.length - 1); - sender = allowlist[senderIndex]; - assumeSafeAddress(sender); - - uint256 salt = globalActive ? type(uint256).max : tokenId; - (root, proof) = TestHelper.getMerkleParts(allowlist, salt, senderIndex); - - if (globalActive) { - sale.setGlobalSaleDetails( - 0, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), root - ); - } else { - sale.setTokenSaleDetails( - tokenId, 0, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), root - ); + uint256 amount, + uint256 expectedCost + ) public withFactory(useFactory) { + assumeSafeAddress(recipient); + details = validSaleDetails(tokenId, details); + details.paymentToken = address(erc20); + if (details.cost == 0) { + details.cost = 1; } + uint256 saleIndex = sale.addSaleDetails(details); + amount = bound(amount, 1, details.supply); + uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); + uint256[] memory amounts = TestHelper.singleToArray(amount); + uint256[] memory saleIndexes = TestHelper.singleToArray(saleIndex); + bytes32[][] memory proofs = new bytes32[][](1); + proofs[0] = TestHelper.blankProof(); + + uint256 realExpectedCost = details.cost * amount; + erc20.mint(address(this), realExpectedCost); + erc20.approve(address(sale), expectedCost); + expectedCost = bound(expectedCost, 0, realExpectedCost - 1); + + vm.expectRevert( + abi.encodeWithSelector( + IERC1155Sale.InsufficientPayment.selector, details.paymentToken, realExpectedCost, expectedCost + ) + ); + sale.mint(recipient, tokenIds, amounts, "", saleIndexes, details.paymentToken, expectedCost, proofs); + } - vm.expectEmit(true, true, true, true, address(sale)); + function test_mint_fail_valueOnERC20Payment( + bool useFactory, + address recipient, + IERC1155Sale.SaleDetails memory details, + uint256 tokenId, + uint256 amount, + uint256 value + ) public withFactory(useFactory) returns (uint256 saleIndex) { + assumeSafeAddress(recipient); + details = validSaleDetails(tokenId, details); + details.paymentToken = address(erc20); + saleIndex = sale.addSaleDetails(details); + amount = bound(amount, 1, details.supply); uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); - uint256[] memory amounts = TestHelper.singleToArray(uint256(1)); - emit ItemsMinted(sender, tokenIds, amounts); - vm.prank(sender); - sale.mint(sender, tokenIds, amounts, "", address(0), 0, proof); + uint256[] memory amounts = TestHelper.singleToArray(amount); + uint256[] memory saleIndexes = TestHelper.singleToArray(saleIndex); + bytes32[][] memory proofs = new bytes32[][](1); + proofs[0] = TestHelper.blankProof(); - assertEq(1, token.balanceOf(sender, tokenId)); + uint256 expectedCost = details.cost * amount; + erc20.mint(address(this), expectedCost); + erc20.approve(address(sale), expectedCost); + + value = bound(value, 1, type(uint256).max); + vm.deal(address(this), value); + + vm.expectRevert(abi.encodeWithSelector(IERC1155Sale.InsufficientPayment.selector, address(0), 0, value)); + sale.mint{ value: value }(recipient, tokenIds, amounts, "", saleIndexes, address(erc20), expectedCost, proofs); } - // Minting with merkle success. - function testMerkleSuccessGlobalMultiple( - address[] memory allowlist, - uint256 senderIndex, - uint256[] memory tokenIds - ) public { - uint256 tokenIdLen = tokenIds.length; - vm.assume(tokenIdLen > 1); - vm.assume(tokenIds[0] != tokenIds[1]); - if (tokenIds[0] > tokenIds[1]) { - // Must be ordered - (tokenIds[1], tokenIds[0]) = (tokenIds[0], tokenIds[1]); - } + function test_mint_multiple_success( + bool useFactory, + address recipient, + IERC1155Sale.SaleDetails memory details1, + IERC1155Sale.SaleDetails memory details2, + uint256 tokenId1, + uint256 tokenId2, + uint256 amount1, + uint256 amount2 + ) public withFactory(useFactory) { + assumeSafeAddress(recipient); + details1 = validSaleDetails(tokenId1, details1); + details2 = validSaleDetails(tokenId2, details2); - // solhint-disable-next-line no-inline-assembly - assembly { - mstore(tokenIds, 2) // Exactly 2 unique tokenIds - } - uint256[] memory amounts = new uint256[](2); - amounts[0] = 1; - amounts[1] = 1; + // Avoid overflows on total cost + details1.cost = details1.cost / 2 + 1; + details2.cost = details2.cost / 2 + 1; + details1.supply = details1.supply / 2 + 1; + details2.supply = details2.supply / 2 + 1; - // Construct a merkle tree with the allowlist. - vm.assume(allowlist.length > 1); - senderIndex = bound(senderIndex, 0, allowlist.length - 1); - address sender = allowlist[senderIndex]; - assumeSafeAddress(sender); + amount1 = bound(amount1, 1, details1.supply); + amount2 = bound(amount2, 1, details2.supply); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = tokenId1; + tokenIds[1] = tokenId2; + uint256[] memory amounts = new uint256[](2); + amounts[0] = amount1; + amounts[1] = amount2; + bytes32[][] memory proofs = new bytes32[][](2); + uint256[] memory saleIndexes = new uint256[](2); - (bytes32 root, bytes32[] memory proof) = TestHelper.getMerkleParts(allowlist, type(uint256).max, senderIndex); + saleIndexes[0] = sale.addSaleDetails(details1); + saleIndexes[1] = sale.addSaleDetails(details2); - sale.setGlobalSaleDetails(0, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), root); + uint256 totalCost = (details1.cost * amount1) + (details2.cost * amount2); + vm.deal(address(this), totalCost); + vm.expectEmit(true, true, true, true, address(token)); + emit TransferBatch(address(sale), address(0), recipient, tokenIds, amounts); vm.expectEmit(true, true, true, true, address(sale)); - emit ItemsMinted(sender, tokenIds, amounts); - vm.prank(sender); - sale.mint(sender, tokenIds, amounts, "", address(0), 0, proof); + emit IERC1155Sale.ItemsMinted(recipient, tokenIds, amounts, saleIndexes); + sale.mint{ value: totalCost }( + recipient, tokenIds, amounts, "", saleIndexes, details1.paymentToken, totalCost, proofs + ); - assertEq(1, token.balanceOf(sender, tokenIds[0])); - assertEq(1, token.balanceOf(sender, tokenIds[1])); + assertEq(address(sale).balance, totalCost); } - // Minting with merkle reuse fail. - function testMerkleReuseFail( - address[] memory allowlist, - uint256 senderIndex, - uint256 tokenId, - bool globalActive - ) public { - (address sender, bytes32 root, bytes32[] memory proof) = - testMerkleSuccess(allowlist, senderIndex, tokenId, globalActive); - - { - vm.expectRevert( - abi.encodeWithSelector( - MerkleProofInvalid.selector, root, proof, sender, globalActive ? type(uint256).max : tokenId - ) - ); - vm.prank(sender); - sale.mint( - sender, - TestHelper.singleToArray(tokenId), - TestHelper.singleToArray(uint256(1)), - "", - address(0), - 0, - proof - ); - } - } + function test_mint_multiple_success_proof( + bool useFactory, + address recipient, + IERC1155Sale.SaleDetails memory details1, + IERC1155Sale.SaleDetails memory details2, + uint256 tokenId1, + uint256 tokenId2, + uint256 amount1, + uint256 amount2, + address[] memory allowlist1, + uint256 leafIndex1, + address[] memory allowlist2, + uint256 leafIndex2 + ) public withFactory(useFactory) { + assumeSafeAddress(recipient); + details1 = validSaleDetails(tokenId1, details1); + details2 = validSaleDetails(tokenId2, details2); - // Minting with merkle fail no proof. - function testMerkleFailNoProof( - address[] memory allowlist, - address sender, - uint256 tokenId, - bool globalActive - ) public { - // Construct a merkle tree with the allowlist. - vm.assume(allowlist.length > 1); + // Avoid overflows on total cost + details1.cost = details1.cost / 2 + 1; + details2.cost = details2.cost / 2 + 1; + details1.supply = details1.supply / 2 + 1; + details2.supply = details2.supply / 2 + 1; - uint256 salt = globalActive ? type(uint256).max : tokenId; - (bytes32 root,) = TestHelper.getMerkleParts(allowlist, salt, 0); - bytes32[] memory proof = TestHelper.blankProof(); + vm.assume(allowlist1.length > 1); + leafIndex1 = bound(leafIndex1, 0, allowlist1.length - 1); - if (globalActive) { - sale.setGlobalSaleDetails( - 0, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), root - ); - } else { - sale.setTokenSaleDetails( - tokenId, 0, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), root - ); - } + vm.assume(allowlist2.length > 1); + leafIndex2 = bound(leafIndex2, 0, allowlist2.length - 1); + + address sender = allowlist1[leafIndex1]; + allowlist2[leafIndex2] = sender; + + bytes32[][] memory proofs = new bytes32[][](2); + (details1.merkleRoot, proofs[0]) = TestHelper.getMerkleParts(allowlist1, tokenId1, leafIndex1); + (details2.merkleRoot, proofs[1]) = TestHelper.getMerkleParts(allowlist2, tokenId2, leafIndex2); - vm.expectRevert(abi.encodeWithSelector(MerkleProofInvalid.selector, root, proof, sender, salt)); + amount1 = bound(amount1, 1, details1.supply); + amount2 = bound(amount2, 1, details2.supply); + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = tokenId1; + tokenIds[1] = tokenId2; + uint256[] memory amounts = new uint256[](2); + amounts[0] = amount1; + amounts[1] = amount2; + uint256[] memory saleIndexes = new uint256[](2); + saleIndexes[0] = sale.addSaleDetails(details1); + saleIndexes[1] = sale.addSaleDetails(details2); + + uint256 expectedCost = details1.cost * amount1 + details2.cost * amount2; + vm.deal(sender, expectedCost); + + vm.expectEmit(true, true, true, true, address(token)); + emit TransferBatch(address(sale), address(0), recipient, tokenIds, amounts); + vm.expectEmit(true, true, true, true, address(sale)); + emit IERC1155Sale.ItemsMinted(recipient, tokenIds, amounts, saleIndexes); vm.prank(sender); - sale.mint( - sender, TestHelper.singleToArray(tokenId), TestHelper.singleToArray(uint256(1)), "", address(0), 0, proof + sale.mint{ value: expectedCost }( + recipient, tokenIds, amounts, "", saleIndexes, address(0), expectedCost, proofs ); + + assertEq(address(sale).balance, expectedCost); } - // Minting with merkle fail bad proof. - function testMerkleFailBadProof( + function test_mint_repeat_success_proof( + bool useFactory, + address recipient, + IERC1155Sale.SaleDetails memory details, + uint256 tokenId1, + uint256 tokenId2, + uint256 amount1, + uint256 amount2, address[] memory allowlist, - address sender, - uint256 tokenId, - bool globalActive - ) public { - // Construct a merkle tree with the allowlist. - vm.assume(allowlist.length > 1); - vm.assume(allowlist[1] != sender); - - uint256 salt = globalActive ? type(uint256).max : tokenId; - (bytes32 root, bytes32[] memory proof) = TestHelper.getMerkleParts(allowlist, salt, 1); // Wrong sender - - if (globalActive) { - sale.setGlobalSaleDetails( - 0, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), root - ); - } else { - sale.setTokenSaleDetails( - tokenId, 0, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), root - ); + uint256 leafIndex1, + uint256 leafIndex2 + ) public withFactory(useFactory) { + assumeSafeAddress(recipient); + vm.assume(tokenId1 != tokenId2); + if (tokenId1 > tokenId2) { + (tokenId1, tokenId2) = (tokenId2, tokenId1); } + details = validSaleDetails(tokenId1, details); + details.maxTokenId = bound(details.maxTokenId, tokenId2, type(uint256).max); - uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); - uint256[] memory amounts = TestHelper.singleToArray(uint256(1)); + // Avoid overflows on total cost + details.cost = details.cost / 10 + 1; + details.supply = details.supply / 10 + 1; - vm.expectRevert(abi.encodeWithSelector(MerkleProofInvalid.selector, root, proof, sender, salt)); - vm.prank(sender); - sale.mint(sender, tokenIds, amounts, "", address(0), 0, proof); - } + vm.assume(allowlist.length > 1); + uint256 maxAllowList = allowlist.length > 10 ? 10 : allowlist.length; + assembly { + mstore(allowlist, maxAllowList) + } - // Minting fails with invalid maxTotal. - function testMintFailMaxTotal( - bool useFactory, - address mintTo, - uint256 tokenId, - uint256 amount - ) public withFactory(useFactory) withGlobalSaleActive { - (tokenId, amount) = assumeSafe(mintTo, tokenId, amount); - uint256[] memory tokenIds = TestHelper.singleToArray(tokenId); - uint256[] memory amounts = TestHelper.singleToArray(amount); - uint256 cost = amount * perTokenCost; + // Construct a merkle tree that supports multiple tokens + bytes32[] memory leaves = new bytes32[](allowlist.length * 2); + leafIndex1 = bound(leafIndex1, 0, leaves.length - 1); + leafIndex2 = bound(leafIndex2, 0, leaves.length - 1); + vm.assume(leafIndex1 != leafIndex2); + for (uint256 i = 0; i < leaves.length; i++) { + if (i == leafIndex1) { + leaves[i] = keccak256(abi.encodePacked(address(this), bytes32(tokenId1))); + } else if (i == leafIndex2) { + leaves[i] = keccak256(abi.encodePacked(address(this), bytes32(tokenId2))); + } else { + leaves[i] = keccak256(abi.encodePacked(allowlist[i / 2], bytes32(i % 2 == 0 ? tokenId1 : tokenId2))); + } + } + details.merkleRoot = getRoot(leaves); + bytes32[][] memory proofs = new bytes32[][](2); + proofs[0] = getProof(leaves, leafIndex1); + proofs[1] = getProof(leaves, leafIndex2); - bytes memory err = abi.encodeWithSelector(InsufficientPayment.selector, address(0), cost, cost - 1); + amount1 = bound(amount1, 1, details.supply); + amount2 = bound(amount2, 1, details.supply); + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = tokenId1; + tokenIds[1] = tokenId2; + uint256[] memory amounts = new uint256[](2); + amounts[0] = amount1; + amounts[1] = amount2; + uint256[] memory saleIndexes = new uint256[](2); + saleIndexes[0] = sale.addSaleDetails(details); + saleIndexes[1] = saleIndexes[0]; - vm.expectRevert(err); - sale.mint{ value: cost }(mintTo, tokenIds, amounts, "", address(0), cost - 1, TestHelper.blankProof()); + uint256 expectedCost = details.cost * (amount1 + amount2); + vm.deal(address(this), expectedCost); - sale.setTokenSaleDetails( - tokenId, perTokenCost, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), "" + vm.expectEmit(true, true, true, true, address(token)); + emit TransferBatch(address(sale), address(0), recipient, tokenIds, amounts); + vm.expectEmit(true, true, true, true, address(sale)); + emit IERC1155Sale.ItemsMinted(recipient, tokenIds, amounts, saleIndexes); + sale.mint{ value: expectedCost }( + recipient, tokenIds, amounts, "", saleIndexes, address(0), expectedCost, proofs ); - vm.expectRevert(err); - sale.mint{ value: cost }(mintTo, tokenIds, amounts, "", address(0), cost - 1, TestHelper.blankProof()); - sale.setPaymentToken(address(erc20)); - sale.setGlobalSaleDetails( - perTokenCost, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), "" - ); - vm.expectRevert(err); - sale.mint(mintTo, tokenIds, amounts, "", address(erc20), cost - 1, TestHelper.blankProof()); + assertEq(address(sale).balance, expectedCost); } - // Minting fails with invalid payment token. - function testMintFailWrongPaymentToken( + function test_mint_multiple_success_ERC20( bool useFactory, - address mintTo, - uint256 tokenId, - uint256 amount, - address wrongToken - ) public withFactory(useFactory) withERC20 { - (tokenId, amount) = assumeSafe(mintTo, tokenId, amount); - address paymentToken = wrongToken == address(0) ? address(erc20) : address(0); - sale.setPaymentToken(paymentToken); - sale.setGlobalSaleDetails(0, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), ""); - - bytes memory err = abi.encodeWithSelector(InsufficientPayment.selector, paymentToken, 0, 0); - vm.expectRevert(err); - sale.mint( - mintTo, - TestHelper.singleToArray(tokenId), - TestHelper.singleToArray(amount), - "", - wrongToken, - 0, - TestHelper.blankProof() - ); + address recipient, + IERC1155Sale.SaleDetails memory details1, + IERC1155Sale.SaleDetails memory details2, + uint256 tokenId1, + uint256 tokenId2, + uint256 amount1, + uint256 amount2 + ) public withFactory(useFactory) { + assumeSafeAddress(recipient); + details1 = validSaleDetails(tokenId1, details1); + details2 = validSaleDetails(tokenId2, details2); - sale.setTokenSaleDetails( - tokenId, 0, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), "" - ); + details1.paymentToken = address(erc20); + details2.paymentToken = address(erc20); - vm.expectRevert(err); - sale.mint( - mintTo, - TestHelper.singleToArray(tokenId), - TestHelper.singleToArray(amount), - "", - wrongToken, - 0, - TestHelper.blankProof() - ); - } + // Avoid overflows on total cost + details1.cost = details1.cost / 2 + 1; + details2.cost = details2.cost / 2 + 1; + details1.supply = details1.supply / 2 + 1; + details2.supply = details2.supply / 2 + 1; - // Minting fails with invalid payment token. - function testERC20MintFailPaidETH( - bool useFactory, - address mintTo, - uint256 tokenId, - uint256 amount - ) public withFactory(useFactory) withERC20 { - (tokenId, amount) = assumeSafe(mintTo, tokenId, amount); - sale.setPaymentToken(address(erc20)); - sale.setGlobalSaleDetails(0, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), ""); - - bytes memory err = abi.encodeWithSelector(InsufficientPayment.selector, address(0), 0, 1); - vm.expectRevert(err); - sale.mint{ value: 1 }( - mintTo, - TestHelper.singleToArray(tokenId), - TestHelper.singleToArray(amount), - "", - address(erc20), - 0, - TestHelper.blankProof() - ); + amount1 = bound(amount1, 1, details1.supply); + amount2 = bound(amount2, 1, details2.supply); - sale.setTokenSaleDetails( - tokenId, 0, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), "" - ); + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = tokenId1; + tokenIds[1] = tokenId2; + uint256[] memory amounts = new uint256[](2); + amounts[0] = amount1; + amounts[1] = amount2; + bytes32[][] memory proofs = new bytes32[][](2); + uint256[] memory saleIndexes = new uint256[](2); - vm.expectRevert(err); - sale.mint{ value: 1 }( - mintTo, - TestHelper.singleToArray(tokenId), - TestHelper.singleToArray(amount), - "", - address(erc20), - 0, - TestHelper.blankProof() - ); + saleIndexes[0] = sale.addSaleDetails(details1); + saleIndexes[1] = sale.addSaleDetails(details2); + + uint256 totalCost = (details1.cost * amount1) + (details2.cost * amount2); + erc20.mint(address(this), totalCost); + erc20.approve(address(sale), totalCost); + + vm.expectEmit(true, true, true, true, address(token)); + emit TransferBatch(address(sale), address(0), recipient, tokenIds, amounts); + vm.expectEmit(true, true, true, true, address(sale)); + emit IERC1155Sale.ItemsMinted(recipient, tokenIds, amounts, saleIndexes); + sale.mint(recipient, tokenIds, amounts, "", saleIndexes, details1.paymentToken, totalCost, proofs); + + assertEq(erc20.balanceOf(address(this)), 0); + assertEq(erc20.balanceOf(address(sale)), totalCost); } // @@ -692,39 +810,19 @@ contract ERC1155SaleMintTest is TestHelper, IERC1155SaleSignals, IERC1155SupplyS _; } - function assumeSafe( - address nonContract, - uint256 tokenId, - uint256 amount - ) private view returns (uint256 boundTokenId, uint256 boundAmount) { - assumeSafeAddress(nonContract); - vm.assume(nonContract != proxyOwner); - tokenId = bound(tokenId, 0, 100); - amount = bound(amount, 1, 19); - return (tokenId, amount); - } - - // Create ERC20. Give this contract 1000 ERC20 tokens. Approve token to spend 100 ERC20 tokens. - modifier withERC20() { - erc20 = new ERC20Mock(address(this)); - erc20.mint(address(this), 1000 ether); - erc20.approve(address(sale), 1000 ether); - _; - } - - modifier withGlobalSaleActive() { - sale.setGlobalSaleDetails( - perTokenCost, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), "" - ); - _; - } - - function setTokenSaleActive( - uint256 tokenId - ) private { - sale.setTokenSaleDetails( - tokenId, perTokenCost, type(uint256).max, uint64(block.timestamp - 1), uint64(block.timestamp + 1), "" - ); + function validSaleDetails( + uint256 validTokenId, + IERC1155Sale.SaleDetails memory saleDetails + ) public view returns (IERC1155Sale.SaleDetails memory) { + saleDetails.minTokenId = bound(saleDetails.minTokenId, 0, validTokenId); + saleDetails.maxTokenId = bound(saleDetails.maxTokenId, validTokenId, type(uint256).max); + saleDetails.supply = bound(saleDetails.supply, 1, type(uint256).max); + saleDetails.cost = bound(saleDetails.cost, 0, type(uint256).max / saleDetails.supply); + saleDetails.startTime = uint64(bound(saleDetails.startTime, 0, block.timestamp)); + saleDetails.endTime = uint64(bound(saleDetails.endTime, block.timestamp, type(uint64).max)); + saleDetails.paymentToken = address(0); + saleDetails.merkleRoot = bytes32(0); + return saleDetails; } }