diff --git a/contracts/erc721/ERC721Editions.sol b/contracts/erc721/ERC721Editions.sol index 83fad2f..52a252d 100644 --- a/contracts/erc721/ERC721Editions.sol +++ b/contracts/erc721/ERC721Editions.sol @@ -15,6 +15,7 @@ import "./interfaces/IERC721EditionMint.sol"; import "./MarketplaceFilterer/MarketplaceFiltererAbridged.sol"; import "../utils/ERC721/ERC721Upgradeable.sol"; import "../mint/interfaces/IAbridgedMintVector.sol"; +import "../mint/mechanics/interfaces/IMechanicMintManager.sol"; /** * @title ERC721 Editions @@ -205,6 +206,47 @@ contract ERC721Editions is return editionId; } + /** + * @notice Create edition with a mechanic vector + * @param _editionInfo Info of the Edition + * @param _editionSize Size of the Edition + * @param _editionTokenManager Edition's token manager + * @param editionRoyalty Edition royalty object for contract (optional) + * @param mechanicVectorData Mechanic mint vector data + * @ param mechanicVectorId Global mechanic vector ID + * @ param mechanic Mechanic address + * @ param mintManager Mint manager address + * @ param vectorData Vector data + * @notice Used to create a new Edition within the Collection + */ + function createEditionWithMechanicVector( + bytes memory _editionInfo, + uint256 _editionSize, + address _editionTokenManager, + IRoyaltyManager.Royalty memory editionRoyalty, + bytes calldata mechanicVectorData + ) external onlyOwner nonReentrant returns (uint256) { + uint256 editionId = _createEdition(_editionInfo, _editionSize, _editionTokenManager); + if (editionRoyalty.recipientAddress != address(0)) { + _royalties[editionId] = editionRoyalty; + } + + if (mechanicVectorData.length != 0) { + (uint96 seed, address mechanic, address mintManager, bytes memory vectorData) = abi.decode( + mechanicVectorData, + (uint96, address, address, bytes) + ); + + IMechanicMintManager(mintManager).registerMechanicVector( + IMechanicData.MechanicVectorMetadata(address(this), uint96(editionId), mechanic, true, false, false), + seed, + vectorData + ); + } + + return editionId; + } + /** * @notice Create edition with auction * @param _editionInfo Info of the Edition diff --git a/contracts/erc721/ERC721EditionsDFS.sol b/contracts/erc721/ERC721EditionsDFS.sol index 7957adf..362b8f6 100644 --- a/contracts/erc721/ERC721EditionsDFS.sol +++ b/contracts/erc721/ERC721EditionsDFS.sol @@ -16,6 +16,7 @@ import "./interfaces/IERC721EditionMint.sol"; import "./MarketplaceFilterer/MarketplaceFiltererAbridged.sol"; import "../utils/ERC721/ERC721Upgradeable.sol"; import "../mint/interfaces/IAbridgedMintVector.sol"; +import "../mint/mechanics/interfaces/IMechanicMintManager.sol"; /** * @title ERC721 Editions @@ -200,6 +201,46 @@ contract ERC721EditionsDFS is return editionId; } + /** + * @notice Used to create a new Edition within the Collection + * @param _editionUri Edition uri (metadata) + * @param _editionSize Size of the Edition + * @param _editionTokenManager Edition's token manager + * @param editionRoyalty Edition royalty object for contract (optional) + * @param mechanicVectorData Mechanic mint vector data + * @ param mechanicVectorId Global mechanic vector ID + * @ param mechanic Mechanic address + * @ param mintManager Mint manager address + * @ param vectorData Vector data + */ + function createEditionWithMechanicVector( + string memory _editionUri, + uint256 _editionSize, + address _editionTokenManager, + IRoyaltyManager.Royalty memory editionRoyalty, + bytes calldata mechanicVectorData + ) external onlyOwner nonReentrant returns (uint256) { + uint256 editionId = _createEdition(_editionUri, _editionSize, _editionTokenManager); + if (editionRoyalty.recipientAddress != address(0)) { + _royalties[editionId] = editionRoyalty; + } + + if (mechanicVectorData.length > 0) { + (uint96 seed, address mechanic, address mintManager, bytes memory vectorData) = abi.decode( + mechanicVectorData, + (uint96, address, address, bytes) + ); + + IMechanicMintManager(mintManager).registerMechanicVector( + IMechanicData.MechanicVectorMetadata(address(this), uint96(editionId), mechanic, true, false, false), + seed, + vectorData + ); + } + + return editionId; + } + /** * @notice Create edition with auction * @param _editionUri Edition uri (metadata) diff --git a/contracts/erc721/ERC721GeneralSequenceBase.sol b/contracts/erc721/ERC721GeneralSequenceBase.sol index 4d030fd..25a4b5f 100644 --- a/contracts/erc721/ERC721GeneralSequenceBase.sol +++ b/contracts/erc721/ERC721GeneralSequenceBase.sol @@ -5,15 +5,16 @@ import "./ERC721Base.sol"; import "../metadata/MetadataEncryption.sol"; import "../tokenManager/interfaces/IPostTransfer.sol"; import "../tokenManager/interfaces/IPostBurn.sol"; -import "./interfaces/IERC721GeneralMint.sol"; +import "./interfaces/IERC721GeneralSequenceMint.sol"; import "./erc721a/ERC721AURIStorageUpgradeable.sol"; +import "./custom/interfaces/IHighlightRenderer.sol"; /** * @title Generalized Base ERC721 * @author highlight.xyz * @notice Generalized Base NFT smart contract */ -abstract contract ERC721GeneralSequenceBase is ERC721Base, ERC721AURIStorageUpgradeable, IERC721GeneralMint { +abstract contract ERC721GeneralSequenceBase is ERC721Base, ERC721AURIStorageUpgradeable, IERC721GeneralSequenceMint { using EnumerableSet for EnumerableSet.AddressSet; /** @@ -41,6 +42,16 @@ abstract contract ERC721GeneralSequenceBase is ERC721Base, ERC721AURIStorageUpgr */ error EmptyString(); + /** + * @notice Custom renderer config, used for collections where metadata is rendered "in-chain" + * @param renderer Renderer address + * @param processMintDataOnRenderer If true, process mint data on renderer + */ + struct CustomRendererConfig { + address renderer; + bool processMintDataOnRenderer; + } + /** * @notice Contract metadata */ @@ -51,6 +62,11 @@ abstract contract ERC721GeneralSequenceBase is ERC721Base, ERC721AURIStorageUpgr */ uint256 public limitSupply; + /** + * @notice Custom renderer config + */ + CustomRendererConfig public customRendererConfig; + /** * @notice Emitted when uris are set for tokens * @param ids IDs of tokens to set uris for @@ -67,7 +83,7 @@ abstract contract ERC721GeneralSequenceBase is ERC721Base, ERC721AURIStorageUpgr /** * @notice See {IERC721GeneralMint-mintOneToOneRecipient} */ - function mintOneToOneRecipient(address recipient) external onlyMinter nonReentrant returns (uint256) { + function mintOneToOneRecipient(address recipient) external virtual onlyMinter nonReentrant returns (uint256) { if (_mintFrozen == 1) { _revert(MintFrozen.selector); } @@ -77,13 +93,19 @@ abstract contract ERC721GeneralSequenceBase is ERC721Base, ERC721AURIStorageUpgr _mint(recipient, 1); + // process mint on custom renderer if present + CustomRendererConfig memory _customRendererConfig = customRendererConfig; + if (_customRendererConfig.processMintDataOnRenderer) { + IHighlightRenderer(_customRendererConfig.renderer).processOneRecipientMint(tempSupply, 1, recipient); + } + return tempSupply; } /** * @notice See {IERC721GeneralMint-mintAmountToOneRecipient} */ - function mintAmountToOneRecipient(address recipient, uint256 amount) external onlyMinter nonReentrant { + function mintAmountToOneRecipient(address recipient, uint256 amount) external virtual onlyMinter nonReentrant { if (_mintFrozen == 1) { _revert(MintFrozen.selector); } @@ -92,6 +114,16 @@ abstract contract ERC721GeneralSequenceBase is ERC721Base, ERC721AURIStorageUpgr _mint(recipient, amount); _requireLimitSupply(tempSupply + amount); + + // process mint on custom renderer if present + CustomRendererConfig memory _customRendererConfig = customRendererConfig; + if (_customRendererConfig.processMintDataOnRenderer) { + IHighlightRenderer(_customRendererConfig.renderer).processOneRecipientMint( + tempSupply + 1, + amount, + recipient + ); + } } /** @@ -109,6 +141,16 @@ abstract contract ERC721GeneralSequenceBase is ERC721Base, ERC721AURIStorageUpgr } _requireLimitSupply(tempSupply + recipientsLength); + + // process mint on custom renderer if present + CustomRendererConfig memory _customRendererConfig = customRendererConfig; + if (_customRendererConfig.processMintDataOnRenderer) { + IHighlightRenderer(_customRendererConfig.renderer).processMultipleRecipientMint( + tempSupply + 1, + 1, + recipients + ); + } } /** @@ -129,23 +171,26 @@ abstract contract ERC721GeneralSequenceBase is ERC721Base, ERC721AURIStorageUpgr } _requireLimitSupply(tempSupply + recipientsLength * amount); - } - - /* solhint-disable no-empty-blocks */ - /** - * @notice Mint a chosen token id to a single recipient - * @dev Unavailable for ERC721GeneralSequenceBase, keep interface adhered for backwards compatiblity - */ - function mintSpecificTokenToOneRecipient(address recipient, uint256 tokenId) external {} + // process mint on custom renderer if present + CustomRendererConfig memory _customRendererConfig = customRendererConfig; + if (_customRendererConfig.processMintDataOnRenderer) { + IHighlightRenderer(_customRendererConfig.renderer).processMultipleRecipientMint( + tempSupply + 1, + amount, + recipients + ); + } + } /** - * @notice Mint chosen token ids to a single recipient - * @dev Unavailable for ERC721GeneralSequenceBase, keep interface adhered for backwards compatiblity + * @notice Set custom renderer and processing config + * @param _customRendererConfig New custom renderer config */ - function mintSpecificTokensToOneRecipient(address recipient, uint256[] calldata tokenIds) external {} - - /* solhint-enable no-empty-blocks */ + function setCustomRenderer(CustomRendererConfig calldata _customRendererConfig) external onlyOwner { + require(_customRendererConfig.renderer != address(0), "Invalid input"); + customRendererConfig = _customRendererConfig; + } /** * @notice Override base URI system for select tokens, with custom per-token metadata @@ -220,6 +265,13 @@ abstract contract ERC721GeneralSequenceBase is ERC721Base, ERC721AURIStorageUpgr observability.emitContractMetadataSet(newName, newSymbol, newContractUri); } + /** + * @notice Return the total number of minted tokens on the collection + */ + function supply() external view returns (uint256) { + return ERC721AUpgradeable._totalMinted(); + } + /** * @notice See {IERC721-burn}. Overrides default behaviour to check associated tokenManager. */ @@ -247,6 +299,9 @@ abstract contract ERC721GeneralSequenceBase is ERC721Base, ERC721AURIStorageUpgr * @param tokenId ID of token to get uri for */ function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + if (customRendererConfig.renderer != address(0)) { + return IHighlightRenderer(customRendererConfig.renderer).tokenURI(tokenId); + } return ERC721AURIStorageUpgradeable.tokenURI(tokenId); } @@ -322,7 +377,7 @@ abstract contract ERC721GeneralSequenceBase is ERC721Base, ERC721AURIStorageUpgr * @notice Require the new supply of tokens after mint to be less than limit supply * @param newSupply New supply */ - function _requireLimitSupply(uint256 newSupply) private view { + function _requireLimitSupply(uint256 newSupply) internal view { uint256 _limitSupply = limitSupply; if (_limitSupply != 0 && newSupply > _limitSupply) { _revert(OverLimitSupply.selector); diff --git a/contracts/erc721/custom/BitRot/BitRotGenerative.sol b/contracts/erc721/custom/BitRot/BitRotGenerative.sol new file mode 100644 index 0000000..874dcde --- /dev/null +++ b/contracts/erc721/custom/BitRot/BitRotGenerative.sol @@ -0,0 +1,154 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +import "./interfaces/IBitRotRenderer.sol"; +import "../../onchain/ERC721GenerativeOnchain.sol"; + +/** + * @title BitRotGenerative (ERC721) + */ +contract BitRotGenerative is ERC721GenerativeOnchain { + /** + * @notice Throw when mint details are queried for a token that hasn't been minted + */ + error InvalidTokenId(); + + /** + * @notice Data partially used to seed outputs + */ + struct SeedDetails { + bytes32 previousBlockHash; + uint256 blockTimestamp; + } + + /** + * @notice BitRotRenderer address + */ + IBitRotRenderer public renderer; + + /** + * @notice Store the block hash for every minted token batch + */ + mapping(uint256 => SeedDetails) private _startTokenIdToSeedDetails; + + /** + * @notice Store the first token id of each minted batch + */ + uint256[] private _startTokenIds; + + /** + * @notice Store the image previews base uri + */ + string private _previewsBaseUri; + + /** + * @notice Emit when BitRotRenderer is updated + */ + event RendererUpdated(address indexed newRenderer); + + /* solhint-disable not-rely-on-block-hash */ + /** + * @notice See {IERC721GeneralMint-mintOneToOneRecipient} + * @dev Update BitRot mint details + */ + function mintOneToOneRecipient(address recipient) external override onlyMinter nonReentrant returns (uint256) { + if (_mintFrozen == 1) { + _revert(MintFrozen.selector); + } + + uint256 tempSupply = _nextTokenId(); + _requireLimitSupply(tempSupply); + + _mint(recipient, 1); + + _startTokenIds.push(tempSupply); + _startTokenIdToSeedDetails[tempSupply] = SeedDetails(blockhash(block.number - 1), block.timestamp); + + return tempSupply; + } + + /* solhint-enable not-rely-on-block-hash */ + + /** + * @notice See {IERC721GeneralMint-mintAmountToOneRecipient} + * @dev Update BitRot mint details + */ + function mintAmountToOneRecipient(address recipient, uint256 amount) external override onlyMinter nonReentrant { + if (_mintFrozen == 1) { + _revert(MintFrozen.selector); + } + uint256 tempSupply = _nextTokenId() - 1; // cache + + _mint(recipient, amount); + + _requireLimitSupply(tempSupply + amount); + + _startTokenIds.push(tempSupply + 1); + _startTokenIdToSeedDetails[tempSupply + 1] = SeedDetails(blockhash(block.number - 1), block.timestamp); + } + + /** + * @notice Update BitRot renderer + */ + function updateRenderer(address newRenderer) external onlyOwner { + renderer = IBitRotRenderer(newRenderer); + + emit RendererUpdated(newRenderer); + } + + /** + * @notice Update previews base uri + */ + function updatePreviewsBaseUri(string memory newPreviewsBaseUri) external onlyOwner { + _previewsBaseUri = newPreviewsBaseUri; + } + + /** + * @notice Override tokenURI to use BitRotRenderer + */ + function tokenURI(uint256 tokenId) public view override returns (string memory) { + SeedDetails memory seedDetails = getSeedDetails(tokenId); + + return + renderer.tokenURI( + seedDetails.previousBlockHash, + tokenId, + seedDetails.blockTimestamp, + address(this), + _previewsBaseUri + ); + } + + function getSeedDetails(uint256 tokenId) public view returns (SeedDetails memory) { + uint256 nextTokenId = _nextTokenId(); + uint256[] memory tempStartTokenIds = _startTokenIds; + uint256 numBatches = tempStartTokenIds.length; + + if (numBatches == 0) { + _revert(InvalidTokenId.selector); + } + + uint256 previousStartTokenId = tempStartTokenIds[0]; + if (numBatches == 1) { + if (tokenId >= previousStartTokenId && tokenId < nextTokenId) { + return _startTokenIdToSeedDetails[previousStartTokenId]; + } else { + _revert(InvalidTokenId.selector); + } + } + + for (uint256 i = 1; i < numBatches; i++) { + if (tokenId >= previousStartTokenId && tokenId < tempStartTokenIds[i]) { + return _startTokenIdToSeedDetails[previousStartTokenId]; + } + + previousStartTokenId = tempStartTokenIds[i]; + } + + if (tokenId >= previousStartTokenId && tokenId < nextTokenId) { + return _startTokenIdToSeedDetails[previousStartTokenId]; + } else { + _revert(InvalidTokenId.selector); + } + } +} diff --git a/contracts/erc721/custom/BitRot/SplitFunds.sol b/contracts/erc721/custom/BitRot/SplitFunds.sol new file mode 100644 index 0000000..1310737 --- /dev/null +++ b/contracts/erc721/custom/BitRot/SplitFunds.sol @@ -0,0 +1,18 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +contract SplitFunds { + function split(address payable[] calldata recipients, uint256[] calldata amounts) external payable { + uint256 recipientsLength = recipients.length; + require(recipientsLength == amounts.length, "ii"); + + uint256 totalAmount = 0; + + for (uint256 i = 0; i < recipientsLength; i++) { + (bool sentToRecipient, bytes memory data) = recipients[i].call{ value: amounts[i] }(""); + totalAmount += amounts[i]; + require(sentToRecipient, "fs"); + } + require(totalAmount == msg.value, "lc"); + } +} diff --git a/contracts/erc721/custom/BitRot/interfaces/IBitRotRenderer.sol b/contracts/erc721/custom/BitRot/interfaces/IBitRotRenderer.sol new file mode 100644 index 0000000..036479a --- /dev/null +++ b/contracts/erc721/custom/BitRot/interfaces/IBitRotRenderer.sol @@ -0,0 +1,15 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +/** + * @notice BitRotRenderer interface as it pertains to BitRotGenerative + */ +interface IBitRotRenderer { + function tokenURI( + bytes32 blockHash, + uint256 tokenId, + uint256 timestamp, + address storageContract, + string memory previewsBaseUri + ) external view returns (string memory); +} diff --git a/contracts/erc721/custom/BitRot/test/BitRotGenerativeTest.sol b/contracts/erc721/custom/BitRot/test/BitRotGenerativeTest.sol new file mode 100644 index 0000000..dcab989 --- /dev/null +++ b/contracts/erc721/custom/BitRot/test/BitRotGenerativeTest.sol @@ -0,0 +1,74 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +import "../BitRotGenerative.sol"; + +contract BitRotGenerativeTest { + BitRotGenerative public bitRot; + + constructor(address bitRotAddress) { + bitRot = BitRotGenerative(bitRotAddress); + } + + /** + * @dev This contract needs to be a valid minter on BitRotGenerative + */ + function test() external { + // updating renderer should fail, only owner can do it + bool updateFailed = false; + try bitRot.updateRenderer(address(0)) {} catch { + updateFailed = true; + } + if (!updateFailed) { + revert("Updating renderer worked"); + } + + // test seed details recording + _getSeedDetailsShouldFail(1); + + bitRot.mintAmountToOneRecipient(msg.sender, 2); + _validateTokenRange(1, 2); + + bitRot.mintOneToOneRecipient(msg.sender); + _validateTokenRange(3, 1); + _validateTokenRange(1, 3); + _getSeedDetailsShouldFail(4); + + bitRot.mintAmountToOneRecipient(msg.sender, 12); + _validateTokenRange(4, 12); + _validateTokenRange(1, 15); + _getSeedDetailsShouldFail(16); + + bitRot.mintAmountToOneRecipient(msg.sender, 4); + _validateTokenRange(16, 4); + _validateTokenRange(1, 19); + _getSeedDetailsShouldFail(20); + + bitRot.mintOneToOneRecipient(msg.sender); + _validateTokenRange(20, 1); + _validateTokenRange(1, 20); + _getSeedDetailsShouldFail(21); + } + + function _validateTokenRange(uint256 start, uint256 num) private { + for (uint256 i = start; i < start + num; i++) { + BitRotGenerative.SeedDetails memory seedDetails = bitRot.getSeedDetails(i); + if ( + seedDetails.previousBlockHash != blockhash(block.number - 1) || + seedDetails.blockTimestamp != block.timestamp + ) { + revert("Failed seed recording"); + } + } + } + + function _getSeedDetailsShouldFail(uint256 tokenId) private { + bool getSeedFailed = false; + try bitRot.getSeedDetails(tokenId) {} catch { + getSeedFailed = true; + } + if (!getSeedFailed) { + revert("Getting seed failed"); + } + } +} diff --git a/contracts/erc721/custom/interfaces/IHighlightRenderer.sol b/contracts/erc721/custom/interfaces/IHighlightRenderer.sol new file mode 100644 index 0000000..aae2b7b --- /dev/null +++ b/contracts/erc721/custom/interfaces/IHighlightRenderer.sol @@ -0,0 +1,36 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +/** + * @notice Highlight's custom renderer interface for collections + */ +interface IHighlightRenderer { + /** + * @notice Process a mint to multiple recipients (likely store mint details) + * @dev Implementations should assume msg.sender to be the NFT contract + * @param firstTokenId ID of first token to be minted (next ones are minted sequentially) + * @param numTokensPerRecipient Number of tokens minted to each recipient + * @param orderedRecipients Recipients to mint tokens to, sequentially + */ + function processMultipleRecipientMint( + uint256 firstTokenId, + uint256 numTokensPerRecipient, + address[] calldata orderedRecipients + ) external; + + /** + * @notice Process a mint to one recipient (likely store mint details) + * @dev Implementations should assume msg.sender to be the NFT contract + * @param firstTokenId ID of first token to be minted (next ones are minted sequentially) + * @param numTokens Number of tokens minted + * @param recipient Recipient to mint to + */ + function processOneRecipientMint(uint256 firstTokenId, uint256 numTokens, address recipient) external; + + /** + * @notice Return token metadata for a token + * @dev Implementations should assume msg.sender to be the NFT contract + * @param tokenId ID of token to return metadata for + */ + function tokenURI(uint256 tokenId) external view returns (string memory); +} diff --git a/contracts/erc721/custom/test/TestHighlightRenderer.sol b/contracts/erc721/custom/test/TestHighlightRenderer.sol new file mode 100644 index 0000000..4ea4127 --- /dev/null +++ b/contracts/erc721/custom/test/TestHighlightRenderer.sol @@ -0,0 +1,136 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +import "../interfaces/IHighlightRenderer.sol"; +import "../../interfaces/IERC721GeneralSupplyMetadata.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + +/** + * @notice Mock implementation of IHighlightRenderer + */ +contract TestHighlightRenderer is IHighlightRenderer { + /** + * @notice Throw when mint details are queried for a token that hasn't been minted + */ + error InvalidTokenId(); + + /** + * @notice Details that seed token metadata + */ + struct SeedDetails { + bytes32 previousBlockHash; + uint256 blockTimestamp; + // etc. + } + + /** + * @notice Store the seed details for each token batch (for each nft contract) + */ + mapping(address => mapping(uint256 => SeedDetails)) private _startTokenIdToSeedDetails; + + /** + * @notice Store the first token id of each minted batch (for each nft contract) + */ + mapping(address => uint256[]) private _startTokenIds; + + /** + * @notice See {IHighlightRenderer-processMultipleRecipientMint} + */ + function processMultipleRecipientMint( + uint256 firstTokenId, + uint256 numTokensPerRecipient, // unused in this implementation + address[] calldata orderedRecipients // unused in this implementation + ) external { + _startTokenIdToSeedDetails[msg.sender][firstTokenId] = SeedDetails( + blockhash(block.number - 1), + block.timestamp + ); + _startTokenIds[msg.sender].push(firstTokenId); + } + + /** + * @notice See {IHighlightRenderer-processOneRecipientMint} + */ + function processOneRecipientMint( + uint256 firstTokenId, + uint256 numTokens, // unused in this implementation + address recipient // unused in this implementation + ) external { + _startTokenIdToSeedDetails[msg.sender][firstTokenId] = SeedDetails( + blockhash(block.number - 1), + block.timestamp + ); + _startTokenIds[msg.sender].push(firstTokenId); + } + + /** + * @notice See {IHighlightRenderer-tokenURI} + */ + function tokenURI(uint256 tokenId) external view returns (string memory) { + // typically return a base64-encoded json + // probably store a preview images base uri globally (stored via Highlight) + // for demonstration purposes, just return a simple string here: + uint256 numTokens = IERC721GeneralSupplyMetadata(msg.sender).supply(); + return concatenateSeedDetails(getSeedDetails(tokenId, numTokens + 1, msg.sender), tokenId); + } + + /** + * @notice Concatenate seed details into a fake uri + */ + function concatenateSeedDetails( + SeedDetails memory _seedDetails, + uint256 tokenId + ) public view returns (string memory) { + return + string( + abi.encodePacked( + Strings.toString(uint256(_seedDetails.previousBlockHash)), + Strings.toString(_seedDetails.blockTimestamp), + Strings.toString(tokenId) + ) + ); + } + + /** + * @notice Get a token's seed details + * @dev Assumes _startTokenIds are in ascending order + * @param tokenId ID of token to get seed details for + * @param nextTokenId ID of immediate token that hasn't been minted on NFT contract + * @param nftContract NFT contract + */ + function getSeedDetails( + uint256 tokenId, + uint256 nextTokenId, + address nftContract + ) public view returns (SeedDetails memory) { + uint256[] memory tempStartTokenIds = _startTokenIds[nftContract]; + uint256 numBatches = tempStartTokenIds.length; + + if (numBatches == 0) { + revert InvalidTokenId(); + } + + uint256 previousStartTokenId = tempStartTokenIds[0]; + if (numBatches == 1) { + if (tokenId >= previousStartTokenId && tokenId < nextTokenId) { + return _startTokenIdToSeedDetails[nftContract][previousStartTokenId]; + } else { + revert InvalidTokenId(); + } + } + + for (uint256 i = 1; i < numBatches; i++) { + if (tokenId >= previousStartTokenId && tokenId < tempStartTokenIds[i]) { + return _startTokenIdToSeedDetails[nftContract][previousStartTokenId]; + } + + previousStartTokenId = tempStartTokenIds[i]; + } + + if (tokenId >= previousStartTokenId && tokenId < nextTokenId) { + return _startTokenIdToSeedDetails[nftContract][previousStartTokenId]; + } else { + revert InvalidTokenId(); + } + } +} diff --git a/contracts/erc721/instances/GenerativeSeries.sol b/contracts/erc721/instances/GenerativeSeries.sol index cab5bb6..d939062 100644 --- a/contracts/erc721/instances/GenerativeSeries.sol +++ b/contracts/erc721/instances/GenerativeSeries.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.10; import "../../mint/interfaces/IAbridgedMintVector.sol"; +import "../../mint/mechanics/interfaces/IMechanicMintManager.sol"; import "@openzeppelin/contracts/proxy/Proxy.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/utils/StorageSlot.sol"; @@ -37,12 +38,18 @@ contract GenerativeSeries is Proxy { * @ param maxTotalClaimableViaVector * @ param maxUserClaimableViaVector * @ param allowlistRoot + * @param mechanicVectorData Mechanic mint vector data + * @ param mechanicVectorId Global mechanic vector ID + * @ param mechanic Mechanic address + * @ param mintManager Mint manager address + * @ param vectorData Vector data * @param _observability Observability contract address */ constructor( address implementation_, bytes memory initializeData, bytes memory mintVectorData, + bytes memory mechanicVectorData, address _observability ) { assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)); @@ -87,13 +94,26 @@ contract GenerativeSeries is Proxy { ) ); } + + if (mechanicVectorData.length != 0) { + (uint96 seed, address mechanic, address mintManager, bytes memory vectorData) = abi.decode( + mechanicVectorData, + (uint96, address, address, bytes) + ); + + IMechanicMintManager(mintManager).registerMechanicVector( + IMechanicData.MechanicVectorMetadata(address(this), 0, mechanic, false, false, false), + seed, + vectorData + ); + } } /** * @notice Return the contract type */ function standard() external pure returns (string memory) { - return "GenerativeSeries"; + return "GenerativeSeries2"; } /** diff --git a/contracts/erc721/instances/MultipleEditions.sol b/contracts/erc721/instances/MultipleEditions.sol index dafb917..ac53b49 100644 --- a/contracts/erc721/instances/MultipleEditions.sol +++ b/contracts/erc721/instances/MultipleEditions.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.10; import "../../auction/interfaces/IAuctionManager.sol"; import "../../royaltyManager/interfaces/IRoyaltyManager.sol"; import "../../mint/interfaces/IAbridgedMintVector.sol"; +import "../../mint/mechanics/interfaces/IMechanicMintManager.sol"; import "@openzeppelin/contracts/proxy/Proxy.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/utils/StorageSlot.sol"; @@ -47,6 +48,11 @@ contract MultipleEditions is Proxy { * @ param maxTotalClaimableViaVector * @ param maxUserClaimableViaVector * @ param allowlistRoot + * @param mechanicVectorData Mechanic mint vector data + * @ param mechanicVectorId Global mechanic vector ID + * @ param mechanic Mechanic address + * @ param mintManager Mint manager address + * @ param vectorData Vector data */ constructor( address implementation_, @@ -56,7 +62,8 @@ contract MultipleEditions is Proxy { address _editionTokenManager, IRoyaltyManager.Royalty memory editionRoyalty, bytes memory auctionData, - bytes memory mintVectorData + bytes memory mintVectorData, + bytes memory mechanicVectorData ) { assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)); StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = implementation_; @@ -64,17 +71,31 @@ contract MultipleEditions is Proxy { if (_editionInfo.length > 0) { // create edition - Address.functionDelegateCall( - implementation_, - abi.encodeWithSignature( - "createEdition(bytes,uint256,address,(address,uint16),bytes)", - _editionInfo, - editionSize, - _editionTokenManager, - editionRoyalty, - mintVectorData - ) - ); + if (mechanicVectorData.length > 0) { + Address.functionDelegateCall( + implementation_, + abi.encodeWithSignature( + "createEditionWithMechanicVector(bytes,uint256,address,(address,uint16),bytes)", + _editionInfo, + editionSize, + _editionTokenManager, + editionRoyalty, + mechanicVectorData + ) + ); + } else { + Address.functionDelegateCall( + implementation_, + abi.encodeWithSignature( + "createEdition(bytes,uint256,address,(address,uint16),bytes)", + _editionInfo, + editionSize, + _editionTokenManager, + editionRoyalty, + mintVectorData + ) + ); + } } if (auctionData.length > 0) { @@ -111,7 +132,7 @@ contract MultipleEditions is Proxy { * @notice Return the contract type */ function standard() external pure returns (string memory) { - return "MultipleEditions"; + return "MultipleEditions2"; } /** diff --git a/contracts/erc721/instances/MultipleEditionsDFS.sol b/contracts/erc721/instances/MultipleEditionsDFS.sol index a20ef5b..d39d46a 100644 --- a/contracts/erc721/instances/MultipleEditionsDFS.sol +++ b/contracts/erc721/instances/MultipleEditionsDFS.sol @@ -46,6 +46,11 @@ contract MultipleEditionsDFS is Proxy { * @ param maxTotalClaimableViaVector * @ param maxUserClaimableViaVector * @ param allowlistRoot + * @param mechanicVectorData Mechanic mint vector data + * @ param mechanicVectorId Global mechanic vector ID + * @ param mechanic Mechanic address + * @ param mintManager Mint manager address + * @ param vectorData Vector data */ constructor( address implementation_, @@ -55,7 +60,8 @@ contract MultipleEditionsDFS is Proxy { address _editionTokenManager, IRoyaltyManager.Royalty memory editionRoyalty, bytes memory auctionData, - bytes memory mintVectorData + bytes memory mintVectorData, + bytes memory mechanicVectorData ) { assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)); StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = implementation_; @@ -63,17 +69,31 @@ contract MultipleEditionsDFS is Proxy { // create edition if (bytes(_editionUri).length > 0) { - Address.functionDelegateCall( - implementation_, - abi.encodeWithSignature( - "createEdition(string,uint256,address,(address,uint16),bytes)", - _editionUri, - editionSize, - _editionTokenManager, - editionRoyalty, - mintVectorData - ) - ); + if (mechanicVectorData.length > 0) { + Address.functionDelegateCall( + implementation_, + abi.encodeWithSignature( + "createEditionWithMechanicVector(string,uint256,address,(address,uint16),bytes)", + _editionUri, + editionSize, + _editionTokenManager, + editionRoyalty, + mechanicVectorData + ) + ); + } else { + Address.functionDelegateCall( + implementation_, + abi.encodeWithSignature( + "createEdition(string,uint256,address,(address,uint16),bytes)", + _editionUri, + editionSize, + _editionTokenManager, + editionRoyalty, + mintVectorData + ) + ); + } } if (auctionData.length > 0) { @@ -110,7 +130,7 @@ contract MultipleEditionsDFS is Proxy { * @notice Return the contract type */ function standard() external pure returns (string memory) { - return "MultipleEditionsDFS"; + return "MultipleEditionsDFS2"; } /** diff --git a/contracts/erc721/instances/Series.sol b/contracts/erc721/instances/Series.sol index 2e0bab5..5efb784 100644 --- a/contracts/erc721/instances/Series.sol +++ b/contracts/erc721/instances/Series.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.10; import "../../mint/interfaces/IAbridgedMintVector.sol"; +import "../../mint/mechanics/interfaces/IMechanicMintManager.sol"; import "@openzeppelin/contracts/proxy/Proxy.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/utils/StorageSlot.sol"; @@ -37,8 +38,20 @@ contract Series is Proxy { * @ param maxTotalClaimableViaVector * @ param maxUserClaimableViaVector * @ param allowlistRoot + * @param mechanicVectorData Mechanic mint vector data + * @ param mechanicVectorId Global mechanic vector ID + * @ param mechanic Mechanic address + * @ param mintManager Mint manager address + * @ param vectorData Vector data + * @param isCollectorsChoice True if series will have collector's choice based minting */ - constructor(address implementation_, bytes memory initializeData, bytes memory mintVectorData) { + constructor( + address implementation_, + bytes memory initializeData, + bytes memory mintVectorData, + bytes memory mechanicVectorData, + bool isCollectorsChoice + ) { assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)); StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = implementation_; Address.functionDelegateCall(implementation_, abi.encodeWithSignature("initialize(bytes)", initializeData)); @@ -78,13 +91,26 @@ contract Series is Proxy { ) ); } + + if (mechanicVectorData.length != 0) { + (uint96 seed, address mechanic, address mintManager, bytes memory vectorData) = abi.decode( + mechanicVectorData, + (uint96, address, address, bytes) + ); + + IMechanicMintManager(mintManager).registerMechanicVector( + IMechanicData.MechanicVectorMetadata(address(this), 0, mechanic, false, isCollectorsChoice, false), + seed, + vectorData + ); + } } /** * @notice Return the contract type */ function standard() external pure returns (string memory) { - return "Series"; + return "Series2"; } /** diff --git a/contracts/erc721/instances/SingleEdition.sol b/contracts/erc721/instances/SingleEdition.sol index fe4b287..89e97b6 100644 --- a/contracts/erc721/instances/SingleEdition.sol +++ b/contracts/erc721/instances/SingleEdition.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.10; import "../../mint/interfaces/IAbridgedMintVector.sol"; +import "../../mint/mechanics/interfaces/IMechanicMintManager.sol"; import "@openzeppelin/contracts/proxy/Proxy.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/utils/StorageSlot.sol"; @@ -37,12 +38,18 @@ contract SingleEdition is Proxy { * @ param maxTotalClaimableViaVector * @ param maxUserClaimableViaVector * @ param allowlistRoot + * @param mechanicVectorData Mechanic mint vector data + * @ param mechanicVectorId Global mechanic vector ID + * @ param mechanic Mechanic address + * @ param mintManager Mint manager address + * @ param vectorData Vector data * @param _observability Observability contract address */ constructor( address implementation_, bytes memory initializeData, bytes memory mintVectorData, + bytes memory mechanicVectorData, address _observability ) { assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)); @@ -87,13 +94,26 @@ contract SingleEdition is Proxy { ) ); } + + if (mechanicVectorData.length != 0) { + (uint96 seed, address mechanic, address mintManager, bytes memory vectorData) = abi.decode( + mechanicVectorData, + (uint96, address, address, bytes) + ); + + IMechanicMintManager(mintManager).registerMechanicVector( + IMechanicData.MechanicVectorMetadata(address(this), 0, mechanic, true, false, false), + seed, + vectorData + ); + } } /** * @notice Return the contract type */ function standard() external pure returns (string memory) { - return "SingleEdition"; + return "SingleEdition2"; } /** diff --git a/contracts/erc721/instances/SingleEditionDFS.sol b/contracts/erc721/instances/SingleEditionDFS.sol index a8f6216..a79d353 100644 --- a/contracts/erc721/instances/SingleEditionDFS.sol +++ b/contracts/erc721/instances/SingleEditionDFS.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.10; import "../../mint/interfaces/IAbridgedMintVector.sol"; +import "../../mint/mechanics/interfaces/IMechanicMintManager.sol"; import "@openzeppelin/contracts/proxy/Proxy.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/utils/StorageSlot.sol"; @@ -37,12 +38,18 @@ contract SingleEditionDFS is Proxy { * @ param maxTotalClaimableViaVector * @ param maxUserClaimableViaVector * @ param allowlistRoot + * @param mechanicVectorData Mechanic mint vector data + * @ param mechanicVectorId Global mechanic vector ID + * @ param mechanic Mechanic address + * @ param mintManager Mint manager address + * @ param vectorData Vector data * @param _observability Observability contract address */ constructor( address implementation_, bytes memory initializeData, bytes memory mintVectorData, + bytes memory mechanicVectorData, address _observability ) { assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)); @@ -87,13 +94,26 @@ contract SingleEditionDFS is Proxy { ) ); } + + if (mechanicVectorData.length != 0) { + (uint96 seed, address mechanic, address mintManager, bytes memory vectorData) = abi.decode( + mechanicVectorData, + (uint96, address, address, bytes) + ); + + IMechanicMintManager(mintManager).registerMechanicVector( + IMechanicData.MechanicVectorMetadata(address(this), 0, mechanic, true, false, false), + seed, + vectorData + ); + } } /** * @notice Return the contract type */ function standard() external pure returns (string memory) { - return "SingleEditionDFS"; + return "SingleEditionDFS2"; } /** diff --git a/contracts/erc721/interfaces/IERC721GeneralSequenceMint.sol b/contracts/erc721/interfaces/IERC721GeneralSequenceMint.sol new file mode 100644 index 0000000..7c91884 --- /dev/null +++ b/contracts/erc721/interfaces/IERC721GeneralSequenceMint.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +/** + * @notice General721 mint interface for sequentially minted collections + * @author highlight.xyz + */ +interface IERC721GeneralSequenceMint { + /** + * @notice Mint one token to one recipient + * @param recipient Recipient of minted NFT + */ + function mintOneToOneRecipient(address recipient) external returns (uint256); + + /** + * @notice Mint an amount of tokens to one recipient + * @param recipient Recipient of minted NFTs + * @param amount Amount of NFTs minted + */ + function mintAmountToOneRecipient(address recipient, uint256 amount) external; + + /** + * @notice Mint one token to multiple recipients. Useful for use-cases like airdrops + * @param recipients Recipients of minted NFTs + */ + function mintOneToMultipleRecipients(address[] calldata recipients) external; + + /** + * @notice Mint the same amount of tokens to multiple recipients + * @param recipients Recipients of minted NFTs + * @param amount Amount of NFTs minted to each recipient + */ + function mintSameAmountToMultipleRecipients(address[] calldata recipients, uint256 amount) external; +} diff --git a/contracts/erc721/interfaces/IERC721GeneralSupplyMetadata.sol b/contracts/erc721/interfaces/IERC721GeneralSupplyMetadata.sol new file mode 100644 index 0000000..5f40aea --- /dev/null +++ b/contracts/erc721/interfaces/IERC721GeneralSupplyMetadata.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +/** + * @notice Get a Series based collection's supply metadata + * @author highlight.xyz + */ +interface IERC721GeneralSupplyMetadata { + /** + * @notice Get a series based collection's supply, burned tokens notwithstanding + */ + function supply() external view returns (uint256); + + /** + * @notice Get a series based collection's total supply + */ + function totalSupply() external view returns (uint256); + + /** + * @notice Get a series based collection's supply cap + */ + function limitSupply() external view returns (uint256); +} diff --git a/contracts/erc721/ocs/ERC721GenerativeOnchain.sol b/contracts/erc721/onchain/ERC721GenerativeOnchain.sol similarity index 100% rename from contracts/erc721/ocs/ERC721GenerativeOnchain.sol rename to contracts/erc721/onchain/ERC721GenerativeOnchain.sol diff --git a/contracts/erc721/ocs/FileDeployer.sol b/contracts/erc721/onchain/FileDeployer.sol similarity index 100% rename from contracts/erc721/ocs/FileDeployer.sol rename to contracts/erc721/onchain/FileDeployer.sol diff --git a/contracts/erc721/ocs/OnchainFileStorage.sol b/contracts/erc721/onchain/OnchainFileStorage.sol similarity index 100% rename from contracts/erc721/ocs/OnchainFileStorage.sol rename to contracts/erc721/onchain/OnchainFileStorage.sol diff --git a/contracts/mint/MintManager.sol b/contracts/mint/MintManager.sol index 8921e17..d0ce3b0 100644 --- a/contracts/mint/MintManager.sol +++ b/contracts/mint/MintManager.sol @@ -9,6 +9,8 @@ import "./interfaces/INativeMetaTransaction.sol"; import "../utils/EIP712Upgradeable.sol"; import "../metatx/ERC2771ContextUpgradeable.sol"; import "./interfaces/IAbridgedMintVector.sol"; +import "./mechanics/interfaces/IMechanicMintManager.sol"; +import "./mechanics/interfaces/IMechanic.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; @@ -27,7 +29,8 @@ contract MintManager is UUPSUpgradeable, OwnableUpgradeable, ERC2771ContextUpgradeable, - IAbridgedMintVector + IAbridgedMintVector, + IMechanicMintManager { using ECDSA for bytes32; using EnumerableSet for EnumerableSet.Bytes32Set; @@ -115,6 +118,21 @@ contract MintManager is */ error MintPaused(); + /** + * @notice Throw when an entity is already registered with a given ID + */ + error AlreadyRegisteredWithId(); + + /** + * @notice Throw when a mechanic is invalid + */ + error InvalidMechanic(); + + /** + * @notice Throw when a mechanic is paused + */ + error MechanicPaused(); + /** * @notice On-chain mint vector * @param contractAddress NFT smart contract address @@ -201,40 +219,6 @@ contract MintManager is bytes32 offchainVectorId; } - /** - * @notice Claim that is signed off-chain with EIP-712, and unwrapped to facilitate fulfillment of mint. - * Includes meta-tx packets to impersonate purchaser and make payments. - * @param currency Currency used for payment. Native gas token, if zero address - * @param contractAddress NFT smart contract address - * @param claimer Account able to use this claim - * @param paymentRecipient Payment recipient - * @param pricePerToken Price that has to be paid per minted token - * @param numTokensToMint Number of NFTs to mint in this transaction - * @param purchaseToCreatorPacket Meta-tx packet that send portion of payment to creator - * @param purchaseToPlatformPacket Meta-tx packet that send portion of payment to platform - * @param maxClaimableViaVector Max number of tokens that can be minted via vector - * @param maxClaimablePerUser Max number of tokens that can be minted by user via vector - * @param editionId ID of edition to mint on. Unused if claim is passed into ERC721General minting function - * @param claimExpiryTimestamp Time when claim expires - * @param claimNonce Unique identifier of claim - * @param offchainVectorId Unique identifier of vector offchain - */ - struct ClaimWithMetaTxPacket { - address currency; - address contractAddress; - address claimer; - uint256 pricePerToken; - uint64 numTokensToMint; - PurchaserMetaTxPacket purchaseToCreatorPacket; - PurchaserMetaTxPacket purchaseToPlatformPacket; - uint256 maxClaimableViaVector; - uint256 maxClaimablePerUser; - uint256 editionId; // unused if for general contract mints - uint256 claimExpiryTimestamp; - bytes32 claimNonce; - bytes32 offchainVectorId; - } - /** * @notice Claim that is signed off-chain with EIP-712, and unwrapped to facilitate fulfillment of mint on a Series * @dev Max number claimable per transaction is enforced off-chain @@ -368,6 +352,11 @@ contract MintManager is */ uint256 private constant _BITMASK_AV_PAUSED = 1; + /** + * @notice Global mechanic vector metadatas + */ + mapping(bytes32 => MechanicVectorMetadata) public mechanicVectorMetadata; + /** * @notice Emitted when platform executor is added or removed * @param executor Changed executor @@ -442,24 +431,6 @@ contract MintManager is uint32 percentageBPSOfTotal ); - /** - * @notice Emitted when payment is made in ERC20 via meta-tx packet method - * @param currency ERC20 currency - * @param msgSender Payer - * @param vectorId Vector that payment was for - * @param purchaseToCreatorPacket Meta-tx packet facilitating payment to creator - * @param purchaseToPlatformPacket Meta-tx packet facilitating payment to platform - * @param amount Payment amount - */ - event ERC20PaymentMetaTxPackets( - address indexed currency, - address indexed msgSender, - bytes32 indexed vectorId, - PurchaserMetaTxPacket purchaseToCreatorPacket, - PurchaserMetaTxPacket purchaseToPlatformPacket, - uint256 amount - ); - /** * @notice Emitted on a mint where discrete token ids are minted * @param vectorId Vector that payment was for @@ -488,6 +459,50 @@ contract MintManager is uint256 numMinted ); + /** + * @notice Emitted on a mint where a number of tokens are minted monotonically by the owner + * @param contractAddress Address of contract being minted on + * @param isEditionBased Denotes whether collection is edition-based + * @param editionId Edition ID, if applicable + * @param numMinted Number of tokens minted + */ + event CreatorReservesNumMint( + address indexed contractAddress, + bool indexed isEditionBased, + uint256 indexed editionId, + uint256 numMinted + ); + + /** + * @notice Emitted on a mint where a number of tokens are minted monotonically by the owner + * @param contractAddress Address of contract being minted on + * @param tokenIds IDs of tokens minted + */ + event CreatorReservesChooseMint(address indexed contractAddress, uint256[] tokenIds); + + /** + * @notice Emitted when a mechanic vector is registered + * @param mechanicVectorId Global mechanic vector ID + * @param mechanic Mechanic's address + * @param contractAddress Address of collection the mechanic is minting on + * @param editionId ID of edition, if applicable + * @param isEditionBased If true, edition based + */ + event MechanicVectorRegistered( + bytes32 indexed mechanicVectorId, + address indexed mechanic, + address indexed contractAddress, + uint256 editionId, + bool isEditionBased + ); + + /** + * @notice Emitted when a mechanic vector's pause state is toggled + * @param mechanicVectorId Global mechanic vector ID + * @param paused If true, mechanic was paused. If false, mechanic was unpaused + */ + event MechanicVectorPauseSet(bytes32 indexed mechanicVectorId, bool indexed paused); + /** * @notice Restricts calls to platform */ @@ -523,25 +538,20 @@ contract MintManager is } /** - * @notice Add platform executor. Expected to be protected by a smart contract wallet. - * @param _executor Platform executor to add + * @notice Add or deprecate platform executor + * @param _executor Platform executor to add or deprecate */ - function addPlatformExecutor(address _executor) external onlyOwner { - if (_executor == address(0) || !_platformExecutors.add(_executor)) { + function addOrDeprecatePlatformExecutor(address _executor) external onlyOwner { + if (_executor == address(0)) { _revert(InvalidExecutorChanged.selector); } - emit PlatformExecutorChanged(_executor, true); - } - - /** - * @notice Deprecate platform executor. Expected to be protected by a smart contract wallet. - * @param _executor Platform executor to deprecate - */ - function deprecatePlatformExecutor(address _executor) external onlyOwner { - if (!_platformExecutors.remove(_executor)) { - _revert(InvalidExecutorChanged.selector); + if (_platformExecutors.contains(_executor)) { + // remove exeuctor + _platformExecutors.remove(_executor); + } else { + // add executor + _platformExecutors.add(_executor); } - emit PlatformExecutorChanged(_executor, false); } /** @@ -661,6 +671,241 @@ contract MintManager is } } + /** + * @notice See {IMechanicMintManager-registerMechanicVector} + */ + function registerMechanicVector( + MechanicVectorMetadata memory _mechanicVectorMetadata, + uint96 seed, + bytes calldata vectorData + ) external { + address msgSender = _msgSender(); + bytes32 mechanicVectorId = _produceMechanicVectorId(_mechanicVectorMetadata, seed); + if ( + msgSender == _mechanicVectorMetadata.contractAddress || + Ownable(_mechanicVectorMetadata.contractAddress).owner() == msgSender + ) { + if (mechanicVectorMetadata[mechanicVectorId].contractAddress != address(0)) { + _revert(AlreadyRegisteredWithId.selector); + } + if ( + _mechanicVectorMetadata.contractAddress == address(0) || + _mechanicVectorMetadata.mechanic == address(0) || + (_mechanicVectorMetadata.isEditionBased && _mechanicVectorMetadata.isChoose) || + mechanicVectorId == bytes32(0) + ) { + _revert(InvalidMechanic.selector); + } + _mechanicVectorMetadata.paused = false; + mechanicVectorMetadata[mechanicVectorId] = _mechanicVectorMetadata; + } else { + _revert(Unauthorized.selector); + } + + IMechanic(_mechanicVectorMetadata.mechanic).createVector(mechanicVectorId, vectorData); + + emit MechanicVectorRegistered( + mechanicVectorId, + _mechanicVectorMetadata.mechanic, + _mechanicVectorMetadata.contractAddress, + _mechanicVectorMetadata.editionId, + _mechanicVectorMetadata.isEditionBased + ); + } + + /** + * @notice See {IMechanicMintManager-setPauseOnMechanicMintVector} + */ + function setPauseOnMechanicMintVector(bytes32 mechanicVectorId, bool pause) external { + address msgSender = _msgSender(); + address contractAddress = mechanicVectorMetadata[mechanicVectorId].contractAddress; + if (contractAddress == address(0)) { + _revert(InvalidMechanic.selector); + } + + if (Ownable(contractAddress).owner() == msgSender || msgSender == contractAddress) { + mechanicVectorMetadata[mechanicVectorId].paused = pause; + } else { + _revert(Unauthorized.selector); + } + + emit MechanicVectorPauseSet(mechanicVectorId, pause); + } + + /** + * @notice See {IMechanicMintManager-mechanicMintNum} + */ + function mechanicMintNum( + bytes32 mechanicVectorId, + address recipient, + uint32 numToMint, + bytes calldata data + ) external payable { + MechanicVectorMetadata memory _mechanicVectorMetadata = mechanicVectorMetadata[mechanicVectorId]; + + if (_mechanicVectorMetadata.paused) { + _revert(MechanicPaused.selector); + } + if (_mechanicVectorMetadata.isChoose) { + _revert(InvalidMechanic.selector); + } + uint256 _platformFee = (numToMint * _platformMintFee); + if (msg.value < _platformFee) { + _revert(MintFeeTooLow.selector); + } + + uint256 amountWithoutMintFee = msg.value - _platformFee; + IMechanic(_mechanicVectorMetadata.mechanic).processNumMint{ value: amountWithoutMintFee }( + mechanicVectorId, + recipient, + numToMint, + _mechanicVectorMetadata, + data + ); + + if (_mechanicVectorMetadata.isEditionBased) { + if (numToMint == 1) { + IERC721EditionMint(_mechanicVectorMetadata.contractAddress).mintOneToRecipient( + _mechanicVectorMetadata.editionId, + recipient + ); + } else { + IERC721EditionMint(_mechanicVectorMetadata.contractAddress).mintAmountToRecipient( + _mechanicVectorMetadata.editionId, + recipient, + uint256(numToMint) + ); + } + } else { + if (numToMint == 1) { + IERC721GeneralMint(_mechanicVectorMetadata.contractAddress).mintOneToOneRecipient(recipient); + } else { + IERC721GeneralMint(_mechanicVectorMetadata.contractAddress).mintAmountToOneRecipient( + recipient, + uint256(numToMint) + ); + } + } + + emit NumTokenMint(mechanicVectorId, _mechanicVectorMetadata.contractAddress, true, uint256(numToMint)); + } + + /** + * @notice See {IMechanicMintManager-mechanicMintChoose} + */ + function mechanicMintChoose( + bytes32 mechanicVectorId, + address recipient, + uint256[] calldata tokenIds, + bytes calldata data + ) external payable { + MechanicVectorMetadata memory _mechanicVectorMetadata = mechanicVectorMetadata[mechanicVectorId]; + + if (_mechanicVectorMetadata.paused) { + _revert(MechanicPaused.selector); + } + if (!_mechanicVectorMetadata.isChoose) { + _revert(InvalidMechanic.selector); + } + uint32 numToMint = uint32(tokenIds.length); + uint256 _platformFee = (numToMint * _platformMintFee); + if (msg.value < _platformFee) { + _revert(MintFeeTooLow.selector); + } + + // send value without amount needed for mint fee + IMechanic(_mechanicVectorMetadata.mechanic).processChooseMint{ value: msg.value - _platformFee }( + mechanicVectorId, + recipient, + tokenIds, + _mechanicVectorMetadata, + data + ); + + if (numToMint == 1) { + IERC721GeneralMint(_mechanicVectorMetadata.contractAddress).mintSpecificTokenToOneRecipient( + recipient, + tokenIds[0] + ); + } else { + IERC721GeneralMint(_mechanicVectorMetadata.contractAddress).mintSpecificTokensToOneRecipient( + recipient, + tokenIds + ); + } + + emit ChooseTokenMint(mechanicVectorId, _mechanicVectorMetadata.contractAddress, true, tokenIds); + } + + /* solhint-disable code-complexity */ + + /** + * @notice Let the owner of a collection mint creator reserves + * @param collection Collection contract address + * @param isEditionBased If true, collection is edition-based + * @param editionId Edition ID of collection, if applicable + * @param numToMint Number of tokens to mint on sequential mints + * @param tokenIds To reserve mint collector's choice based mints + * @param isCollectorsChoice If true, mint via collector's choice based paradigm + * @param recipient Recipient of minted tokens + */ + function creatorReservesMint( + address collection, + bool isEditionBased, + uint256 editionId, + uint256 numToMint, + uint256[] calldata tokenIds, + bool isCollectorsChoice, + address recipient + ) external payable { + address msgSender = _msgSender(); + + uint256 tokenIdsLength = tokenIds.length; + if (tokenIdsLength > 0) { + numToMint = tokenIdsLength; + } + + if (Ownable(collection).owner() == msgSender || msgSender == collection) { + // validate platform mint fee + uint256 mintFeeAmount = _platformMintFee * numToMint; + if (mintFeeAmount > msg.value) { + _revert(InvalidPaymentAmount.selector); + } + + if (isEditionBased) { + if (numToMint == 1) { + IERC721EditionMint(collection).mintOneToRecipient(editionId, recipient); + } else { + IERC721EditionMint(collection).mintAmountToRecipient(editionId, recipient, numToMint); + } + } else { + if (numToMint == 1) { + if (isCollectorsChoice) { + IERC721GeneralMint(collection).mintSpecificTokenToOneRecipient(recipient, tokenIds[0]); + } else { + IERC721GeneralMint(collection).mintOneToOneRecipient(recipient); + } + } else { + if (isCollectorsChoice) { + IERC721GeneralMint(collection).mintSpecificTokensToOneRecipient(recipient, tokenIds); + } else { + IERC721GeneralMint(collection).mintAmountToOneRecipient(recipient, numToMint); + } + } + } + + if (isCollectorsChoice) { + emit CreatorReservesChooseMint(collection, tokenIds); + } else { + emit CreatorReservesNumMint(collection, isEditionBased, editionId, numToMint); + } + } else { + _revert(Unauthorized.selector); + } + } + + /* solhint-enable code-complexity */ + /** * @notice Mint on a Series with a valid claim where one can choose the tokens to mint * @param claim Series Claim @@ -719,100 +964,12 @@ contract MintManager is } /** - * @notice Mint on vector pointing to ERC721Editions or ERC721SingleEdiion collection - * @param vectorId ID of vector - * @param numTokensToMint Number of tokens to mint - * @param mintRecipient Who to mint the NFT(s) to - */ - function vectorMintEdition721(uint256 vectorId, uint48 numTokensToMint, address mintRecipient) external payable { - address msgSender = _msgSender(); - address user = mintRecipient; - if (_useSenderForUserLimit(vectorId)) { - user = msgSender; - } - - AbridgedVectorData memory _vector = _abridgedVectors[vectorId]; - uint48 newNumClaimedViaVector = _vector.totalClaimedViaVector + numTokensToMint; - uint48 newNumClaimedForUser = uint48(userClaims[vectorId][user]) + numTokensToMint; - - if (_vector.allowlistRoot != 0) { - _revert(AllowlistInvalid.selector); - } - if (!_vector.editionBasedCollection) { - _revert(VectorWrongCollectionType.selector); - } - if (_vector.requireDirectEOA && msgSender != tx.origin) { - _revert(SenderNotDirectEOA.selector); - } - - _abridgedVectors[vectorId].totalClaimedViaVector = newNumClaimedViaVector; - userClaims[vectorId][user] = uint64(newNumClaimedForUser); - - _vectorMintEdition721( - vectorId, - _vector, - numTokensToMint, - mintRecipient, - newNumClaimedViaVector, - newNumClaimedForUser - ); - } - - /** - * @notice Mint on vector pointing to ERC721Editions or ERC721SingleEdiion collection, with allowlist - * @param vectorId ID of vector - * @param numTokensToMint Number of tokens to mint - * @param mintRecipient Who to mint the NFT(s) to - * @param proof Proof of minter's inclusion in allowlist - */ - function vectorMintEdition721WithAllowlist( - uint256 vectorId, - uint48 numTokensToMint, - address mintRecipient, - bytes32[] calldata proof - ) external payable { - address msgSender = _msgSender(); - address user = mintRecipient; - if (_useSenderForUserLimit(vectorId)) { - user = msgSender; - } - - AbridgedVectorData memory _vector = _abridgedVectors[vectorId]; - uint48 newNumClaimedViaVector = _vector.totalClaimedViaVector + numTokensToMint; - uint48 newNumClaimedForUser = uint48(userClaims[vectorId][user]) + numTokensToMint; - - // merkle tree allowlist validation - bytes32 leaf = keccak256(abi.encodePacked(user)); - if (!MerkleProof.verify(proof, _vector.allowlistRoot, leaf)) { - _revert(AllowlistInvalid.selector); - } - if (!_vector.editionBasedCollection) { - _revert(VectorWrongCollectionType.selector); - } - if (_vector.requireDirectEOA && msgSender != tx.origin) { - _revert(SenderNotDirectEOA.selector); - } - - _abridgedVectors[vectorId].totalClaimedViaVector = newNumClaimedViaVector; - userClaims[vectorId][user] = uint64(newNumClaimedForUser); - - _vectorMintEdition721( - vectorId, - _vector, - numTokensToMint, - mintRecipient, - newNumClaimedViaVector, - newNumClaimedForUser - ); - } - - /** - * @notice Mint on vector pointing to ERC721General or ERC721Generative collection + * @notice Mint via an abridged vector * @param vectorId ID of vector * @param numTokensToMint Number of tokens to mint * @param mintRecipient Who to mint the NFT(s) to */ - function vectorMintSeries721(uint256 vectorId, uint48 numTokensToMint, address mintRecipient) external payable { + function vectorMint721(uint256 vectorId, uint48 numTokensToMint, address mintRecipient) external payable { address msgSender = _msgSender(); address user = mintRecipient; if (_useSenderForUserLimit(vectorId)) { @@ -826,9 +983,6 @@ contract MintManager is if (_vector.allowlistRoot != 0) { _revert(AllowlistInvalid.selector); } - if (_vector.editionBasedCollection) { - _revert(VectorWrongCollectionType.selector); - } if (_vector.requireDirectEOA && msgSender != tx.origin) { _revert(SenderNotDirectEOA.selector); } @@ -836,62 +990,25 @@ contract MintManager is _abridgedVectors[vectorId].totalClaimedViaVector = newNumClaimedViaVector; userClaims[vectorId][user] = uint64(newNumClaimedForUser); - _vectorMintGeneral721( - vectorId, - _vector, - numTokensToMint, - mintRecipient, - newNumClaimedViaVector, - newNumClaimedForUser - ); - } - - /** - * @notice Mint on vector pointing to ERC721General or ERC721Generative collection - * @param vectorId ID of vector - * @param numTokensToMint Number of tokens to mint - * @param mintRecipient Who to mint the NFT(s) to - * @param proof Proof of minter's inclusion in allowlist - */ - function vectorMintSeries721WithAllowlist( - uint256 vectorId, - uint48 numTokensToMint, - address mintRecipient, - bytes32[] calldata proof - ) external payable { - address msgSender = _msgSender(); - address user = mintRecipient; - if (_useSenderForUserLimit(vectorId)) { - user = msgSender; - } - - AbridgedVectorData memory _vector = _abridgedVectors[vectorId]; - uint48 newNumClaimedViaVector = _vector.totalClaimedViaVector + numTokensToMint; - uint48 newNumClaimedForUser = uint48(userClaims[vectorId][user]) + numTokensToMint; - - // merkle tree allowlist validation - bytes32 leaf = keccak256(abi.encodePacked(user)); - if (!MerkleProof.verify(proof, _vector.allowlistRoot, leaf)) { - _revert(AllowlistInvalid.selector); - } if (_vector.editionBasedCollection) { - _revert(VectorWrongCollectionType.selector); - } - if (_vector.requireDirectEOA && msgSender != tx.origin) { - _revert(SenderNotDirectEOA.selector); + _vectorMintEdition721( + vectorId, + _vector, + numTokensToMint, + mintRecipient, + newNumClaimedViaVector, + newNumClaimedForUser + ); + } else { + _vectorMintGeneral721( + vectorId, + _vector, + numTokensToMint, + mintRecipient, + newNumClaimedViaVector, + newNumClaimedForUser + ); } - - _abridgedVectors[vectorId].totalClaimedViaVector = newNumClaimedViaVector; - userClaims[vectorId][user] = uint64(newNumClaimedForUser); - - _vectorMintGeneral721( - vectorId, - _vector, - numTokensToMint, - mintRecipient, - newNumClaimedViaVector, - newNumClaimedForUser - ); } /** @@ -927,27 +1044,29 @@ contract MintManager is /** * @notice Withdraw native gas token owed to platform */ - function withdrawNativeGasToken() external onlyPlatform { - uint256 withdrawnValue = address(this).balance; - (bool sentToPlatform, bytes memory dataPlatform) = _platform.call{ value: withdrawnValue }(""); + function withdrawNativeGasToken(uint256 amountToWithdraw) external onlyPlatform { + (bool sentToPlatform, bytes memory dataPlatform) = _platform.call{ value: amountToWithdraw }(""); if (!sentToPlatform) { _revert(EtherSendFailed.selector); } } /** - * @notice Update platform mint fee - * @param newPlatformMintFee New platform mint fee + * @notice Update platform payment address */ - function updatePlatformMintFee(uint256 newPlatformMintFee) external onlyOwner { + function updatePlatformAndMintFee(address payable newPlatform, uint256 newPlatformMintFee) external onlyOwner { + if (newPlatform == address(0)) { + _revert(Unauthorized.selector); + } + _platform = newPlatform; _platformMintFee = newPlatformMintFee; } /** * @notice Returns platform executors */ - function platformExecutors() external view returns (address[] memory) { - return _platformExecutors.values(); + function isPlatformExecutor(address _executor) external view returns (bool) { + return _platformExecutors.contains(_executor); } /** @@ -981,7 +1100,7 @@ contract MintManager is address signer = _claimSigner(claim, signature); return - _isPlatformExecutor(signer) && + _platformExecutors.contains(signer) && !_offchainVectorsToNoncesUsed[claim.offchainVectorId].contains(claim.claimNonce) && block.timestamp <= claim.claimExpiryTimestamp && (claim.maxClaimableViaVector == 0 || @@ -993,77 +1112,6 @@ contract MintManager is claim.maxClaimablePerUser); } - /** - * @notice Verify that series claim and series claim signature are valid for a mint - * @param claim Series Claim - * @param signature Signed + encoded claim - * @param expectedMsgSender *DEPRECATED*, keep for interface adherence - * @param tokenIds IDs of NFTs to be minted - */ - function verifySeriesClaim( - SeriesClaim calldata claim, - bytes calldata signature, - address expectedMsgSender, - uint256[] calldata tokenIds - ) external view returns (bool) { - address signer = _seriesClaimSigner(claim, signature); - uint256 numTokensToMint = tokenIds.length; - - /* solhint-disable no-empty-blocks */ - for (uint256 i = 0; i < numTokensToMint; i++) { - // if any token has already been minted, return false - try IERC721(claim.contractAddress).ownerOf(tokenIds[i]) returns (address tokenOwner) { - if (tokenOwner != address(0)) { - return false; - } - } catch { - // valid, ownerOf reverted - } - } - /* solhint-enable no-empty-blocks */ - - return - _isPlatformExecutor(signer) && - numTokensToMint <= claim.maxPerTxn && - !_offchainVectorsToNoncesUsed[claim.offchainVectorId].contains(claim.claimNonce) && - block.timestamp <= claim.claimExpiryTimestamp && - (claim.maxClaimableViaVector == 0 || - numTokensToMint + offchainVectorsClaimState[claim.offchainVectorId].numClaimed <= - claim.maxClaimableViaVector) && - (claim.maxClaimablePerUser == 0 || - numTokensToMint + offchainVectorsClaimState[claim.offchainVectorId].numClaimedPerUser[claim.claimer] <= - claim.maxClaimablePerUser); - } - - /** - * @notice Verify that claim and claim signature are valid for a mint (claim version with meta-tx packets) - * @param claim Claim - * @param signature Signed + encoded claim - * @param expectedMsgSender Expected claimer to verify claim for - */ - function verifyClaimWithMetaTxPacket( - ClaimWithMetaTxPacket calldata claim, - bytes calldata signature, - address expectedMsgSender - ) external view returns (bool) { - address signer = _claimWithMetaTxPacketSigner(claim, signature); - if (expectedMsgSender != claim.claimer) { - _revert(SenderNotClaimer.selector); - } - - return - _isPlatformExecutor(signer) && - !_offchainVectorsToNoncesUsed[claim.offchainVectorId].contains(claim.claimNonce) && - block.timestamp <= claim.claimExpiryTimestamp && - (claim.maxClaimableViaVector == 0 || - claim.numTokensToMint + offchainVectorsClaimState[claim.offchainVectorId].numClaimed <= - claim.maxClaimableViaVector) && - (claim.maxClaimablePerUser == 0 || - claim.numTokensToMint + - offchainVectorsClaimState[claim.offchainVectorId].numClaimedPerUser[expectedMsgSender] <= - claim.maxClaimablePerUser); - } - /** * @notice Returns if nonce is used for the vector * @param vectorId ID of offchain vector @@ -1226,7 +1274,7 @@ contract MintManager is ] + claim.numTokensToMint; if ( - !_isPlatformExecutor(signer) || + !_platformExecutors.contains(signer) || _offchainVectorsToNoncesUsed[claim.offchainVectorId].contains(claim.claimNonce) || block.timestamp > claim.claimExpiryTimestamp || (expectedNumClaimedViaVector > claim.maxClaimableViaVector && claim.maxClaimableViaVector != 0) || @@ -1262,7 +1310,7 @@ contract MintManager is ] + numTokensToMint; if ( - !_isPlatformExecutor(signer) || + !_platformExecutors.contains(signer) || numTokensToMint > claim.maxPerTxn || _offchainVectorsToNoncesUsed[claim.offchainVectorId].contains(claim.claimNonce) || block.timestamp > claim.claimExpiryTimestamp || @@ -1278,40 +1326,6 @@ contract MintManager is offchainVectorsClaimState[claim.offchainVectorId].numClaimedPerUser[claim.claimer] = expectedNumClaimedByUser; } - /** - * @notice Verify, and update the state of a gated mint claim (version w/ meta-tx packets) - * @param claim Claim - * @param signature Signed + encoded claim - */ - function _verifyAndUpdateClaimWithMetaTxPacket( - ClaimWithMetaTxPacket calldata claim, - bytes calldata signature - ) private { - address signer = _claimWithMetaTxPacketSigner(claim, signature); - - // cannot cache here due to nested mapping - uint256 expectedNumClaimedViaVector = offchainVectorsClaimState[claim.offchainVectorId].numClaimed + - claim.numTokensToMint; - uint256 expectedNumClaimedByUser = offchainVectorsClaimState[claim.offchainVectorId].numClaimedPerUser[ - claim.claimer - ] + claim.numTokensToMint; - - if ( - !_isPlatformExecutor(signer) || - _offchainVectorsToNoncesUsed[claim.offchainVectorId].contains(claim.claimNonce) || - block.timestamp > claim.claimExpiryTimestamp || - (expectedNumClaimedViaVector > claim.maxClaimableViaVector && claim.maxClaimableViaVector != 0) || - (expectedNumClaimedByUser > claim.maxClaimablePerUser && claim.maxClaimablePerUser != 0) - ) { - _revert(InvalidClaim.selector); - } - - _offchainVectorsToNoncesUsed[claim.offchainVectorId].add(claim.claimNonce); // mark claim nonce as used - // update claim state - offchainVectorsClaimState[claim.offchainVectorId].numClaimed = expectedNumClaimedViaVector; - offchainVectorsClaimState[claim.offchainVectorId].numClaimedPerUser[claim.claimer] = expectedNumClaimedByUser; - } - /** * @notice Process a mint on an on-chain vector * @param _vectorId ID of vector being minted on @@ -1444,7 +1458,7 @@ contract MintManager is address payable recipient, bytes32 vectorId ) private { - if (totalAmount + mintFeeAmount > msg.value) { + if (totalAmount + mintFeeAmount != msg.value) { _revert(InvalidPaymentAmount.selector); } @@ -1472,7 +1486,7 @@ contract MintManager is address currency, bytes32 vectorId ) private { - if (mintFeeAmount > msg.value) { + if (mintFeeAmount != msg.value) { _revert(MintFeeTooLow.selector); } IERC20(currency).transferFrom(payer, recipient, totalAmount); @@ -1481,54 +1495,6 @@ contract MintManager is emit ERC20Payment(currency, recipient, vectorId, payer, totalAmount, 10000); } - /** - * @notice Process payment in ERC20 with meta-tx packets, sending to creator and platform - * @param currency ERC20 currency - * @param purchaseToCreatorPacket Meta-tx packet facilitating payment to creator recipient - * @param purchaseToPlatformPacket Meta-tx packet facilitating payment to platform - * @param msgSender Claimer - * @param vectorId ID of vector (on-chain or off-chain) - * @param amount Total amount paid - */ - function _processERC20PaymentWithMetaTxPackets( - address currency, - PurchaserMetaTxPacket calldata purchaseToCreatorPacket, - PurchaserMetaTxPacket calldata purchaseToPlatformPacket, - address msgSender, - bytes32 vectorId, - uint256 amount - ) private { - uint256 previousBalance = IERC20(currency).balanceOf(msgSender); - INativeMetaTransaction(currency).executeMetaTransaction( - msgSender, - purchaseToCreatorPacket.functionSignature, - purchaseToCreatorPacket.sigR, - purchaseToCreatorPacket.sigS, - purchaseToCreatorPacket.sigV - ); - - INativeMetaTransaction(currency).executeMetaTransaction( - msgSender, - purchaseToPlatformPacket.functionSignature, - purchaseToPlatformPacket.sigR, - purchaseToPlatformPacket.sigS, - purchaseToPlatformPacket.sigV - ); - - if (IERC20(currency).balanceOf(msgSender) > (previousBalance - amount)) { - _revert(InvalidPaymentAmount.selector); - } - - emit ERC20PaymentMetaTxPackets( - currency, - msgSender, - vectorId, - purchaseToCreatorPacket, - purchaseToPlatformPacket, - amount - ); - } - /** * @notice Recover claim signature signer * @param claim Claim @@ -1550,39 +1516,13 @@ contract MintManager is return _hashTypedDataV4(keccak256(_seriesClaimABIEncoded(claim))).recover(signature); } - /** - * @notice Recover claimWithMetaTxPacket signature signer - * @param claim Claim - * @param signature Claim signature - */ - function _claimWithMetaTxPacketSigner( - ClaimWithMetaTxPacket calldata claim, - bytes calldata signature - ) private view returns (address) { - return - _hashTypedDataV4( - keccak256( - bytes.concat( - _claimWithMetaTxABIEncoded1(claim), - _claimWithMetaTxABIEncoded2(claim.claimNonce, claim.offchainVectorId) - ) - ) - ).recover(signature); - } - - /** - * @notice Returns true if account passed in is a platform executor - * @param _executor Account being checked - */ - function _isPlatformExecutor(address _executor) private view returns (bool) { - return _platformExecutors.contains(_executor); - } - /** * @dev Understand whether to use the transaction sender or the nft recipient for per-user limits on onchain vectors */ function _useSenderForUserLimit(uint256 mintVectorId) private view returns (bool) { - return ((block.chainid == 1 && mintVectorId < 19) || + return false; + /* + ((block.chainid == 1 && mintVectorId < 19) || (block.chainid == 5 && mintVectorId < 188) || (block.chainid == 42161 && mintVectorId < 6) || (block.chainid == 421613 && mintVectorId < 3) || @@ -1594,6 +1534,27 @@ contract MintManager is (block.chainid == 420 && mintVectorId < 3) || (block.chainid == 137 && mintVectorId < 7) || (block.chainid == 80001 && mintVectorId < 16)); + */ + } + + /** + * @notice Deterministically produce mechanic vector ID from mechanic vector inputs + * @param metadata Mechanic vector metadata + * @param seed Used to seed uniqueness + */ + function _produceMechanicVectorId( + MechanicVectorMetadata memory metadata, + uint96 seed + ) private pure returns (bytes32 mechanicVectorId) { + mechanicVectorId = keccak256( + abi.encodePacked( + metadata.contractAddress, + metadata.editionId, + metadata.mechanic, + metadata.isEditionBased, + seed + ) + ); } /* solhint-disable max-line-length */ @@ -1617,16 +1578,6 @@ contract MintManager is ); } - /** - * @notice Get claimWithMetaTxPacket typehash - */ - function _getClaimWithMetaTxPacketTypeHash() private pure returns (bytes32) { - return - keccak256( - "ClaimWithMetaTxPacket(address currency,address contractAddress,address claimer,uint256 pricePerToken,uint64 numTokensToMint,PurchaserMetaTxPacket purchaseToCreatorPacket,PurchaserMetaTxPacket purchaseToPlatformPacket,uint256 maxClaimableViaVector,uint256 maxClaimablePerUser,uint256 editionId,uint256 claimExpiryTimestamp,bytes32 claimNonce,bytes32 offchainVectorId)" - ); - } - /* solhint-enable max-line-length */ /** @@ -1681,40 +1632,6 @@ contract MintManager is return abi.encode(offchainVectorId); } - /** - * @notice Return abi-encoded claimWithMetaTxPacket part one - * @param claim Claim - */ - function _claimWithMetaTxABIEncoded1(ClaimWithMetaTxPacket calldata claim) private pure returns (bytes memory) { - return - abi.encode( - _getClaimWithMetaTxPacketTypeHash(), - claim.currency, - claim.contractAddress, - claim.claimer, - claim.pricePerToken, - claim.numTokensToMint, - claim.purchaseToCreatorPacket, - claim.purchaseToPlatformPacket, - claim.maxClaimableViaVector, - claim.maxClaimablePerUser, - claim.editionId, - claim.claimExpiryTimestamp - ); - } - - /** - * @notice Return abi-encoded claimWithMetaTxPacket part two - * @param claimNonce Claim's unique identifier - * @param offchainVectorId Offchain vector ID of claim - */ - function _claimWithMetaTxABIEncoded2( - bytes32 claimNonce, - bytes32 offchainVectorId - ) private pure returns (bytes memory) { - return abi.encode(claimNonce, offchainVectorId); - } - /** * @notice Compose abridged vector metadata into a `uint256` * @param paused If the abridged vector is paused diff --git a/contracts/mint/mechanics/DiscreteDutchAuctionMechanic.sol b/contracts/mint/mechanics/DiscreteDutchAuctionMechanic.sol new file mode 100644 index 0000000..e47e12a --- /dev/null +++ b/contracts/mint/mechanics/DiscreteDutchAuctionMechanic.sol @@ -0,0 +1,784 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +import "./MechanicMintManagerClientUpgradeable.sol"; +import "../../erc721/interfaces/IEditionCollection.sol"; +import "../../erc721/interfaces/IERC721GeneralSupplyMetadata.sol"; +import "./PackedPrices.sol"; + +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; + +/** + * @notice Highlight's bespoke Dutch Auction mint mechanic (rebates, discrete prices, not continuous) + * @dev Processes ether based auctions only + * DPP = Dynamic Price Period + * FPP = Fixed Price Period + * @author highlight.xyz + */ +contract DiscreteDutchAuctionMechanic is MechanicMintManagerClientUpgradeable, UUPSUpgradeable { + using EnumerableSet for EnumerableSet.Bytes32Set; + + /** + * @notice Throw when an action is unauthorized + */ + error Unauthorized(); + + /** + * @notice Throw when a vector is attempted to be created or updated with an invalid configuration + */ + error InvalidVectorConfig(); + + /** + * @notice Throw when a vector is attempted to be updated or deleted at an invalid time + */ + error InvalidUpdate(); + + /** + * @notice Throw when a vector is already created with a mechanic vector ID + */ + error VectorAlreadyCreated(); + + /** + * @notice Throw when it is invalid to mint on a vector + */ + error InvalidMint(); + + /** + * @notice Throw when it is invalid to withdraw funds from a DPP + */ + error InvalidDPPFundsWithdrawl(); + + /** + * @notice Throw when it is invalid to collect a rebate + */ + error InvalidRebate(); + + /** + * @notice Throw when a collector isn't owed any rebates + */ + error CollectorNotOwedRebate(); + + /** + * @notice Throw when the contract fails to send ether to a payment recipient + */ + error EtherSendFailed(); + + /** + * @notice Throw when the transaction sender has sent an invalid payment amount during a mint + */ + error InvalidPaymentAmount(); + + /** + * @notice Vector data + * @dev Guiding uint typing: + * log(periodDuration) <= log(timestamps) + * log(numTokensBought) <= log(maxUser) + * log(numToMint) <= log(numTokensBought) + * log(maxUser) <= log(maxTotal) + * log(lowestPriceSoldAtIndex) < log(numPrices) + * log(prices[i]) <= log(totalSales) + * log(totalPosted) <= log(totalSales) + * log(prices[i]) <= log(totalPosted) + * log(numTokensbought) + log(totalPosted) <= 256 + */ + struct DutchAuctionVector { + // slot 0 + uint48 startTimestamp; + uint48 endTimestamp; + uint32 periodDuration; + uint32 maxUserClaimableViaVector; + uint48 maxTotalClaimableViaVector; + uint48 currentSupply; + // slot 1 + uint32 lowestPriceSoldAtIndex; + uint32 tokenLimitPerTx; + uint32 numPrices; + address payable paymentRecipient; + // slot 2 + uint240 totalSales; + uint8 bytesPerPrice; + bool auctionExhausted; + bool payeeRevenueHasBeenWithdrawn; + } + + /** + * @notice Config used to control updating of fields in DutchAuctionVector + */ + struct DutchAuctionVectorUpdateConfig { + bool updateStartTimestamp; + bool updateEndTimestamp; + bool updatePeriodDuration; + bool updateMaxUserClaimableViaVector; + bool updateMaxTotalClaimableViaVector; + bool updateTokenLimitPerTx; + bool updatePaymentRecipient; + bool updatePrices; + } + + /** + * @notice User purchase info per dutch auction per user + * @param numTokensBought Number of tokens bought in the dutch auction + * @param numRebates Number of times the user has requested a rebate + * @param totalPosted Total amount paid by buyer minus rebates sent + */ + struct UserPurchaseInfo { + uint32 numTokensBought; + uint24 numRebates; + uint200 totalPosted; + } + + /** + * @notice Stores dutch auctions, indexed by global mechanic vector id + */ + mapping(bytes32 => DutchAuctionVector) private vector; + + /** + * @notice Stores dutch auction prices (packed), indexed by global mechanic vector id + */ + mapping(bytes32 => bytes) private vectorPackedPrices; + + /** + * @notice Stores user purchase info, per user per auction + */ + mapping(bytes32 => mapping(address => UserPurchaseInfo)) public userPurchaseInfo; + + /** + * @notice Emitted when a dutch auction is created + */ + event DiscreteDutchAuctionCreated(bytes32 indexed mechanicVectorId); + + /** + * @notice Emitted when a dutch auction is updated + */ + event DiscreteDutchAuctionUpdated(bytes32 indexed mechanicVectorId); + + /** + * @notice Emitted when a number of tokens are minted via a dutch auction + */ + event DiscreteDutchAuctionMint( + bytes32 indexed mechanicVectorId, + address indexed recipient, + uint200 pricePerToken, + uint48 numMinted + ); + + /** + * @notice Emitted when a collector receives a rebate + * @param mechanicVectorId Mechanic vector ID + * @param collector Collector receiving rebate + * @param rebate The amount of ETH returned to the collector + * @param currentPricePerNft The current price per NFT at the time of rebate + */ + event DiscreteDutchAuctionCollectorRebate( + bytes32 indexed mechanicVectorId, + address indexed collector, + uint200 rebate, + uint200 currentPricePerNft + ); + + /** + * @notice Emitted when the DPP revenue is withdrawn to the payment recipient once the auction hits the FPP. + * @dev NOTE - amount of funds withdrawn may include sales from the FPP. After funds are withdrawn, payment goes + * straight to the payment recipient on mint + * @param mechanicVectorId Mechanic vector ID + * @param paymentRecipient Payment recipient at time of withdrawal + * @param clearingPrice The final clearing price per NFT + * @param currentSupply The number of minted tokens to withdraw sales for + */ + event DiscreteDutchAuctionDPPFundsWithdrawn( + bytes32 indexed mechanicVectorId, + address indexed paymentRecipient, + uint200 clearingPrice, + uint48 currentSupply + ); + + /** + * @notice Initialize mechanic contract + * @param _mintManager Mint manager address + * @param platform Platform owning the contract + */ + function initialize(address _mintManager, address platform) external initializer { + __MechanicMintManagerClientUpgradeable_initialize(_mintManager, platform); + } + + /** + * @notice Create a dutch auction vector + * @param mechanicVectorId Global mechanic vector ID + * @param vectorData Vector data, to be deserialized into dutch auction vector data + */ + function createVector(bytes32 mechanicVectorId, bytes memory vectorData) external onlyMintManager { + // precaution, although MintManager tightly controls creation and prevents double creation + if (vector[mechanicVectorId].periodDuration != 0) { + _revert(VectorAlreadyCreated.selector); + } + ( + uint48 startTimestamp, + uint48 endTimestamp, + uint32 periodDuration, + uint32 maxUserClaimableViaVector, + uint48 maxTotalClaimableViaVector, + uint32 tokenLimitPerTx, + uint32 numPrices, + uint8 bytesPerPrice, + address paymentRecipient, + bytes memory packedPrices + ) = abi.decode(vectorData, (uint48, uint48, uint32, uint32, uint48, uint32, uint32, uint8, address, bytes)); + + DutchAuctionVector memory _vector = DutchAuctionVector( + startTimestamp == 0 ? uint48(block.timestamp) : startTimestamp, + endTimestamp, + periodDuration, + maxUserClaimableViaVector, + maxTotalClaimableViaVector, + 0, + 0, + tokenLimitPerTx, + numPrices, + payable(paymentRecipient), + 0, + bytesPerPrice, + false, + false + ); + + _validateVectorConfig(_vector, packedPrices, true); + + vector[mechanicVectorId] = _vector; + vectorPackedPrices[mechanicVectorId] = packedPrices; + + emit DiscreteDutchAuctionCreated(mechanicVectorId); + } + + /* solhint-disable code-complexity */ + /** + * @notice Update a dutch auction vector + * @param mechanicVectorId Global mechanic vector ID + * @param newVector New vector fields + * @param updateConfig Config denoting what fields on vector to update + */ + function updateVector( + bytes32 mechanicVectorId, + DutchAuctionVector calldata newVector, + bytes calldata newPackedPrices, + DutchAuctionVectorUpdateConfig calldata updateConfig + ) external { + MechanicVectorMetadata memory metadata = _getMechanicVectorMetadata(mechanicVectorId); + if ( + metadata.contractAddress != msg.sender && OwnableUpgradeable(metadata.contractAddress).owner() != msg.sender + ) { + _revert(Unauthorized.selector); + } + DutchAuctionVector memory currentVector = vector[mechanicVectorId]; + + // after first token has been minted, cannot update: prices, period, start time, max total claimable via vector + if ( + currentVector.currentSupply > 0 && + (updateConfig.updatePrices || + updateConfig.updatePeriodDuration || + updateConfig.updateStartTimestamp || + updateConfig.updateMaxTotalClaimableViaVector) + ) { + _revert(InvalidUpdate.selector); + } + + // construct end state of vector with updates applied, then validate + if (updateConfig.updateStartTimestamp) { + currentVector.startTimestamp = newVector.startTimestamp == 0 + ? uint48(block.timestamp) + : newVector.startTimestamp; + } + if (updateConfig.updateEndTimestamp) { + currentVector.endTimestamp = newVector.endTimestamp; + } + if (updateConfig.updatePeriodDuration) { + currentVector.periodDuration = newVector.periodDuration; + } + if (updateConfig.updateMaxUserClaimableViaVector) { + currentVector.maxUserClaimableViaVector = newVector.maxUserClaimableViaVector; + } + if (updateConfig.updateMaxTotalClaimableViaVector) { + currentVector.maxTotalClaimableViaVector = newVector.maxTotalClaimableViaVector; + } + if (updateConfig.updateTokenLimitPerTx) { + currentVector.tokenLimitPerTx = newVector.tokenLimitPerTx; + } + if (updateConfig.updatePaymentRecipient) { + currentVector.paymentRecipient = newVector.paymentRecipient; + } + if (updateConfig.updatePrices) { + currentVector.bytesPerPrice = newVector.bytesPerPrice; + currentVector.numPrices = newVector.numPrices; + } + + _validateVectorConfig(currentVector, newPackedPrices, updateConfig.updatePrices); + + // rather than updating entire vector, update per-field + if (updateConfig.updateStartTimestamp) { + vector[mechanicVectorId].startTimestamp = currentVector.startTimestamp; + } + if (updateConfig.updateEndTimestamp) { + vector[mechanicVectorId].endTimestamp = currentVector.endTimestamp; + } + if (updateConfig.updatePeriodDuration) { + vector[mechanicVectorId].periodDuration = currentVector.periodDuration; + } + if (updateConfig.updateMaxUserClaimableViaVector) { + vector[mechanicVectorId].maxUserClaimableViaVector = currentVector.maxUserClaimableViaVector; + } + if (updateConfig.updateMaxTotalClaimableViaVector) { + vector[mechanicVectorId].maxTotalClaimableViaVector = currentVector.maxTotalClaimableViaVector; + } + if (updateConfig.updateTokenLimitPerTx) { + vector[mechanicVectorId].tokenLimitPerTx = currentVector.tokenLimitPerTx; + } + if (updateConfig.updatePaymentRecipient) { + vector[mechanicVectorId].paymentRecipient = currentVector.paymentRecipient; + } + if (updateConfig.updatePrices) { + vectorPackedPrices[mechanicVectorId] = newPackedPrices; + vector[mechanicVectorId].bytesPerPrice = currentVector.bytesPerPrice; + vector[mechanicVectorId].numPrices = currentVector.numPrices; + } + + emit DiscreteDutchAuctionUpdated(mechanicVectorId); + } + + /* solhint-enable code-complexity */ + + /** + * @notice See {IMechanic-processNumMint} + */ + function processNumMint( + bytes32 mechanicVectorId, + address recipient, + uint32 numToMint, + MechanicVectorMetadata calldata mechanicVectorMetadata, + bytes calldata data + ) external payable onlyMintManager { + _processMint(mechanicVectorId, recipient, numToMint); + } + + /** + * @notice See {IMechanic-processChooseMint} + */ + function processChooseMint( + bytes32 mechanicVectorId, + address recipient, + uint256[] calldata tokenIds, + MechanicVectorMetadata calldata mechanicVectorMetadata, + bytes calldata data + ) external payable onlyMintManager { + _processMint(mechanicVectorId, recipient, uint32(tokenIds.length)); + } + + /** + * @notice Rebate a collector any rebates they're eligible for + * @param mechanicVectorId Mechanic vector ID + * @param collector Collector to send rebates to + */ + function rebateCollector(bytes32 mechanicVectorId, address payable collector) external { + DutchAuctionVector memory _vector = vector[mechanicVectorId]; + UserPurchaseInfo memory _userPurchaseInfo = userPurchaseInfo[mechanicVectorId][collector]; + + if (_vector.currentSupply == 0) { + _revert(InvalidRebate.selector); + } + bool _auctionExhausted = _vector.auctionExhausted; + if (!_auctionExhausted) { + _auctionExhausted = _isAuctionExhausted( + mechanicVectorId, + _vector.currentSupply, + _vector.maxTotalClaimableViaVector + ); + if (_auctionExhausted) { + vector[mechanicVectorId].auctionExhausted = true; + } + } + + // rebate collector at the price: + // - lowest price sold at if auction is exhausted (vector sold out or collection sold out) + // - current price otherwise + uint200 currentPrice = PackedPrices.priceAt( + vectorPackedPrices[mechanicVectorId], + _vector.bytesPerPrice, + _auctionExhausted + ? _vector.lowestPriceSoldAtIndex + : _calculatePriceIndex(_vector.startTimestamp, _vector.periodDuration, _vector.numPrices) + ); + uint200 currentPriceObligation = _userPurchaseInfo.numTokensBought * currentPrice; + uint200 amountOwed = _userPurchaseInfo.totalPosted - currentPriceObligation; + + if (amountOwed == 0) { + _revert(CollectorNotOwedRebate.selector); + } + + userPurchaseInfo[mechanicVectorId][collector].totalPosted = currentPriceObligation; + userPurchaseInfo[mechanicVectorId][collector].numRebates = _userPurchaseInfo.numRebates + 1; + + (bool sentToCollector, bytes memory data) = collector.call{ value: amountOwed }(""); + if (!sentToCollector) { + _revert(EtherSendFailed.selector); + } + + emit DiscreteDutchAuctionCollectorRebate(mechanicVectorId, collector, amountOwed, currentPrice); + } + + /** + * @notice Withdraw funds collected through the dynamic period of a dutch auction + * @param mechanicVectorId Mechanic vector ID + */ + function withdrawDPPFunds(bytes32 mechanicVectorId) external { + // all slots are used, so load entire object from storage + DutchAuctionVector memory _vector = vector[mechanicVectorId]; + + if (_vector.payeeRevenueHasBeenWithdrawn || _vector.currentSupply == 0) { + _revert(InvalidDPPFundsWithdrawl.selector); + } + bool _auctionExhausted = _vector.auctionExhausted; + if (!_auctionExhausted) { + _auctionExhausted = _isAuctionExhausted( + mechanicVectorId, + _vector.currentSupply, + _vector.maxTotalClaimableViaVector + ); + if (_auctionExhausted) { + vector[mechanicVectorId].auctionExhausted = true; + } + } + uint32 priceIndex = _auctionExhausted + ? _vector.lowestPriceSoldAtIndex + : _calculatePriceIndex(_vector.startTimestamp, _vector.periodDuration, _vector.numPrices); + + // if any of the following 3 are met, DPP funds can be withdrawn: + // - auction is in FPP + // - maxTotalClaimableViaVector is reached + // - all tokens have been minted on collection (outside of vector knowledge) + if (!_auctionExhausted && !_auctionIsInFPP(_vector.currentSupply, priceIndex, _vector.numPrices)) { + _revert(InvalidDPPFundsWithdrawl.selector); + } + + vector[mechanicVectorId].payeeRevenueHasBeenWithdrawn = true; + + uint200 clearingPrice = PackedPrices.priceAt( + vectorPackedPrices[mechanicVectorId], + _vector.bytesPerPrice, + priceIndex + ); + uint200 totalRefund = _vector.currentSupply * clearingPrice; + // precaution: protect against pulling out more than total sales -> + // guards against bad actor pulling out more via + // funds collection + rebate price ascending setup (theoretically not possible) + if (totalRefund > _vector.totalSales) { + _revert(InvalidDPPFundsWithdrawl.selector); + } + + (bool sentToPaymentRecipient, bytes memory data) = _vector.paymentRecipient.call{ value: totalRefund }(""); + if (!sentToPaymentRecipient) { + _revert(EtherSendFailed.selector); + } + + emit DiscreteDutchAuctionDPPFundsWithdrawn( + mechanicVectorId, + _vector.paymentRecipient, + clearingPrice, + _vector.currentSupply + ); + } + + /** + * @notice Get how much of a rebate a user is owed + * @param mechanicVectorId Mechanic vector ID + * @param user User to get rebate information for + */ + function getUserInfo( + bytes32 mechanicVectorId, + address user + ) external view returns (uint256 rebate, UserPurchaseInfo memory) { + DutchAuctionVector memory _vector = vector[mechanicVectorId]; + UserPurchaseInfo memory _userPurchaseInfo = userPurchaseInfo[mechanicVectorId][user]; + + if (_vector.currentSupply == 0) { + return (0, _userPurchaseInfo); + } + + // rebate collector at the price: + // - lowest price sold at if vector is sold out or collection is sold out + // - current price otherwise + uint200 currentPrice = PackedPrices.priceAt( + vectorPackedPrices[mechanicVectorId], + _vector.bytesPerPrice, + _isAuctionExhausted(mechanicVectorId, _vector.currentSupply, _vector.maxTotalClaimableViaVector) + ? _vector.lowestPriceSoldAtIndex + : _calculatePriceIndex(_vector.startTimestamp, _vector.periodDuration, _vector.numPrices) + ); + uint200 currentPriceObligation = _userPurchaseInfo.numTokensBought * currentPrice; + uint256 amountOwed = uint256(_userPurchaseInfo.totalPosted - currentPriceObligation); + + return (amountOwed, _userPurchaseInfo); + } + + /** + * @notice Get how much is owed to the payment recipient (currently) + * @param mechanicVectorId Mechanic vector ID + * @param escrowFunds Amount owed to the creator currently + * @param amountFinalized Whether this is the actual amount that will be owed (will decrease until the auction ends) + */ + function getPayeePotentialEscrowedFunds( + bytes32 mechanicVectorId + ) external view returns (uint256 escrowFunds, bool amountFinalized) { + return _getPayeePotentialEscrowedFunds(mechanicVectorId); + } + + /** + * @notice Get raw vector data + * @param mechanicVectorId Mechanic vector ID + */ + function getRawVector( + bytes32 mechanicVectorId + ) external view returns (DutchAuctionVector memory _vector, bytes memory packedPrices) { + _vector = vector[mechanicVectorId]; + packedPrices = vectorPackedPrices[mechanicVectorId]; + } + + /** + * @notice Get a vector's full state, including the refund currently owed to the creator and human-readable prices + * @param mechanicVectorId Mechanic vector ID + */ + function getVectorState( + bytes32 mechanicVectorId + ) + external + view + returns ( + DutchAuctionVector memory _vector, + uint200[] memory prices, + uint200 currentPrice, + uint256 payeePotentialEscrowedFunds, + uint256 collectionSupply, + uint256 collectionSize, + bool escrowedFundsAmountFinalized, + bool auctionExhausted, + bool auctionInFPP + ) + { + _vector = vector[mechanicVectorId]; + (payeePotentialEscrowedFunds, escrowedFundsAmountFinalized) = _getPayeePotentialEscrowedFunds(mechanicVectorId); + (collectionSupply, collectionSize) = _collectionSupplyAndSize(mechanicVectorId); + auctionExhausted = + _vector.auctionExhausted || + _isAuctionExhausted(mechanicVectorId, _vector.currentSupply, _vector.maxTotalClaimableViaVector); + uint32 priceIndex = auctionExhausted + ? _vector.lowestPriceSoldAtIndex + : _calculatePriceIndex(_vector.startTimestamp, _vector.periodDuration, _vector.numPrices); + currentPrice = PackedPrices.priceAt(vectorPackedPrices[mechanicVectorId], _vector.bytesPerPrice, priceIndex); + auctionInFPP = _auctionIsInFPP(_vector.currentSupply, priceIndex, _vector.numPrices); + prices = PackedPrices.unpack(vectorPackedPrices[mechanicVectorId], _vector.bytesPerPrice, _vector.numPrices); + } + + /* solhint-disable no-empty-blocks */ + /** + * @notice Limit upgrades of contract to DiscreteDutchAuctionMechanic owner + * @param // New implementation address + */ + function _authorizeUpgrade(address) internal override onlyOwner {} + + /** + * @notice Process mint logic common through sequential and collector's choice based mints + * @param mechanicVectorId Mechanic vector ID + * @param recipient Mint recipient + * @param numToMint Number of tokens to mint + */ + function _processMint(bytes32 mechanicVectorId, address recipient, uint32 numToMint) private { + DutchAuctionVector memory _vector = vector[mechanicVectorId]; + UserPurchaseInfo memory _userPurchaseInfo = userPurchaseInfo[mechanicVectorId][recipient]; + + uint48 newSupply = _vector.currentSupply + numToMint; + if ( + block.timestamp < _vector.startTimestamp || + (block.timestamp > _vector.endTimestamp && _vector.endTimestamp != 0) || + (_vector.maxTotalClaimableViaVector != 0 && newSupply > _vector.maxTotalClaimableViaVector) || + (_vector.maxUserClaimableViaVector != 0 && + _userPurchaseInfo.numTokensBought + numToMint > _vector.maxUserClaimableViaVector) || + (_vector.tokenLimitPerTx != 0 && numToMint > _vector.tokenLimitPerTx) || + _vector.auctionExhausted + ) { + _revert(InvalidMint.selector); + } + + // can safely cast down here since the value is dependent on array length + uint32 priceIndex = _calculatePriceIndex(_vector.startTimestamp, _vector.periodDuration, _vector.numPrices); + uint200 price = PackedPrices.priceAt(vectorPackedPrices[mechanicVectorId], _vector.bytesPerPrice, priceIndex); + uint200 totalPrice = price * numToMint; + + if (totalPrice > msg.value) { + _revert(InvalidPaymentAmount.selector); + } + + // update lowestPriceSoldAtindex, currentSupply, totalSales and user purchase info + if (_vector.lowestPriceSoldAtIndex != priceIndex) { + vector[mechanicVectorId].lowestPriceSoldAtIndex = priceIndex; + } + vector[mechanicVectorId].currentSupply = newSupply; + vector[mechanicVectorId].totalSales = _vector.totalSales + totalPrice; + _userPurchaseInfo.numTokensBought += numToMint; + _userPurchaseInfo.totalPosted += uint200(msg.value); // if collector sent more, let them collect the difference + userPurchaseInfo[mechanicVectorId][recipient] = _userPurchaseInfo; + + if (_vector.payeeRevenueHasBeenWithdrawn) { + // send ether value to payment recipient + (bool sentToPaymentRecipient, bytes memory data) = _vector.paymentRecipient.call{ value: totalPrice }(""); + if (!sentToPaymentRecipient) { + _revert(EtherSendFailed.selector); + } + } + + emit DiscreteDutchAuctionMint(mechanicVectorId, recipient, price, numToMint); + } + + /** + * @notice Validate a dutch auction vector + * @param _vector Dutch auction vector being validated + */ + function _validateVectorConfig( + DutchAuctionVector memory _vector, + bytes memory packedPrices, + bool validateIndividualPrices + ) private { + if ( + _vector.periodDuration == 0 || + _vector.paymentRecipient == address(0) || + _vector.numPrices < 2 || + _vector.bytesPerPrice > 32 + ) { + _revert(InvalidVectorConfig.selector); + } + if (_vector.endTimestamp != 0) { + // allow the last period to be truncated + if (_vector.startTimestamp + ((_vector.numPrices - 1) * _vector.periodDuration) >= _vector.endTimestamp) { + _revert(InvalidVectorConfig.selector); + } + } + if (validateIndividualPrices) { + if (_vector.bytesPerPrice * _vector.numPrices != packedPrices.length) { + _revert(InvalidVectorConfig.selector); + } + uint200[] memory prices = PackedPrices.unpack(packedPrices, _vector.bytesPerPrice, _vector.numPrices); + uint200 lastPrice = prices[0]; + uint256 numPrices = uint256(_vector.numPrices); // cast up into uint256 for gas savings on array check + for (uint256 i = 1; i < _vector.numPrices; i++) { + if (prices[i] >= lastPrice) { + _revert(InvalidVectorConfig.selector); + } + lastPrice = prices[i]; + } + } + } + + /** + * @notice Get how much is owed to the payment recipient currently + * @param mechanicVectorId Mechanic vector ID + * @return escrowFunds + isFinalAmount + */ + function _getPayeePotentialEscrowedFunds(bytes32 mechanicVectorId) private view returns (uint256, bool) { + DutchAuctionVector memory _vector = vector[mechanicVectorId]; + + if (_vector.payeeRevenueHasBeenWithdrawn) { + // escrowed funds have already been withdrawn / finalized + return (0, true); + } + if (_vector.currentSupply == 0) { + return (0, false); + } + + bool auctionExhausted = _vector.auctionExhausted || + _isAuctionExhausted(mechanicVectorId, _vector.currentSupply, _vector.maxTotalClaimableViaVector); + uint32 priceIndex = auctionExhausted + ? _vector.lowestPriceSoldAtIndex + : _calculatePriceIndex(_vector.startTimestamp, _vector.periodDuration, _vector.numPrices); + uint200 potentialClearingPrice = PackedPrices.priceAt( + vectorPackedPrices[mechanicVectorId], + _vector.bytesPerPrice, + priceIndex + ); + + // escrowFunds is only final if auction is exhausted or in FPP + return ( + uint256(_vector.currentSupply * potentialClearingPrice), + (auctionExhausted || _auctionIsInFPP(_vector.currentSupply, priceIndex, _vector.numPrices)) + ); + } + + /** + * @notice Return true if an auction has reached its max supply or if the underlying collection has + * @param mechanicVectorId Mechanic vector ID + * @param currentSupply Current supply minted through the vector + * @param maxTotalClaimableViaVector Max claimable via the vector + */ + function _isAuctionExhausted( + bytes32 mechanicVectorId, + uint48 currentSupply, + uint48 maxTotalClaimableViaVector + ) private view returns (bool) { + if (maxTotalClaimableViaVector != 0 && currentSupply >= maxTotalClaimableViaVector) return true; + (uint256 supply, uint256 size) = _collectionSupplyAndSize(mechanicVectorId); + return size != 0 && supply >= size; + } + + /** + * @notice Returns a collection's current supply + * @param mechanicVectorId Mechanic vector ID + */ + function _collectionSupplyAndSize(bytes32 mechanicVectorId) private view returns (uint256 supply, uint256 size) { + MechanicVectorMetadata memory metadata = _getMechanicVectorMetadata(mechanicVectorId); + if (metadata.contractAddress == address(0)) { + revert("Vector doesn't exist"); + } + if (metadata.isEditionBased) { + IEditionCollection.EditionDetails memory edition = IEditionCollection(metadata.contractAddress) + .getEditionDetails(metadata.editionId); + supply = edition.supply; + size = edition.size; + } else { + // supply holds a tighter constraint (no burns), some old contracts don't have it + try IERC721GeneralSupplyMetadata(metadata.contractAddress).supply() returns (uint256 _supply) { + supply = _supply; + } catch { + supply = IERC721GeneralSupplyMetadata(metadata.contractAddress).totalSupply(); + } + size = IERC721GeneralSupplyMetadata(metadata.contractAddress).limitSupply(); + } + } + + /** + * @notice Calculate what price the dutch auction is at + * @param startTimestamp Auction start time + * @param periodDuration Time per period + * @param numPrices Number of prices + */ + function _calculatePriceIndex( + uint48 startTimestamp, + uint32 periodDuration, + uint32 numPrices + ) private view returns (uint32) { + if (block.timestamp <= startTimestamp) { + return 0; + } + uint256 hypotheticalIndex = uint256((block.timestamp - startTimestamp) / periodDuration); + if (hypotheticalIndex >= numPrices) { + return numPrices - 1; + } else { + return uint32(hypotheticalIndex); + } + } + + /** + * @notice Return if the auction is in the fixed price period + * @param currentSupply Current supply of tokens minted via mechanic vector + * @param priceIndex Index of price prices + * @param numPrices Number of prices + */ + function _auctionIsInFPP(uint48 currentSupply, uint256 priceIndex, uint32 numPrices) private pure returns (bool) { + return currentSupply > 0 && priceIndex == numPrices - 1; + } +} diff --git a/contracts/mint/mechanics/MechanicMintManagerClient.sol b/contracts/mint/mechanics/MechanicMintManagerClient.sol new file mode 100644 index 0000000..810498a --- /dev/null +++ b/contracts/mint/mechanics/MechanicMintManagerClient.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +import "../../utils/Ownable.sol"; +import "./interfaces/IMechanic.sol"; +import "./interfaces/IMechanicMintManagerView.sol"; + +/** + * @notice MintManager client, to be used by mechanic contracts + * @author highlight.xyz + */ +abstract contract MechanicMintManagerClient is Ownable, IMechanic { + /** + * @notice Throw when caller is not MintManager + */ + error NotMintManager(); + + /** + * @notice Throw when input mint manager is invalid + */ + error InvalidMintManager(); + + /** + * @notice Mint manager + */ + address public mintManager; + + /** + * @notice Enforce caller to be mint manager + */ + modifier onlyMintManager() { + if (msg.sender != mintManager) { + _revert(NotMintManager.selector); + } + _; + } + + /** + * @notice Initialize mechanic contract + * @param _mintManager Mint manager address + * @param platform Platform owning the contract + */ + constructor(address _mintManager, address platform) Ownable() { + mintManager = _mintManager; + _transferOwnership(platform); + } + + /** + * @notice Update the mint manager + * @param _mintManager New mint manager + */ + function updateMintManager(address _mintManager) external onlyOwner { + if (_mintManager == address(0)) { + _revert(InvalidMintManager.selector); + } + + mintManager = _mintManager; + } + + /** + * @notice Get a mechanic mint vector's metadata + * @param mechanicVectorId Mechanic vector ID + */ + function _getMechanicVectorMetadata( + bytes32 mechanicVectorId + ) internal view returns (MechanicVectorMetadata memory) { + return IMechanicMintManagerView(mintManager).mechanicVectorMetadata(mechanicVectorId); + } + + /** + * @dev For more efficient reverts. + */ + function _revert(bytes4 errorSelector) internal pure { + assembly { + mstore(0x00, errorSelector) + revert(0x00, 0x04) + } + } +} diff --git a/contracts/mint/mechanics/MechanicMintManagerClientUpgradeable.sol b/contracts/mint/mechanics/MechanicMintManagerClientUpgradeable.sol new file mode 100644 index 0000000..895aad4 --- /dev/null +++ b/contracts/mint/mechanics/MechanicMintManagerClientUpgradeable.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "./interfaces/IMechanic.sol"; +import "./interfaces/IMechanicMintManagerView.sol"; + +/** + * @notice MintManager client, to be used by mechanic contracts + * @author highlight.xyz + */ +abstract contract MechanicMintManagerClientUpgradeable is OwnableUpgradeable, IMechanic { + /** + * @notice Throw when caller is not MintManager + */ + error NotMintManager(); + + /** + * @notice Throw when input mint manager is invalid + */ + error InvalidMintManager(); + + /** + * @notice Mint manager + */ + address public mintManager; + + /** + * @notice Enforce caller to be mint manager + */ + modifier onlyMintManager() { + if (msg.sender != mintManager) { + _revert(NotMintManager.selector); + } + _; + } + + /** + * @notice Update the mint manager + * @param _mintManager New mint manager + */ + function updateMintManager(address _mintManager) external onlyOwner { + if (_mintManager == address(0)) { + _revert(InvalidMintManager.selector); + } + + mintManager = _mintManager; + } + + /** + * @notice Initialize mechanic mint manager client + * @param _mintManager Mint manager address + * @param platform Platform owning the contract + */ + function __MechanicMintManagerClientUpgradeable_initialize( + address _mintManager, + address platform + ) internal onlyInitializing { + __Ownable_init(); + mintManager = _mintManager; + _transferOwnership(platform); + } + + /** + * @notice Get a mechanic mint vector's metadata + * @param mechanicVectorId Mechanic vector ID + */ + function _getMechanicVectorMetadata( + bytes32 mechanicVectorId + ) internal view returns (MechanicVectorMetadata memory) { + return IMechanicMintManagerView(mintManager).mechanicVectorMetadata(mechanicVectorId); + } + + /** + * @dev For more efficient reverts. + */ + function _revert(bytes4 errorSelector) internal pure { + assembly { + mstore(0x00, errorSelector) + revert(0x00, 0x04) + } + } +} diff --git a/contracts/mint/mechanics/PackedPrices.sol b/contracts/mint/mechanics/PackedPrices.sol new file mode 100644 index 0000000..4a5132a --- /dev/null +++ b/contracts/mint/mechanics/PackedPrices.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +/** + * @notice Util library to pack, unpack, and access packed prices data + * @author highlight.xyz + */ +library PackedPrices { + /** + * @notice Return unpacked prices + * @dev Assume length validations are met + */ + function unpack( + bytes memory packedPrices, + uint8 bytesPerPrice, + uint32 numPrices + ) internal view returns (uint200[] memory prices) { + prices = new uint200[](numPrices); + + for (uint32 i = 0; i < numPrices; i++) { + prices[i] = priceAt(packedPrices, bytesPerPrice, i); + } + } + + /** + * @notice Return price at an index + * @dev Assume length validations are met + */ + function priceAt(bytes memory packedPrices, uint8 bytesPerPrice, uint32 index) internal view returns (uint200) { + uint256 readIndex = index * bytesPerPrice; + uint256 price; + + assembly { + // Load 32 bytes starting from the correct position in packedPrices + price := mload(add(packedPrices, add(32, readIndex))) + } + + return uint200(price >> (256 - (bytesPerPrice * 8))); + } +} diff --git a/contracts/mint/mechanics/interfaces/IMechanic.sol b/contracts/mint/mechanics/interfaces/IMechanic.sol new file mode 100644 index 0000000..a236d9f --- /dev/null +++ b/contracts/mint/mechanics/interfaces/IMechanic.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +import "./IMechanicData.sol"; + +/** + * @notice Interface that mint mechanics are forced to adhere to, + * provided they support both collector's choice and sequential minting + */ +interface IMechanic is IMechanicData { + /** + * @notice Create a mechanic vector on the mechanic + * @param mechanicVectorId Global mechanic vector ID + * @param vectorData Mechanic vector data + */ + function createVector(bytes32 mechanicVectorId, bytes calldata vectorData) external; + + /** + * @notice Process a sequential mint + * @param mechanicVectorId Global ID identifying mint vector, using this mechanic + * @param recipient Mint recipient + * @param numToMint Number of tokens to mint + * @param mechanicVectorMetadata Mechanic vector metadata + * @param data Custom data that can be deserialized and processed according to implementation + */ + function processNumMint( + bytes32 mechanicVectorId, + address recipient, + uint32 numToMint, + MechanicVectorMetadata calldata mechanicVectorMetadata, + bytes calldata data + ) external payable; + + /** + * @notice Process a collector's choice mint + * @param mechanicVectorId Global ID identifying mint vector, using this mechanic + * @param recipient Mint recipient + * @param tokenIds IDs of tokens to mint + * @param mechanicVectorMetadata Mechanic vector metadata + * @param data Custom data that can be deserialized and processed according to implementation + */ + function processChooseMint( + bytes32 mechanicVectorId, + address recipient, + uint256[] calldata tokenIds, + MechanicVectorMetadata calldata mechanicVectorMetadata, + bytes calldata data + ) external payable; +} diff --git a/contracts/mint/mechanics/interfaces/IMechanicData.sol b/contracts/mint/mechanics/interfaces/IMechanicData.sol new file mode 100644 index 0000000..ab8f94c --- /dev/null +++ b/contracts/mint/mechanics/interfaces/IMechanicData.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +/** + * @notice Defines a mechanic's metadata on the MintManager + */ +interface IMechanicData { + /** + * @notice A mechanic's metadata + * @param contractAddress Collection contract address + * @param editionId Edition ID if the collection is edition based + * @param mechanic Address of mint mechanic contract + * @param isEditionBased True if collection is edition based + * @param isChoose True if collection uses a collector's choice mint paradigm + * @param paused True if mechanic vector is paused + */ + struct MechanicVectorMetadata { + address contractAddress; + uint96 editionId; + address mechanic; + bool isEditionBased; + bool isChoose; + bool paused; + } +} diff --git a/contracts/mint/mechanics/interfaces/IMechanicMintManager.sol b/contracts/mint/mechanics/interfaces/IMechanicMintManager.sol new file mode 100644 index 0000000..8eb9690 --- /dev/null +++ b/contracts/mint/mechanics/interfaces/IMechanicMintManager.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +import "./IMechanicData.sol"; + +/** + * @notice Capabilities on MintManager pertaining to mechanics + */ +interface IMechanicMintManager is IMechanicData { + /** + * @notice Register a new mechanic vector + * @param _mechanicVectorMetadata Mechanic vector metadata + * @param seed Used to seed uniqueness into mechanic vector ID generation + * @param vectorData Vector data to store on mechanic (optional) + */ + function registerMechanicVector( + MechanicVectorMetadata calldata _mechanicVectorMetadata, + uint96 seed, + bytes calldata vectorData + ) external; + + /** + * @notice Pause or unpause a mechanic vector + * @param mechanicVectorId Global mechanic ID + * @param pause If true, pause the mechanic mint vector. If false, unpause + */ + function setPauseOnMechanicMintVector(bytes32 mechanicVectorId, bool pause) external; + + /** + * @notice Mint a number of tokens sequentially via a mechanic vector + * @param mechanicVectorId Global mechanic ID + * @param recipient Mint recipient + * @param numToMint Number of tokens to mint + * @param data Custom data to be processed by mechanic + */ + function mechanicMintNum( + bytes32 mechanicVectorId, + address recipient, + uint32 numToMint, + bytes calldata data + ) external payable; + + /** + * @notice Mint a specific set of token ids via a mechanic vector + * @param mechanicVectorId Global mechanic ID + * @param recipient Mint recipient + * @param tokenIds IDs of tokens to mint + * @param data Custom data to be processed by mechanic + */ + function mechanicMintChoose( + bytes32 mechanicVectorId, + address recipient, + uint256[] calldata tokenIds, + bytes calldata data + ) external payable; +} diff --git a/contracts/mint/mechanics/interfaces/IMechanicMintManagerView.sol b/contracts/mint/mechanics/interfaces/IMechanicMintManagerView.sol new file mode 100644 index 0000000..5d0a23b --- /dev/null +++ b/contracts/mint/mechanics/interfaces/IMechanicMintManagerView.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +import "./IMechanicData.sol"; + +interface IMechanicMintManagerView is IMechanicData { + /** + * @notice Get a mechanic vector's metadata + * @param mechanicVectorId Global mechanic vector ID + */ + function mechanicVectorMetadata(bytes32 mechanicVectorId) external view returns (MechanicVectorMetadata memory); +} diff --git a/contracts/mint/mechanics/test/TestDiscreteDutchAuctionMechanic.sol b/contracts/mint/mechanics/test/TestDiscreteDutchAuctionMechanic.sol new file mode 100644 index 0000000..b69baf5 --- /dev/null +++ b/contracts/mint/mechanics/test/TestDiscreteDutchAuctionMechanic.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +import "../DiscreteDutchAuctionMechanic.sol"; + +/** + * @author highlight.xyz + * @dev Mock DiscreteDutchAuctionMechanic + */ +contract TestDiscreteDutchAuctionMechanic is DiscreteDutchAuctionMechanic { + /** + * @dev Test function to test upgrades + */ + function test() external pure returns (bool) { + return true; + } +} diff --git a/contracts/test/TestMintManager.sol b/contracts/test/TestMintManager.sol index e37ecf0..d1cb1b8 100644 --- a/contracts/test/TestMintManager.sol +++ b/contracts/test/TestMintManager.sol @@ -11,7 +11,7 @@ contract TestMintManager is MintManager { /** * @dev Test function to test upgrades */ - function test() external pure returns (string memory) { - return "test"; + function test() external pure returns (bool) { + return true; } } diff --git a/hardhat.config.ts b/hardhat.config.ts index 2e5db8e..c4e7244 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -20,7 +20,7 @@ if (!chainAccounts) { throw new Error("Please setup accounts.json by running `cp sample.accounts.json accounts.json`"); } -const chainIds = { +export const chainIds = { local: 1337, mainnet: 1, "polygon-mainnet": 137, @@ -44,7 +44,7 @@ function getChainConfig(chain: keyof typeof chainIds): NetworkUserConfig { }; } -function getUrl(chain: keyof typeof chainIds): string { +export function getUrl(chain: keyof typeof chainIds): string { if (chain === "arbitrum") { return "https://arb-mainnet.g.alchemy.com/v2/6RXKTS3PtSM59L41inqVagpZW3-r_rq9"; } else if (chain === "arbitrum-goerli") { diff --git a/package.json b/package.json index 0a672d4..2251747 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evm-contracts", - "version": "1.3.0", + "version": "1.4.0", "description": "Highlight EVM Smart Contract Protocols", "main": "index.js", "repository": "git@github.com:highlightxyz/evm-contracts.git", @@ -15,7 +15,7 @@ "@ethersproject/bytes": "^5.6.1", "@ethersproject/providers": "^5.6.8", "@nomicfoundation/hardhat-chai-matchers": "^1.0.2", - "@nomicfoundation/hardhat-network-helpers": "^1.0.3", + "@nomicfoundation/hardhat-network-helpers": "^1.0.9", "@nomicfoundation/hardhat-toolbox": "^1.0.2", "@nomiclabs/hardhat-ethers": "^2.1.0", "@nomiclabs/hardhat-etherscan": "^3.1.0", @@ -73,7 +73,7 @@ "prettier:fix": "yarn prettier:check -w", "test": "yarn test:separate", "test:individual": "AUTO_MINING_ON=true hardhat test", - "test:separate": "yarn test:individual test/AuctionsTest.ts && yarn test:individual test/EditionsMetadataRendererTest.ts && yarn test:individual test/ERC721BaseTest.ts && yarn test:individual test/ERC721EditionsTest.ts && yarn test:individual test/ERC721GeneralTest.ts && yarn test:individual test/ERC721GeneralSequenceTest.ts && yarn test:individual test/ERC721GenerativeTest.ts && yarn test:individual test/ERC721SingleEditionTest.ts && yarn test:individual test/ERC721StandardTest.ts && yarn test:individual test/MetaTransactionsTest.ts && yarn test:individual test/MintManagerTest.ts && yarn test:individual test/UpgradesTest.ts", + "test:separate": "yarn test:individual test/AuctionsTest.ts && yarn test:individual test/EditionsMetadataRendererTest.ts && yarn test:individual test/ERC721BaseTest.ts && yarn test:individual test/ERC721EditionsTest.ts && yarn test:individual test/ERC721GeneralTest.ts && yarn test:individual test/ERC721GeneralSequenceTest.ts && yarn test:individual test/ERC721GenerativeTest.ts && yarn test:individual test/ERC721SingleEditionTest.ts && yarn test:individual test/ERC721StandardTest.ts && yarn test:individual test/MetaTransactionsTest.ts && yarn test:individual test/MintManagerTest.ts && yarn test:individual test/MechanicMintVectorsTest.ts && yarn test:individual test/UpgradesTest.ts", "typechain": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat typechain", "local": "hardhat node", "local:ipv4": "hardhat node --hostname 127.0.0.1", @@ -83,9 +83,12 @@ }, "dependencies": { "@openzeppelin/contracts-upgradeable": "^4.7.3", + "axios": "^1.5.1", + "csv-parse": "^5.5.1", "hardhat-contract-sizer": "^2.6.1", "keccak256": "^1.0.6", "merkletreejs": "^0.2.32", + "mime-types": "^2.1.35", "sol-merger": "^4.1.1", "split-file": "^2.3.0" } diff --git a/protocol-addresses.json b/protocol-addresses.json index 72bfc86..a63fb05 100644 --- a/protocol-addresses.json +++ b/protocol-addresses.json @@ -6,13 +6,15 @@ "AuctionManager": "0x3216FB0105f64cC375E2f431d1a6D00A1A955559", "ERC721EditionsImplementation": "0x91cDE68af933688116337EEBD7d11e8d63AAA76E", "ERC721SingleEditionImplementation": "0x59cC2a7D1Bf61256EbE39F1e9F2497e95317Ea2D", - "ERC721EditionsDFSImplementation": "0x47f690ad0168B6E3476B59A15a5dFe7adb8E34bc", + "ERC721EditionsDFSImplementation": "0x006cdD31f45F7e544a874B28763E1825C81128d5", "ERC721SingleEditionDFSImplementation": "0xD09F64dbbCAa076Fde30Ddd2e23194f5F786665E", "ERC721GeneralImplementation": "0xAAa81ce4795001654Dc56577ed431950D633dABA", "ERC721GeneralSequenceImplementation": "0x8d67B6ACE3fC4d90b8c276e1d70646ec705b0C9b", - "ERC721GenerativeImplementation": "0x66cCdD047d23d17887331BAf500FccDFAc1EB8b9", + "ERC721GenerativeImplementation": "0x2Be3CE514884dcF92505a9FDaBDe6541779C129b", "MinimalForwarder": "0x7Ab179690168f06D4F897A6C0b749C1524F4C772", - "Observability": "0xD21cf74A08CEb52555702658d3556300B0983158" + "Observability": "0xD21cf74A08CEb52555702658d3556300B0983158", + "FileDeployer": "0xd687847559A3bEc088251f3cC33E7BAf31e4aB48", + "DiscreteDutchAuctionMechanic": "0x94fa6e7fc2555ada63ea56cfff425558360f0074" }, "goerli": { "MintManager": "0xBF6B4F9Ef1E4B371c40701b5f856F9Fc1d659c70", @@ -21,13 +23,15 @@ "AuctionManager": "0xa94310AeeD50687f7c39ACdAA5FCd311AEDB25f8", "ERC721EditionsImplementation": "0x703Fd59DEee1727eaf7751EDe79ec22c3F7Db07B", "ERC721SingleEditionImplementation": "0x8B7E1DC485e931F4a15392a0E2DC0D61A16A68aB", - "ERC721EditionsDFSImplementation": "0xb75a9E4495F6995448f68dE5bA978ea8957ba3Eb", + "ERC721EditionsDFSImplementation": "0x14d986A1743af0A53B7D60De5189B7fff3494AFa", "ERC721SingleEditionDFSImplementation": "0xAbAE4df16c1262F8465FCBDcD6E006a75Fb3b739", "ERC721GeneralImplementation": "0x667f810C960537A53532c00a0973205bE2fe2165", "ERC721GeneralSequenceImplementation": "0x96dB8495a5dEA40aDc1d4CFE45eB84F1c82d143B", - "ERC721GenerativeImplementation": "0x098DF5287796C023045A74F82825a7b68CDDACbF", + "ERC721GenerativeImplementation": "0x00Edfc8bE8897893786232e96367c8E040E2eb6D", "MinimalForwarder": "0x4905B2ee259994F664d443e740bC2cA1d9cf2f1D", - "Observability": "0xFfEc25843068E69CAA0E36eA004D7749bD9EfB19" + "Observability": "0xFfEc25843068E69CAA0E36eA004D7749bD9EfB19", + "FileDeployer": "0x29e3F4B932c1E0B989E3B6AbCf56Ae342c5AD65a", + "DiscreteDutchAuctionMechanic": "0xae129080C7840538301550802cBc520c336CEEca" }, "optimism": { "MintManager": "0xFafd47bb399d570b5AC95694c5D2a1fb5EA030bB", @@ -36,13 +40,15 @@ "AuctionManager": "0x3AD45858a983D193D98BD4e6C14852a4cADcDBeA", "ERC721EditionsImplementation": "0x23E4ffb289f7696b9957De566A06cF9B325d9bCA", "ERC721SingleEditionImplementation": "0xcCC80ea84E3e6Ee8CaAB489092d46bb912b493AD", - "ERC721EditionsDFSImplementation": "0xc111b1033DC8f32d85c152D7ac89C4311344D77D", + "ERC721EditionsDFSImplementation": "0xecA1aAfE5437B3a231B9E450c47Ffa8De8575a03", "ERC721SingleEditionDFSImplementation": "0x9fA3eA2B36fed5803Ca743E09fEd3204E2B59866", "ERC721GeneralImplementation": "0x51544960e278b38c13c29F2944C1C839fEfCE6E2", "ERC721GeneralSequenceImplementation": "0xF4007F45DCd05BE758Fe9b26500B0010a07dB3cB", - "ERC721GenerativeImplementation": "0x1B142d3edD537DF8Fb7D29Cb12D842ECD430287f", + "ERC721GenerativeImplementation": "0x1372557dF3Cc3F8616D416e52217c797Ae3eEdce", "MinimalForwarder": "0xC5402e0BAF74c1042D72749cB8cA78c58dD93D6f", - "Observability": "0xAA45a6e4e1E6e43c14B366Dd0228874fb1DC0eF9" + "Observability": "0xAA45a6e4e1E6e43c14B366Dd0228874fb1DC0eF9", + "FileDeployer": "0x21c3a69EaD9b81863B83757ff2645803fF7c7690", + "DiscreteDutchAuctionMechanic": "0x15753e20667961fB30d5aa92e2255B876568BE7e" }, "polygon-mainnet": { "MintManager": "0xfbb65C52f439B762F712026CF6DD7D8E82F81eb9", @@ -51,13 +57,15 @@ "AuctionManager": "0x3CEDCb3170489f2FB509DB23D8A864A55B45036F", "ERC721EditionsImplementation": "0xF150CB22e56FDA37F3c51A6a35f0aC0fd771db2f", "ERC721SingleEditionImplementation": "0x91cDE68af933688116337EEBD7d11e8d63AAA76E", - "ERC721EditionsDFSImplementation": "0xcBdd4A901C7535ceCB7d5c0C1C930fD752a57a35", + "ERC721EditionsDFSImplementation": "0x939Fd86C2a0c58202d1F14F59Acd4466A85bC412", "ERC721SingleEditionDFSImplementation": "0x6abC18F4e8c7D8980DdBb97FDE7d6521B394F16A", "ERC721GeneralImplementation": "0x64b35B64DAB456c489124Dc07aA3eD100DdFeD7E", "ERC721GeneralSequenceImplementation": "0xc27925863bF67384e16Dcb1225228c88d0F44A8f", - "ERC721GenerativeImplementation": "0x8be96B473b08d580418906c7442DeFB5C9C444A9", + "ERC721GenerativeImplementation": "0xa79dafa06bFF0765baa36C4f6731FdC755553887", "MinimalForwarder": "0x03214f1434D84Dd58FcDFc339577c1B3a7Dd9BdE", - "Observability": "0x43Ef6CB43586B4B3ce0F4b728D4AE08dD30a0d1e" + "Observability": "0x43Ef6CB43586B4B3ce0F4b728D4AE08dD30a0d1e", + "FileDeployer": "0x117542b736cB5314a59453081b66208863CC1Acc", + "DiscreteDutchAuctionMechanic": "0xAE22Cd8052D64e7C2aF6B5E3045Fab0a86C8334C" }, "optimism-goerli": { "MintManager": "0x41cbab1028984A34C1338F437C726de791695AE8", @@ -66,13 +74,15 @@ "AuctionManager": "0x79307CeE06153CA7986759B0727023A2472F395B", "ERC721EditionsImplementation": "0xB2416393Ce488DA1EA2Ac86ab0e87a2Cf5d7a44F", "ERC721SingleEditionImplementation": "0xC0CEC6dd216C0388CD28DeC2F6FBe9aaFf749e9c", - "ERC721EditionsDFSImplementation": "0xF6c1093E467Ba60a41aBf901D875CDB027F924ac", + "ERC721EditionsDFSImplementation": "0xB0101CC0443768e5990Cfd9adC03313D283B1a7E", "ERC721SingleEditionDFSImplementation": "0x621c7cE76Cde5761c7611721B770f347a0b6376E", "ERC721GeneralImplementation": "0xBe2099b6361e4551BDdF953011Ed1DD39CCfa2a1", "ERC721GeneralSequenceImplementation": "0xe254901fC1F3ACd6E6AA409f95dB718235a015c8", - "ERC721GenerativeImplementation": "0x323784B03b217Ff00BA4F7De2257698312f2F045", + "ERC721GenerativeImplementation": "0xab162414800fdf441B18F2f5af94334840b8f678", "MinimalForwarder": "0x3AD45858a983D193D98BD4e6C14852a4cADcDBeA", - "Observability": "0xF18660E9E7c1B6015c0f491F4b5602fB3a626Caa" + "Observability": "0xF18660E9E7c1B6015c0f491F4b5602fB3a626Caa", + "FileDeployer": "0x20475183625aE0eD5Dcd2553a660B06FF52af8Bd", + "DiscreteDutchAuctionMechanic": "0x5ae0bE472147dd425f73F5c10069043133401427" }, "zora-goerli": { "MintManager": "0x9AcDfE8020c3c191F7aA158e1c155F12e55c9717", @@ -81,13 +91,15 @@ "AuctionManager": "0xa594011DB733d09C1EEB347fb2f7dFc99d118ba1", "ERC721EditionsImplementation": "0x954386A2b103A8AD2B933E44Ea148036f73DC4B9", "ERC721SingleEditionImplementation": "0x473F9552a53595887074B8A8B798509e223B118E", - "ERC721EditionsDFSImplementation": "0xcD1127AC7Ae10A63E44ca3e3a8238D2d2cf1D455", + "ERC721EditionsDFSImplementation": "0x734ACE995eaE06cFCBfE6cc33e0F524ab27e4ac1", "ERC721SingleEditionDFSImplementation": "0x701703EF716c4fe4086ef9a904683683d553e282", "ERC721GeneralImplementation": "0xf60cb5F236A344080Ca3bF50C5dC523309809F80", "ERC721GeneralSequenceImplementation": "0x9491aA1c2f46319A645637c4105f4199B251e4dD", - "ERC721GenerativeImplementation": "0x3216FB0105f64cC375E2f431d1a6D00A1A955559", + "ERC721GenerativeImplementation": "0x6e83e7ec8dBF2a21C6FE90d95E250158313FDcc3", "MinimalForwarder": "0x8087039152c472Fa74F47398628fF002994056EA", - "Observability": "0xa1Cef877695E24DF6643f5B6B47Eb6fCeF214A38" + "Observability": "0xa1Cef877695E24DF6643f5B6B47Eb6fCeF214A38", + "FileDeployer": "0xAFfC7C9BfB48FFD2a580e1a0d36f8cc7D45Dcb58", + "DiscreteDutchAuctionMechanic": "0x778b5ef98f0C8803F6424bB07412489b2Fbd58B3" }, "base": { "MintManager": "0x8087039152c472Fa74F47398628fF002994056EA", @@ -96,13 +108,15 @@ "AuctionManager": "0x9AcDfE8020c3c191F7aA158e1c155F12e55c9717", "ERC721EditionsImplementation": "0xa95DE682A887A7e7f781F7832CF52a3b59E336F6", "ERC721SingleEditionImplementation": "0x778b5ef98f0C8803F6424bB07412489b2Fbd58B3", - "ERC721EditionsDFSImplementation": "0x08708D961b65f5fA019646ABD1724dEd00AB9272", + "ERC721EditionsDFSImplementation": "0x9a304EFD52C63F030f2910f484d517faA2444575", "ERC721SingleEditionDFSImplementation": "0x295D4e1472CdEe0bB3a2D03fF56dA5a2f8C81197", "ERC721GeneralImplementation": "0x1eB81B6A226591DF4D3248B4f55456De357929e2", "ERC721GeneralSequenceImplementation": "0xae7Fc5F056Ebd29FAdCC390e83EeDaeEEc8674E9", - "ERC721GenerativeImplementation": "0xF9C059C33E47Dd255489dED83acf37d90d1dF960", + "ERC721GenerativeImplementation": "0x08FD471a972Ad95FE2BF14d490EB2aaFE28f0aff", "MinimalForwarder": "0xAB98CD0e04Bb1FCd6320611fCAD6a7e534d8B302", - "Observability": "0x4e0AfBa59894060369881f4Bc9ba05731A4119f1" + "Observability": "0x4e0AfBa59894060369881f4Bc9ba05731A4119f1", + "FileDeployer": "0x799d1CC242637847756f0400d1F83FCF94Cb051e", + "DiscreteDutchAuctionMechanic": "0xA748BE280C9a00edaF7d04076FE8A93c59e95B03" }, "arbitrum": { "MintManager": "0x41cbab1028984A34C1338F437C726de791695AE8", @@ -111,13 +125,15 @@ "AuctionManager": "0x79307CeE06153CA7986759B0727023A2472F395B", "ERC721EditionsImplementation": "0xF6c1093E467Ba60a41aBf901D875CDB027F924ac", "ERC721SingleEditionImplementation": "0x20475183625aE0eD5Dcd2553a660B06FF52af8Bd", - "ERC721EditionsDFSImplementation": "0xab162414800fdf441B18F2f5af94334840b8f678", + "ERC721EditionsDFSImplementation": "0x31C5C70330c9a1D3099d8f77381e82a218d5c71a", "ERC721SingleEditionDFSImplementation": "0xB0101CC0443768e5990Cfd9adC03313D283B1a7E", "ERC721GeneralImplementation": "0x23E4ffb289f7696b9957De566A06cF9B325d9bCA", "ERC721GeneralSequenceImplementation": "0xcCC80ea84E3e6Ee8CaAB489092d46bb912b493AD", - "ERC721GenerativeImplementation": "0xa12f77d7b39a7b556Ba4BE6ec7328B0049288ac3", + "ERC721GenerativeImplementation": "0xfF1C44BbE0943931E5E8962DAA0885a4f5Dd4fcd", "MinimalForwarder": "0x3AD45858a983D193D98BD4e6C14852a4cADcDBeA", - "Observability": "0xF18660E9E7c1B6015c0f491F4b5602fB3a626Caa" + "Observability": "0xF18660E9E7c1B6015c0f491F4b5602fB3a626Caa", + "FileDeployer": "0x4c3896dd0b55B3B62D560620B1D8bF99643fFCCE", + "DiscreteDutchAuctionMechanic": "0x3a2aFe86E594540cbf3eA345dd29e09228f186D2" }, "polygon-mumbai": { "MintManager": "0x2C92212426Ea6E41C894F8db3bEb1E6f4991c75c", @@ -126,13 +142,15 @@ "AuctionManager": "0xF9FEf499aDF4550FA87C63E1111C8a0531DF45a1", "ERC721EditionsImplementation": "0x248AE3998B98D9eb046205f18c9B9210fFECFE2a", "ERC721SingleEditionImplementation": "0x479Cc569416E8934403E12Ec56475Ad6f8aBa3a4", - "ERC721EditionsDFSImplementation": "0xbFFe75006599E92D02C5832Ed17E8D7dC851Ca81", + "ERC721EditionsDFSImplementation": "0xEE5D605bE1aB67344C80F9Dc4836460f56614566", "ERC721SingleEditionDFSImplementation": "0xe4C6a0a3cFe2c004DDFC7eA12726bdE4C53A2784", "ERC721GeneralImplementation": "0xFC954d004b8e4a6F82BEeE38a0C41A89Af3866cE", "ERC721GeneralSequenceImplementation": "0x1333328f8b76a4d0a91a30bea67A9Cd6164A9b96", - "ERC721GenerativeImplementation": "0xDFe1918e9C736751F7bE4CB2E0FbDe1A6da4f75a", + "ERC721GenerativeImplementation": "0xdc01D22327d142f45070Cb01f3f507878734A6f9", "MinimalForwarder": "0xD66A0f91BAFD0Fd6b7503ff97E028c9B54a7001f", - "Observability": "0x74A07B1F2B1d1Dec82341F18959cfb8B89353c87" + "Observability": "0x74A07B1F2B1d1Dec82341F18959cfb8B89353c87", + "FileDeployer": "0x513bd3bc623c42a807fBD162a58682941A12935F", + "DiscreteDutchAuctionMechanic": "0xBf0ddCC1cC1635Ade2F99042771e7cD7a923a187" }, "base-goerli": { "MintManager": "0xa1Cef877695E24DF6643f5B6B47Eb6fCeF214A38", @@ -141,13 +159,15 @@ "AuctionManager": "0xE019FF8033d9C761985A3EE1fa5d97Cc9Cf6d5c0", "ERC721EditionsImplementation": "0x1800E1Db8513Bc6c96E38D9DB840cDFcAb8f9944", "ERC721SingleEditionImplementation": "0xCbd8d75658f82c680727C36AF6c1c365B118938F", - "ERC721EditionsDFSImplementation": "0xcCC80ea84E3e6Ee8CaAB489092d46bb912b493AD", + "ERC721EditionsDFSImplementation": "0xdeAa8693C7085FaC16B20Cd5C69d84F7790926bf", "ERC721SingleEditionDFSImplementation": "0x23E4ffb289f7696b9957De566A06cF9B325d9bCA", "ERC721GeneralImplementation": "0xa12f77d7b39a7b556Ba4BE6ec7328B0049288ac3", "ERC721GeneralSequenceImplementation": "0x4c3896dd0b55B3B62D560620B1D8bF99643fFCCE", - "ERC721GenerativeImplementation": "0xf09f2c184350B0Cf79c71FA35199B8aA77B577FA", + "ERC721GenerativeImplementation": "0xdD606eb8af309BD6e901b2d6E6dE2F233358b324", "MinimalForwarder": "0x4e0AfBa59894060369881f4Bc9ba05731A4119f1", - "Observability": "0xe2CE42156E8456704fbEA047419404858E9324Af" + "Observability": "0xe2CE42156E8456704fbEA047419404858E9324Af", + "FileDeployer": "0xB644D70A52b4e555815EF3Ec76488dbdA9DF972D", + "DiscreteDutchAuctionMechanic": "0x887A07d968b9b515E85a428c287397F4488005EE" }, "arbitrum-goerli": { "MintManager": "0xd698911B1Bb2a9c849Bf5e2604aF110766f396b6", @@ -156,13 +176,15 @@ "AuctionManager": "0x970a9F248Fc6AE03BB255E8863Cd6fc36E631e5d", "ERC721EditionsImplementation": "0xCbd8d75658f82c680727C36AF6c1c365B118938F", "ERC721SingleEditionImplementation": "0x5ae0bE472147dd425f73F5c10069043133401427", - "ERC721EditionsDFSImplementation": "0xB644D70A52b4e555815EF3Ec76488dbdA9DF972D", + "ERC721EditionsDFSImplementation": "0x32e187F0B32C9B8Cbc5980a16C5ED0EcD6f9d96E", "ERC721SingleEditionDFSImplementation": "0xf09f2c184350B0Cf79c71FA35199B8aA77B577FA", "ERC721GeneralImplementation": "0xcCC80ea84E3e6Ee8CaAB489092d46bb912b493AD", "ERC721GeneralSequenceImplementation": "0xa12f77d7b39a7b556Ba4BE6ec7328B0049288ac3", - "ERC721GenerativeImplementation": "0x4c3896dd0b55B3B62D560620B1D8bF99643fFCCE", + "ERC721GenerativeImplementation": "0x90618E3338dd970ca634ac92dAa9E1DcF66B1c57", "MinimalForwarder": "0xa594011DB733d09C1EEB347fb2f7dFc99d118ba1", - "Observability": "0x526fe4Ed6f23f34a97015E41f469fD54f37036f5" + "Observability": "0x526fe4Ed6f23f34a97015E41f469fD54f37036f5", + "FileDeployer": "0x11Eb3C36fa9b2B0F4Ee67BCe1016960588A814ba", + "DiscreteDutchAuctionMechanic": "0x5437D752A878f6969bEd14fD733782BBD230489b" }, "zora": { "MintManager": "0x3AD45858a983D193D98BD4e6C14852a4cADcDBeA", @@ -171,12 +193,14 @@ "AuctionManager": "0x41cbab1028984A34C1338F437C726de791695AE8", "ERC721EditionsImplementation": "0x4AFa58b8c2Dfe756e851d9073aeA95467fc1BBf5", "ERC721SingleEditionImplementation": "0xFAd107F688301db69e99693e00D1D891c44a0913", - "ERC721EditionsDFSImplementation": "0x94E57Bf72643f68c8Fecf00D30aC17e6b1Be0E2C", + "ERC721EditionsDFSImplementation": "0x68bB0F207F0184bf754C141d56939251BbB38Be7", "ERC721SingleEditionDFSImplementation": "0x799d1CC242637847756f0400d1F83FCF94Cb051e", "ERC721GeneralImplementation": "0xbE5AdDc34D89E12572C80C5f672E17C6b6e7c988", "ERC721GeneralSequenceImplementation": "0x4619b9673241eB41B642Dc04371100d238b73fFE", - "ERC721GenerativeImplementation": "0x3c58C9306e61F370c76d4a35083901c75D9592c7", + "ERC721GenerativeImplementation": "0xcEC770BA360aDf184C961A3494521f1B5DCEa39C", "MinimalForwarder": "0xFafd47bb399d570b5AC95694c5D2a1fb5EA030bB", - "Observability": "0x21fed85E54507164FD6c9Eb76870AFF41098106b" + "Observability": "0x21fed85E54507164FD6c9Eb76870AFF41098106b", + "FileDeployer": "0xB627f0469683f68aC78E1deD4eFA8545aa4c4DE3", + "DiscreteDutchAuctionMechanic": "0xf12A4018647DD2275072967Fd5F3ac5Fef7a0471" } } diff --git a/systemContractsConfig.json b/systemContractsConfig.json index 29a9267..1f8493b 100644 --- a/systemContractsConfig.json +++ b/systemContractsConfig.json @@ -124,20 +124,20 @@ "8453": "0x778b5ef98f0C8803F6424bB07412489b2Fbd58B3" }, "ERC721EditionsDFSImplementation": { - "5": "0xb75a9E4495F6995448f68dE5bA978ea8957ba3Eb", + "5": "0x14d986A1743af0A53B7D60De5189B7fff3494AFa", "1337": "0x9A676e781A523b5d0C0e43731313A708CB607508", - "80001": "0xbFFe75006599E92D02C5832Ed17E8D7dC851Ca81", - "137": "0xcBdd4A901C7535ceCB7d5c0C1C930fD752a57a35", - "137-staging": "0xcBdd4A901C7535ceCB7d5c0C1C930fD752a57a35", - "1": "0x47f690ad0168B6E3476B59A15a5dFe7adb8E34bc", - "42161": "0xab162414800fdf441B18F2f5af94334840b8f678", - "421613": "0xB644D70A52b4e555815EF3Ec76488dbdA9DF972D", - "10": "0xc111b1033DC8f32d85c152D7ac89C4311344D77D", - "420": "0xF6c1093E467Ba60a41aBf901D875CDB027F924ac", - "84531": "0xcCC80ea84E3e6Ee8CaAB489092d46bb912b493AD", - "7777777": "0x94E57Bf72643f68c8Fecf00D30aC17e6b1Be0E2C", - "999": "0xcD1127AC7Ae10A63E44ca3e3a8238D2d2cf1D455", - "8453": "0x08708D961b65f5fA019646ABD1724dEd00AB9272" + "80001": "0xEE5D605bE1aB67344C80F9Dc4836460f56614566", + "137": "0x939Fd86C2a0c58202d1F14F59Acd4466A85bC412", + "137-staging": "0x939Fd86C2a0c58202d1F14F59Acd4466A85bC412", + "1": "0x006cdD31f45F7e544a874B28763E1825C81128d5", + "42161": "0x31C5C70330c9a1D3099d8f77381e82a218d5c71a", + "421613": "0x32e187F0B32C9B8Cbc5980a16C5ED0EcD6f9d96E", + "10": "0xecA1aAfE5437B3a231B9E450c47Ffa8De8575a03", + "420": "0xB0101CC0443768e5990Cfd9adC03313D283B1a7E", + "84531": "0xdeAa8693C7085FaC16B20Cd5C69d84F7790926bf", + "7777777": "0x68bB0F207F0184bf754C141d56939251BbB38Be7", + "999": "0x734ACE995eaE06cFCBfE6cc33e0F524ab27e4ac1", + "8453": "0x9a304EFD52C63F030f2910f484d517faA2444575" }, "ERC721SingleEditionDFSImplementation": { "5": "0xAbAE4df16c1262F8465FCBDcD6E006a75Fb3b739", @@ -188,20 +188,20 @@ "8453": "0xae7Fc5F056Ebd29FAdCC390e83EeDaeEEc8674E9" }, "ERC721GenerativeImplementation": { - "5": "0x098DF5287796C023045A74F82825a7b68CDDACbF", + "5": "0x00Edfc8bE8897893786232e96367c8E040E2eb6D", "1337": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", - "80001": "0xDFe1918e9C736751F7bE4CB2E0FbDe1A6da4f75a", - "137": "0x8be96B473b08d580418906c7442DeFB5C9C444A9", - "137-staging": "0x8be96B473b08d580418906c7442DeFB5C9C444A9", - "1": "0x66cCdD047d23d17887331BAf500FccDFAc1EB8b9", - "42161": "0xa12f77d7b39a7b556Ba4BE6ec7328B0049288ac3", - "421613": "0x4c3896dd0b55B3B62D560620B1D8bF99643fFCCE", - "10": "0x1B142d3edD537DF8Fb7D29Cb12D842ECD430287f", - "420": "0x323784B03b217Ff00BA4F7De2257698312f2F045", - "84531": "0xf09f2c184350B0Cf79c71FA35199B8aA77B577FA", - "7777777": "0x3c58C9306e61F370c76d4a35083901c75D9592c7", - "999": "0x3216FB0105f64cC375E2f431d1a6D00A1A955559", - "8453": "0xF9C059C33E47Dd255489dED83acf37d90d1dF960" + "80001": "0xdc01D22327d142f45070Cb01f3f507878734A6f9", + "137": "0xa79dafa06bFF0765baa36C4f6731FdC755553887", + "137-staging": "0xa79dafa06bFF0765baa36C4f6731FdC755553887", + "1": "0x2Be3CE514884dcF92505a9FDaBDe6541779C129b", + "42161": "0xfF1C44BbE0943931E5E8962DAA0885a4f5Dd4fcd", + "421613": "0x90618E3338dd970ca634ac92dAa9E1DcF66B1c57", + "10": "0x1372557dF3Cc3F8616D416e52217c797Ae3eEdce", + "420": "0xab162414800fdf441B18F2f5af94334840b8f678", + "84531": "0xdD606eb8af309BD6e901b2d6E6dE2F233358b324", + "7777777": "0xcEC770BA360aDf184C961A3494521f1B5DCEa39C", + "999": "0x6e83e7ec8dBF2a21C6FE90d95E250158313FDcc3", + "8453": "0x08FD471a972Ad95FE2BF14d490EB2aaFE28f0aff" }, "MinimalForwarder": { "5": "0x4905B2ee259994F664d443e740bC2cA1d9cf2f1D", @@ -250,5 +250,21 @@ "7777777": "0xB627f0469683f68aC78E1deD4eFA8545aa4c4DE3", "999": "0xAFfC7C9BfB48FFD2a580e1a0d36f8cc7D45Dcb58", "8453": "0x799d1CC242637847756f0400d1F83FCF94Cb051e" + }, + "DiscreteDutchAuctionMechanic": { + "5": "0xae129080C7840538301550802cBc520c336CEEca", + "1337": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed", + "80001": "0xBf0ddCC1cC1635Ade2F99042771e7cD7a923a187", + "137": "0xAE22Cd8052D64e7C2aF6B5E3045Fab0a86C8334C", + "137-staging": "0xAE22Cd8052D64e7C2aF6B5E3045Fab0a86C8334C", + "1": "0x94fa6e7fc2555ada63ea56cfff425558360f0074", + "42161": "0x3a2aFe86E594540cbf3eA345dd29e09228f186D2", + "421613": "0x5437D752A878f6969bEd14fD733782BBD230489b", + "10": "0x15753e20667961fB30d5aa92e2255B876568BE7e", + "420": "0x5ae0bE472147dd425f73F5c10069043133401427", + "84531": "0x887A07d968b9b515E85a428c287397F4488005EE", + "7777777": "0xf12A4018647DD2275072967Fd5F3ac5Fef7a0471", + "999": "0x778b5ef98f0C8803F6424bB07412489b2Fbd58B3", + "8453": "0xA748BE280C9a00edaF7d04076FE8A93c59e95B03" } } diff --git a/test/ERC721BaseTest.ts b/test/ERC721BaseTest.ts index ec74b39..b4db6e8 100644 --- a/test/ERC721BaseTest.ts +++ b/test/ERC721BaseTest.ts @@ -297,6 +297,7 @@ describe("ERC721 Base functionality", () => { emr.address, editionsOwner, null, + null, ownerOnlyTokenManager.address, ); @@ -714,6 +715,7 @@ describe("ERC721 Base functionality", () => { "name", "SYM", null, + null, false, ownerOnlyTokenManager.address, ); diff --git a/test/ERC721EditionsDFSTest.ts b/test/ERC721EditionsDFSTest.ts index 11682b8..1b9f021 100644 --- a/test/ERC721EditionsDFSTest.ts +++ b/test/ERC721EditionsDFSTest.ts @@ -680,11 +680,11 @@ describe("ERC721EditionsDFS functionality", () => { DEFAULT_ONCHAIN_MINT_VECTOR.allowlistRoot, ]); - await expect(mintManager.vectorMintEdition721(1, 2, editionsOwner.address, { value: parseEther("0.0008").mul(2) })) + await expect(mintManager.vectorMint721(1, 2, editionsOwner.address, { value: parseEther("0.0008").mul(2) })) .to.emit(mintManager, "NumTokenMint") .withArgs(ethers.utils.hexZeroPad(ethers.utils.hexlify(1), 32), editions.address, true, 2); - await expect(mintManager.vectorMintEdition721(1, 1, editionsOwner.address)).to.be.revertedWithCustomError( + await expect(mintManager.vectorMint721(1, 1, editionsOwner.address)).to.be.revertedWithCustomError( mintManager, "OnchainVectorMintGuardFailed", ); @@ -719,11 +719,11 @@ describe("ERC721EditionsDFS functionality", () => { .to.emit(mintManager, "EditionVectorCreated") .withArgs(2, 1, editions.address); - await expect(mintManager.vectorMintEdition721(2, 1, editionsOwner.address, { value: parseEther("0.0008") })) + await expect(mintManager.vectorMint721(2, 1, editionsOwner.address, { value: parseEther("0.0008") })) .to.emit(mintManager, "NumTokenMint") .withArgs(ethers.utils.hexZeroPad(ethers.utils.hexlify(2), 32), editions.address, true, 1); - await expect(mintManager.vectorMintEdition721(2, 1, editionsOwner.address)).to.be.revertedWithCustomError( + await expect(mintManager.vectorMint721(2, 1, editionsOwner.address)).to.be.revertedWithCustomError( mintManager, "OnchainVectorMintGuardFailed", ); diff --git a/test/ERC721EditionsTest.ts b/test/ERC721EditionsTest.ts index 2ff0b54..8fc27bc 100644 --- a/test/ERC721EditionsTest.ts +++ b/test/ERC721EditionsTest.ts @@ -691,14 +691,14 @@ describe("ERC721Editions functionality", () => { ]); await expect( - mintManager.vectorMintEdition721(1, 2, editionsOwner.address, { + mintManager.vectorMint721(1, 2, editionsOwner.address, { value: ethers.utils.parseEther("0.0008").mul(2), }), ) .to.emit(mintManager, "NumTokenMint") .withArgs(ethers.utils.hexZeroPad(ethers.utils.hexlify(1), 32), editions.address, true, 2); - await expect(mintManager.vectorMintEdition721(1, 1, editionsOwner.address)).to.be.revertedWithCustomError( + await expect(mintManager.vectorMint721(1, 1, editionsOwner.address)).to.be.revertedWithCustomError( mintManager, "OnchainVectorMintGuardFailed", ); @@ -733,13 +733,11 @@ describe("ERC721Editions functionality", () => { .to.emit(mintManager, "EditionVectorCreated") .withArgs(2, 1, editions.address); - await expect( - mintManager.vectorMintEdition721(2, 1, editionsOwner.address, { value: ethers.utils.parseEther("0.0008") }), - ) + await expect(mintManager.vectorMint721(2, 1, editionsOwner.address, { value: ethers.utils.parseEther("0.0008") })) .to.emit(mintManager, "NumTokenMint") .withArgs(ethers.utils.hexZeroPad(ethers.utils.hexlify(2), 32), editions.address, true, 1); - await expect(mintManager.vectorMintEdition721(2, 1, editionsOwner.address)).to.be.revertedWithCustomError( + await expect(mintManager.vectorMint721(2, 1, editionsOwner.address)).to.be.revertedWithCustomError( mintManager, "OnchainVectorMintGuardFailed", ); diff --git a/test/ERC721GeneralSequenceTest.ts b/test/ERC721GeneralSequenceTest.ts index d528fde..aafb121 100644 --- a/test/ERC721GeneralSequenceTest.ts +++ b/test/ERC721GeneralSequenceTest.ts @@ -594,13 +594,11 @@ describe("ERC721GeneralSequence functionality", () => { DEFAULT_ONCHAIN_MINT_VECTOR.allowlistRoot, ]); - await expect( - mintManager.vectorMintSeries721(1, 2, owner.address, { value: ethers.utils.parseEther("0.0008").mul(2) }), - ) + await expect(mintManager.vectorMint721(1, 2, owner.address, { value: ethers.utils.parseEther("0.0008").mul(2) })) .to.emit(mintManager, "NumTokenMint") .withArgs(ethers.utils.hexZeroPad(ethers.utils.hexlify(1), 32), general.address, true, 2); - await expect(mintManager.vectorMintSeries721(1, 1, owner.address)).to.be.revertedWithCustomError( + await expect(mintManager.vectorMint721(1, 1, owner.address)).to.be.revertedWithCustomError( mintManager, "OnchainVectorMintGuardFailed", ); diff --git a/test/ERC721GeneralTest.ts b/test/ERC721GeneralTest.ts index 0f6b3ee..ea3d122 100644 --- a/test/ERC721GeneralTest.ts +++ b/test/ERC721GeneralTest.ts @@ -687,13 +687,11 @@ describe("ERC721General functionality", () => { DEFAULT_ONCHAIN_MINT_VECTOR.allowlistRoot, ]); - await expect( - mintManager.vectorMintSeries721(1, 2, owner.address, { value: ethers.utils.parseEther("0.0008").mul(2) }), - ) + await expect(mintManager.vectorMint721(1, 2, owner.address, { value: ethers.utils.parseEther("0.0008").mul(2) })) .to.emit(mintManager, "NumTokenMint") .withArgs(ethers.utils.hexZeroPad(ethers.utils.hexlify(1), 32), general.address, true, 2); - await expect(mintManager.vectorMintSeries721(1, 1, owner.address)).to.be.revertedWithCustomError( + await expect(mintManager.vectorMint721(1, 1, owner.address)).to.be.revertedWithCustomError( mintManager, "OnchainVectorMintGuardFailed", ); diff --git a/test/ERC721GenerativeTest.ts b/test/ERC721GenerativeTest.ts index 0c65321..8070b35 100644 --- a/test/ERC721GenerativeTest.ts +++ b/test/ERC721GenerativeTest.ts @@ -3,7 +3,11 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { - ERC721General, + BitRotGenerative, + BitRotGenerativeTest, + BitRotGenerativeTest__factory, + BitRotGenerative__factory, + ERC721GeneralSequence, ERC721GenerativeOnchain, ERC721GenerativeOnchain__factory, FileDeployer, @@ -11,6 +15,7 @@ import { MintManager, Observability, OwnerOnlyTokenManager, + TestHighlightRenderer, TotalLockedTokenManager, } from "../types"; import { Errors } from "./__utils__/data"; @@ -19,7 +24,7 @@ import { DEFAULT_ONCHAIN_MINT_VECTOR, setupGenerative, setupSystem } from "./__u describe("ERC721Generative functionality", () => { let totalLockedTokenManager: TotalLockedTokenManager; let ownerOnlyTokenManager: OwnerOnlyTokenManager; - let generative: ERC721General; + let generative: ERC721GeneralSequence; let initialPlatformExecutor: SignerWithAddress, mintManagerOwner: SignerWithAddress, editionsMetadataOwner: SignerWithAddress, @@ -547,6 +552,7 @@ describe("ERC721Generative functionality", () => { mintManager.address, owner, { ...DEFAULT_ONCHAIN_MINT_VECTOR, maxUserClaimableViaVector: 2 }, + null, false, 0, ethers.constants.AddressZero, @@ -574,13 +580,11 @@ describe("ERC721Generative functionality", () => { DEFAULT_ONCHAIN_MINT_VECTOR.allowlistRoot, ]); - await expect( - mintManager.vectorMintSeries721(1, 2, owner.address, { value: ethers.utils.parseEther("0.0008").mul(2) }), - ) + await expect(mintManager.vectorMint721(1, 2, owner.address, { value: ethers.utils.parseEther("0.0008").mul(2) })) .to.emit(mintManager, "NumTokenMint") .withArgs(ethers.utils.hexZeroPad(ethers.utils.hexlify(1), 32), generative.address, true, 2); - await expect(mintManager.vectorMintSeries721(1, 1, owner.address)).to.be.revertedWithCustomError( + await expect(mintManager.vectorMint721(1, 1, owner.address)).to.be.revertedWithCustomError( mintManager, "OnchainVectorMintGuardFailed", ); @@ -772,4 +776,111 @@ describe("ERC721Generative functionality", () => { expect(await ocsERC721.files()).to.eql(["revertSnippet.sol", "readBytecodeSnippet2.sol"]); }); }); + + describe("Custom", function () { + let bitRotTest: BitRotGenerativeTest; + let bitRotGenerative: BitRotGenerative; + + before(async () => { + const bitRotGenerativeImpl = await (await ethers.getContractFactory("BitRotGenerative")).deploy(); + bitRotGenerative = BitRotGenerative__factory.connect( + ( + await setupGenerative( + observability.address, + bitRotGenerativeImpl.address, + trustedForwarder.address, + mintManager.address, + owner, + ) + ).address, + owner, + ); + bitRotTest = BitRotGenerativeTest__factory.connect( + (await (await ethers.getContractFactory("BitRotGenerativeTest")).deploy(bitRotGenerative.address)).address, + owner, + ); + await expect(bitRotGenerative.registerMinter(bitRotTest.address)).to.not.be.reverted; + }); + + it("BitRot test passes", async function () { + await expect(bitRotTest.test()).to.not.be.reverted; + }); + }); + + describe("Custom renderer", function () { + let testHighlightRenderer: TestHighlightRenderer; + + beforeEach(async () => { + await expect(generative.registerMinter(owner.address)).to.emit(generative, "MinterRegistrationChanged"); + const TestHighlightRenderer = await ethers.getContractFactory("TestHighlightRenderer"); + + testHighlightRenderer = await TestHighlightRenderer.deploy(); + await testHighlightRenderer.deployed(); + }); + + it("Only owner can set custom renderer config", async function () { + generative = generative.connect(fan1); + await expect( + generative.setCustomRenderer({ + renderer: testHighlightRenderer.address, + processMintDataOnRenderer: true, + }), + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); + + it("Invalid custom renderer config not allowed", async function () { + generative = generative.connect(owner); + await expect( + generative.setCustomRenderer({ + renderer: ethers.constants.AddressZero, + processMintDataOnRenderer: true, + }), + ).to.be.revertedWith("Invalid input"); + }); + + it("Custom renderer can process mint data, then re-use it on tokenURI query", async function () { + await expect( + generative.setCustomRenderer({ + renderer: testHighlightRenderer.address, + processMintDataOnRenderer: true, + }), + ).to.not.be.reverted; + + await expect(generative.mintOneToOneRecipient(owner.address)).to.not.be.reverted; + await expect(generative.mintAmountToOneRecipient(owner.address, 2)).to.not.be.reverted; + await expect(generative.mintOneToMultipleRecipients([owner.address, fan1.address])).to.not.be.reverted; + await expect(generative.mintSameAmountToMultipleRecipients([owner.address, fan1.address], 2)).to.not.be.reverted; + + const tokenIds = Array.from({ length: 9 }, (_, i) => i + 1); + const seedDetailsPerToken = await Promise.all( + tokenIds.map(async tokenId => { + const seedDetails = await testHighlightRenderer.getSeedDetails(tokenId, 10, generative.address); + expect(seedDetails.blockTimestamp.eq(0)).to.be.false; + + return { ...seedDetails, tokenId }; + }), + ); + + await expect( + generative.setCustomRenderer({ + renderer: testHighlightRenderer.address, + processMintDataOnRenderer: false, + }), + ).to.not.be.reverted; + + await Promise.all( + seedDetailsPerToken.map(async seedDetails => { + const uri = await generative.tokenURI(seedDetails.tokenId); + const predictedUri = await testHighlightRenderer.concatenateSeedDetails( + { + previousBlockHash: seedDetails.previousBlockHash, + blockTimestamp: seedDetails.blockTimestamp, + }, + seedDetails.tokenId, + ); + expect(uri).to.equal(predictedUri); + }), + ); + }); + }); }); diff --git a/test/ERC721SingleEditionDFSTest.ts b/test/ERC721SingleEditionDFSTest.ts index 45e75c3..46149c1 100644 --- a/test/ERC721SingleEditionDFSTest.ts +++ b/test/ERC721SingleEditionDFSTest.ts @@ -534,14 +534,14 @@ describe("ERC721SingleEdition functionality", () => { ]); await expect( - mintManager.vectorMintEdition721(1, 2, editionsOwner.address, { + mintManager.vectorMint721(1, 2, editionsOwner.address, { value: ethers.utils.parseEther("0.0008").mul(2), }), ) .to.emit(mintManager, "NumTokenMint") .withArgs(ethers.utils.hexZeroPad(ethers.utils.hexlify(1), 32), editions.address, true, 2); - await expect(mintManager.vectorMintEdition721(1, 1, editionsOwner.address)).to.be.revertedWithCustomError( + await expect(mintManager.vectorMint721(1, 1, editionsOwner.address)).to.be.revertedWithCustomError( mintManager, "OnchainVectorMintGuardFailed", ); diff --git a/test/ERC721SingleEditionTest.ts b/test/ERC721SingleEditionTest.ts index bbf691c..547b328 100644 --- a/test/ERC721SingleEditionTest.ts +++ b/test/ERC721SingleEditionTest.ts @@ -539,14 +539,14 @@ describe("ERC721SingleEdition functionality", () => { ]); await expect( - mintManager.vectorMintEdition721(1, 2, editionsOwner.address, { + mintManager.vectorMint721(1, 2, editionsOwner.address, { value: ethers.utils.parseEther("0.0008").mul(2), }), ) .to.emit(mintManager, "NumTokenMint") .withArgs(ethers.utils.hexZeroPad(ethers.utils.hexlify(1), 32), editions.address, true, 2); - await expect(mintManager.vectorMintEdition721(1, 1, editionsOwner.address)).to.be.revertedWithCustomError( + await expect(mintManager.vectorMint721(1, 1, editionsOwner.address)).to.be.revertedWithCustomError( mintManager, "OnchainVectorMintGuardFailed", ); diff --git a/test/ERC721StandardTest.ts b/test/ERC721StandardTest.ts index 886034b..5e6edea 100644 --- a/test/ERC721StandardTest.ts +++ b/test/ERC721StandardTest.ts @@ -1172,6 +1172,7 @@ describe("ERC721 Standard with token managers functionality", () => { "name", "SYM", null, + null, false, nonTransferableTokenManager.address, ); @@ -1223,6 +1224,7 @@ describe("ERC721 Standard with token managers functionality", () => { "name", "SYM", null, + null, false, consensualNonTransferableTokenManager.address, ); diff --git a/test/EditionsMetadataRendererTest.ts b/test/EditionsMetadataRendererTest.ts index 635711f..e598950 100644 --- a/test/EditionsMetadataRendererTest.ts +++ b/test/EditionsMetadataRendererTest.ts @@ -89,6 +89,8 @@ describe("Editions Metadata Renderer", () => { emr.address, generalOwner, null, + null, + false, false, 0, ethers.constants.AddressZero, diff --git a/test/MechanicMintVectorsTest.ts b/test/MechanicMintVectorsTest.ts new file mode 100644 index 0000000..0131ffa --- /dev/null +++ b/test/MechanicMintVectorsTest.ts @@ -0,0 +1,1139 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { + AuctionManager, + DiscreteDutchAuctionMechanic, + ERC721EditionsDFS, + ERC721General, + ERC721Generative, + ERC721SingleEditionDFS, + MinimalForwarder, + MintManager, + Observability, +} from "../types"; +import { SAMPLE_DA_VECTOR } from "./__utils__/data"; +import { Errors } from "./__utils__/data"; +import { + dutchAuctionUpdateArgs, + encodeDAVectorData, + encodeMechanicVectorData, + produceMechanicVectorId, + setupGeneral, + setupGenerative, + setupMultipleEditionDFS, + setupSingleEditionDFS, + setupSystem, +} from "./__utils__/helpers"; + +describe("Mechanic mint vectors", () => { + let initialPlatformExecutor: SignerWithAddress, + mintManagerOwner: SignerWithAddress, + editionsMetadataOwner: SignerWithAddress, + platformPaymentAddress: SignerWithAddress, + editionsOwner: SignerWithAddress, + generalOwner: SignerWithAddress, + fan1: SignerWithAddress; + + let observability: Observability; + let mintManager: MintManager; + let auctionManager: AuctionManager; + let trustedForwarder: MinimalForwarder; + let dutchAuction: DiscreteDutchAuctionMechanic; + let editionsDFSImplementation: string; + let singleEditionDFSImplementation: string; + let generalImplementation: string; + let generativeImplementation: string; + + let generative: ERC721Generative; + let editions: ERC721EditionsDFS; + let singleEdition: ERC721SingleEditionDFS; + let general: ERC721General; + + let generativeVectorId: string; + let editionsVectorId: string; + let singleEditionVectorId: string; + let generalVectorId: string; + + const prices1 = ["0.001", "0.0001"]; + const prices2 = ["100", "0.189", "0.09", "0.08", "0.07", "0.06", "0.05", "0.00001"]; + const prices3 = ["0.00000000001", "0.0000000000000001"]; + const prices4: string[] = []; + + const mintFeeWei = ethers.BigNumber.from("800000000000000"); + + before(async () => { + [ + initialPlatformExecutor, + mintManagerOwner, + editionsMetadataOwner, + platformPaymentAddress, + editionsOwner, + generalOwner, + fan1, + ] = await ethers.getSigners(); + + const { + mintManagerProxy, + minimalForwarder, + auctionManagerProxy, + observability: observabilityInstance, + editionsDFSImplementationAddress, + singleEditionDFSImplementationAddress, + generalImplementationAddress, + generativeImplementationAddress, + daMechanic, + } = await setupSystem( + platformPaymentAddress.address, + mintManagerOwner.address, + initialPlatformExecutor.address, + editionsMetadataOwner.address, + mintManagerOwner, + ); + mintManager = mintManagerProxy; + trustedForwarder = minimalForwarder; + auctionManager = auctionManagerProxy; + observability = observabilityInstance; + editionsDFSImplementation = editionsDFSImplementationAddress; + singleEditionDFSImplementation = singleEditionDFSImplementationAddress; + generalImplementation = generalImplementationAddress; + generativeImplementation = generativeImplementationAddress; + dutchAuction = daMechanic; + }); + + // in this, validate that contract deployments with mechanic vector registration works + beforeEach(async function () { + for (let i = 0; i < 30; i++) { + prices4[i] = (1 - i * ((1 - 0.08) / 30)).toString(); + } + + const vector1 = SAMPLE_DA_VECTOR(dutchAuction.address, {}); + const vector2 = SAMPLE_DA_VECTOR(dutchAuction.address, { prices: prices2 }); + const vector3 = SAMPLE_DA_VECTOR(dutchAuction.address, { prices: prices3 }); + const vector4 = SAMPLE_DA_VECTOR(dutchAuction.address, { prices: prices4, periodDuration: 10000 }); + + singleEdition = await setupSingleEditionDFS( + observability.address, + singleEditionDFSImplementation, + mintManager.address, + trustedForwarder.address, + editionsOwner, + 5, + "", + "NM", + null, + vector1, + ); + + editions = await setupMultipleEditionDFS( + observability.address, + editionsDFSImplementation, + mintManager.address, + auctionManager.address, + trustedForwarder.address, + editionsOwner, + 100, + "symbol", + null, + vector2, + ); + + general = await setupGeneral( + observability.address, + generalImplementation, + trustedForwarder.address, + mintManager.address, + generalOwner, + null, + vector3, + true, + ); + + generative = await setupGenerative( + observability.address, + generativeImplementation, + trustedForwarder.address, + mintManager.address, + generalOwner, + null, + vector4, + ); + + singleEditionVectorId = produceMechanicVectorId( + singleEdition.address, + dutchAuction.address, + parseInt(vector1.seed), + 0, + ); + editionsVectorId = produceMechanicVectorId(editions.address, dutchAuction.address, parseInt(vector2.seed), 0); + generalVectorId = produceMechanicVectorId(general.address, dutchAuction.address, parseInt(vector3.seed)); + generativeVectorId = produceMechanicVectorId(generative.address, dutchAuction.address, parseInt(vector4.seed)); + + const vectorMeta1 = await mintManager.mechanicVectorMetadata(singleEditionVectorId); + const vectorMeta2 = await mintManager.mechanicVectorMetadata(editionsVectorId); + const vectorMeta3 = await mintManager.mechanicVectorMetadata(generalVectorId); + const vectorMeta4 = await mintManager.mechanicVectorMetadata(generativeVectorId); + expect(ethers.utils.getAddress(vectorMeta1.contractAddress)).to.equal( + ethers.utils.getAddress(singleEdition.address), + ); + expect(ethers.utils.getAddress(vectorMeta2.contractAddress)).to.equal(ethers.utils.getAddress(editions.address)); + expect(ethers.utils.getAddress(vectorMeta3.contractAddress)).to.equal(ethers.utils.getAddress(general.address)); + expect(ethers.utils.getAddress(vectorMeta4.contractAddress)).to.equal(ethers.utils.getAddress(generative.address)); + + const daState1 = await dutchAuction.getVectorState(singleEditionVectorId); + const daState2 = await dutchAuction.getVectorState(editionsVectorId); + const daState3 = await dutchAuction.getVectorState(generalVectorId); + const daState4 = await dutchAuction.getVectorState(generativeVectorId); + + expect(daState1._vector.numPrices.toString()).to.equal(prices1.length.toString()); + expect( + daState1.prices.map(price => { + return parseFloat(ethers.utils.formatEther(price)); + }), + ).to.eql( + prices1.map(price => { + return parseFloat(price); + }), + ); + expect(daState2._vector.numPrices.toString()).to.equal(prices2.length.toString()); + expect( + daState2.prices.map(price => { + return parseFloat(ethers.utils.formatEther(price)); + }), + ).to.eql( + prices2.map(price => { + return parseFloat(price); + }), + ); + expect(daState3._vector.numPrices.toString()).to.equal(prices3.length.toString()); + expect( + daState3.prices.map(price => { + return parseFloat(ethers.utils.formatEther(price)); + }), + ).to.eql( + prices3.map(price => { + return parseFloat(price); + }), + ); + expect(daState4._vector.numPrices.toString()).to.equal(prices4.length.toString()); + expect( + daState4.prices.map(price => { + return parseFloat(ethers.utils.formatEther(price)); + }), + ).to.eql( + prices4.map(price => { + return parseFloat(price); + }), + ); + }); + + describe("Mechanic vector management", function () { + it("Only the owner of a collection can register mechanic mint vectors", async function () { + const seed = Math.floor(Date.now() / 1000); + const vectorData = encodeMechanicVectorData( + mintManager.address, + fan1.address, + SAMPLE_DA_VECTOR(dutchAuction.address, {}), + ); + mintManager = mintManager.connect(fan1); + await expect( + mintManager.registerMechanicVector( + { + contractAddress: editions.address, + editionId: 1, + isChoose: false, + paused: false, + mechanic: dutchAuction.address, + isEditionBased: true, + }, + seed, + vectorData, + ), + ).to.be.revertedWithCustomError(mintManager, Errors.Unauthorized); + + mintManager = mintManager.connect(generalOwner); + }); + + it("Only the owner can pause/unpause mechanic mint vectors, which cause the mints to be paused/unpaused", async function () { + // do with both mechanicMintNum and mechanicMintChoose + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 2, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther(prices4[0])).mul(2), + }), + ) + .to.emit(mintManager, "NumTokenMint") + .withArgs(generativeVectorId, generative.address, true, 2); + await expect( + mintManager.mechanicMintChoose(generalVectorId, fan1.address, [1, 2], "0x", { + value: mintFeeWei.add(ethers.utils.parseEther(prices3[0])).mul(2), + }), + ) + .to.emit(mintManager, "ChooseTokenMint") + .withArgs(generalVectorId, general.address, true, [1, 2]); + + await expect(mintManager.setPauseOnMechanicMintVector(generativeVectorId, true)).to.be.not.reverted; + await expect(mintManager.setPauseOnMechanicMintVector(generalVectorId, true)).to.be.not.reverted; + + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 2, "0x", { + value: ethers.utils.parseEther("0.0008").mul(2), + }), + ).to.be.revertedWithCustomError(mintManager, Errors.MechanicPaused); + await expect( + mintManager.mechanicMintChoose(generalVectorId, fan1.address, [3], "0x", { + value: ethers.utils.parseEther("0.0008").mul(2), + }), + ).to.be.revertedWithCustomError(mintManager, Errors.MechanicPaused); + }); + + it("Cannot try the wrong mint style", async function () { + await expect( + mintManager.mechanicMintNum(generalVectorId, fan1.address, 2, "0x", { + value: ethers.utils.parseEther("0.0008").mul(2), + }), + ).to.be.revertedWithCustomError(mintManager, Errors.InvalidMechanic); + await expect( + mintManager.mechanicMintChoose(generativeVectorId, fan1.address, [3], "0x", { + value: ethers.utils.parseEther("0.0008").mul(2), + }), + ).to.be.revertedWithCustomError(mintManager, Errors.InvalidMechanic); + }); + + describe("Dutch auction mechanic vector management", function () { + it("Can register/create dutch auction mechanic mint vectors with different configurations", async function () { + mintManager = mintManager.connect(editionsOwner); + const editionId = 0; + const seed = 1; + + await expect( + mintManager.registerMechanicVector( + { + contractAddress: editions.address, + editionId, + isChoose: false, + paused: false, + mechanic: dutchAuction.address, + isEditionBased: true, + }, + seed, + encodeDAVectorData( + SAMPLE_DA_VECTOR(dutchAuction.address, { + prices: ["0.001", "0.0001", "0.00009"], + periodDuration: 10, + maxTotalClaimableViaVector: 20, + startTimestamp: Math.floor(Date.now() / 1000) + 1000, + endTimestamp: Math.floor(Date.now() / 1000) + 1021, // 21 sec dutch auction / 2 periods of 10 sec each + 1 last period of 1 sec + }), + editionsOwner.address, + ), + ), + ) + .to.emit(dutchAuction, "DiscreteDutchAuctionCreated") + .withArgs(produceMechanicVectorId(editions.address, dutchAuction.address, seed, editionId)); + }); + + it("Cannot register/create dutch auction mechanic mint vectors with invalid configurations", async function () { + const editionId = 0; + const seed = 1; + + await expect( + mintManager.registerMechanicVector( + { + contractAddress: editions.address, + editionId, + isChoose: false, + paused: false, + mechanic: dutchAuction.address, + isEditionBased: true, + }, + seed, + encodeDAVectorData( + SAMPLE_DA_VECTOR(dutchAuction.address, { + prices: ["0.001", "0.0001", "0.00009"], + periodDuration: 10, + maxTotalClaimableViaVector: 20, + startTimestamp: Math.floor(Date.now() / 1000) + 1000, + endTimestamp: Math.floor(Date.now() / 1000) + 1020, // invalid, no time for last period + }), + editionsOwner.address, + ), + ), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidVectorConfig); + + await expect( + mintManager.registerMechanicVector( + { + contractAddress: editions.address, + editionId, + isChoose: false, + paused: false, + mechanic: dutchAuction.address, + isEditionBased: true, + }, + seed, + encodeDAVectorData(SAMPLE_DA_VECTOR(dutchAuction.address, {}), ethers.constants.AddressZero), + ), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidVectorConfig); + + await expect( + mintManager.registerMechanicVector( + { + contractAddress: editions.address, + editionId, + isChoose: false, + paused: false, + mechanic: dutchAuction.address, + isEditionBased: true, + }, + seed, + encodeDAVectorData( + SAMPLE_DA_VECTOR(dutchAuction.address, { + periodDuration: 0, + }), + ethers.constants.AddressZero, + ), + ), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidVectorConfig); + + await expect( + mintManager.registerMechanicVector( + { + contractAddress: editions.address, + editionId, + isChoose: false, + paused: false, + mechanic: dutchAuction.address, + isEditionBased: true, + }, + seed, + encodeDAVectorData( + SAMPLE_DA_VECTOR(dutchAuction.address, { + prices: ["0.001"], + }), + ethers.constants.AddressZero, + ), + ), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidVectorConfig); + + await expect( + mintManager.registerMechanicVector( + { + contractAddress: editions.address, + editionId, + isChoose: false, + paused: false, + mechanic: dutchAuction.address, + isEditionBased: true, + }, + seed, + encodeDAVectorData( + SAMPLE_DA_VECTOR(dutchAuction.address, { + prices: ["0.001", "0.0001", "0.01"], + }), + ethers.constants.AddressZero, + ), + ), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidVectorConfig); + + await expect( + mintManager.registerMechanicVector( + { + contractAddress: editions.address, + editionId, + isChoose: false, + paused: false, + mechanic: dutchAuction.address, + isEditionBased: true, + }, + seed, + encodeDAVectorData( + SAMPLE_DA_VECTOR(dutchAuction.address, { + prices: ["0.001", "0.0001", "0.0001"], + }), + ethers.constants.AddressZero, + ), + ), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidVectorConfig); + }); + + it("Non-owner of collection cannot update dutch auction", async function () { + dutchAuction = dutchAuction.connect(fan1); + const { + dutchAuction: dutchAuction1, + updateConfig: updateConfig1, + packedPrices: packedPrices1, + } = dutchAuctionUpdateArgs({ + prices: ["0.1", "0.0001", "0.00001"], + }); + await expect( + dutchAuction.updateVector(generativeVectorId, dutchAuction1, packedPrices1, updateConfig1), + ).to.be.revertedWithCustomError(dutchAuction, Errors.Unauthorized); + }); + + it("Can update auction mechanic mint vectors with different configurations", async function () { + dutchAuction = dutchAuction.connect(generalOwner); + const { + numPrices: da1NumPrices, + bytesPerPrice: da1BytesPerPrice, + periodDuration: da1PeriodDuration, + tokenLimitPerTx: da1TokenLimitPerTx, + endTimestamp: da1EndTimestamp, + } = (await dutchAuction.getRawVector(generativeVectorId))._vector; + const { + dutchAuction: dutchAuction1, + updateConfig: updateConfig1, + packedPrices: packedPrices1, + } = dutchAuctionUpdateArgs({ + prices: ["1000", "0.0001", "0.00001"], + }); + // none of periodDuration, tokenLimitPerTx, endTimestamp should update + await expect( + dutchAuction.updateVector( + generativeVectorId, + { ...dutchAuction1, periodDuration: 5, tokenLimitPerTx: 10, endTimestamp: 100 }, + packedPrices1, + updateConfig1, + ), + ) + .to.emit(dutchAuction, "DiscreteDutchAuctionUpdated") + .withArgs(generativeVectorId); + const { + numPrices: da1NewNumPrices, + bytesPerPrice: da1NewBytesPerPrice, + periodDuration: da1NewPeriodDuration, + tokenLimitPerTx: da1NewTokenLimitPerTx, + endTimestamp: da1NewEndTimestamp, + } = (await dutchAuction.getRawVector(generativeVectorId))._vector; + const newPackedPrices = (await dutchAuction.getRawVector(generativeVectorId)).packedPrices; + expect(da1NumPrices.toString()).to.not.equal(da1NewNumPrices.toString()); + expect(da1NewNumPrices.toString()).to.equal("3"); + expect(da1BytesPerPrice.toString()).to.not.equal(da1NewBytesPerPrice.toString()); + expect(da1PeriodDuration.toString()).to.equal(da1NewPeriodDuration.toString()); + expect(da1TokenLimitPerTx.toString()).to.equal(da1NewTokenLimitPerTx.toString()); + expect(da1EndTimestamp.toString()).to.equal(da1NewEndTimestamp.toString()); + expect(packedPrices1).to.eql(newPackedPrices); + expect( + (await dutchAuction.getVectorState(generativeVectorId)).prices.map(price => { + return ethers.utils.formatEther(price); + }), + ).to.eql(["1000.0", "0.0001", "0.00001"]); + + const { + dutchAuction: dutchAuction2, + updateConfig: updateConfig2, + packedPrices: packedPrices2, + } = dutchAuctionUpdateArgs({ + startTimestamp: 10000, + endTimestamp: 20000, + periodDuration: 334, // 30 periods, so 334 x 30 = 10020, on limit + maxUserClaimableViaVector: 5, + maxTotalClaimableViaVector: 10, + tokenLimitPerTx: 5, + paymentRecipient: fan1.address, + }); + await expect(dutchAuction.updateVector(generativeVectorId, dutchAuction2, packedPrices2, updateConfig2)) + .to.emit(dutchAuction, "DiscreteDutchAuctionUpdated") + .withArgs(generativeVectorId); + const { + startTimestamp: da2NewStartTimestamp, + endTimestamp: da2NewEndTimestamp, + periodDuration: da2NewPeriodDuration, + maxUserClaimableViaVector: da2NewMaxUserClaimableViaVector, + maxTotalClaimableViaVector: da2NewMaxTotalClaimableViaVector, + tokenLimitPerTx: da2NewTokenLimitPerTx, + paymentRecipient: da2NewPaymentRecipient, + } = (await dutchAuction.getRawVector(generativeVectorId))._vector; + + expect(da2NewStartTimestamp.toString()).to.equal("10000"); + expect(da2NewEndTimestamp.toString()).to.equal("20000"); + expect(da2NewPeriodDuration.toString()).to.equal("334"); + expect(da2NewMaxUserClaimableViaVector.toString()).to.equal("5"); + expect(da2NewMaxTotalClaimableViaVector.toString()).to.equal("10"); + expect(da2NewTokenLimitPerTx.toString()).to.equal("5"); + expect(ethers.utils.getAddress(da2NewPaymentRecipient)).to.equal(ethers.utils.getAddress(fan1.address)); + }); + + it("Cannot update dutch auction to set a time range that exceeds or equals (numPrices - 1) * periodDuration", async function () { + // cannot set invalid times, given there are 30 prices + const { + dutchAuction: dutchAuction1, + updateConfig: updateConfig1, + packedPrices: packedPrices1, + } = dutchAuctionUpdateArgs({ + periodDuration: 10, + startTimestamp: 20, + endTimestamp: 310, + }); + await expect( + dutchAuction.updateVector(generativeVectorId, dutchAuction1, packedPrices1, updateConfig1), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidVectorConfig); + }); + + it("Cannot update dutch auction with non-decreasing prices", async function () { + const { + dutchAuction: dutchAuctionData, + updateConfig, + packedPrices, + } = dutchAuctionUpdateArgs({ + prices: ["0.001", "0.001"], + }); + await expect( + dutchAuction.updateVector(generativeVectorId, dutchAuctionData, packedPrices, updateConfig), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidVectorConfig); + }); + + it("Cannot update dutch auction with the payment recipient as the zero address", async function () { + const { + dutchAuction: dutchAuction3, + updateConfig: updateConfig3, + packedPrices: packedPrices3, + } = dutchAuctionUpdateArgs({ + paymentRecipient: ethers.constants.AddressZero, + }); + await expect( + dutchAuction.updateVector(generativeVectorId, dutchAuction3, packedPrices3, updateConfig3), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidVectorConfig); + }); + + it("Cannot update dutch auction to make period duration 0", async function () { + const { + dutchAuction: dutchAuction3, + updateConfig: updateConfig3, + packedPrices: packedPrices3, + } = dutchAuctionUpdateArgs({ + periodDuration: 0, + }); + await expect( + dutchAuction.updateVector(generativeVectorId, dutchAuction3, packedPrices3, updateConfig3), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidVectorConfig); + }); + + describe("Cannot update certain fields on dutch auction after first token is minted", function () { + beforeEach(async function () { + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 2, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther("1")).mul(2), + }), + ).to.emit(mintManager, "NumTokenMint"); + + const { _vector, payeePotentialEscrowedFunds, currentPrice } = await dutchAuction.getVectorState( + generativeVectorId, + ); + expect(_vector.lowestPriceSoldAtIndex).to.equal(0); + expect(_vector.currentSupply).to.equal(2); + expect(ethers.utils.formatEther(_vector.totalSales)).to.equal("2.0"); + expect(ethers.utils.formatEther(currentPrice)).to.equal("1.0"); + expect(ethers.utils.formatEther(payeePotentialEscrowedFunds)).to.equal("2.0"); + }); + + it("maxTotalClaimableViaVector", async function () { + const { + dutchAuction: dutchAuctionData, + updateConfig, + packedPrices, + } = dutchAuctionUpdateArgs({ + maxTotalClaimableViaVector: 30, + }); + await expect( + dutchAuction.updateVector(generativeVectorId, dutchAuctionData, packedPrices, updateConfig), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidUpdate); + }); + + it("prices", async function () { + const { + dutchAuction: dutchAuctionData, + updateConfig, + packedPrices, + } = dutchAuctionUpdateArgs({ + prices: ["0.008", "0.007"], + }); + await expect( + dutchAuction.updateVector(generativeVectorId, dutchAuctionData, packedPrices, updateConfig), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidUpdate); + }); + + it("periodDuration", async function () { + const { + dutchAuction: dutchAuctionData, + updateConfig, + packedPrices, + } = dutchAuctionUpdateArgs({ + periodDuration: 1001, + }); + await expect( + dutchAuction.updateVector(generativeVectorId, dutchAuctionData, packedPrices, updateConfig), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidUpdate); + }); + + it("startTimestamp", async function () { + const { + dutchAuction: dutchAuctionData, + updateConfig, + packedPrices, + } = dutchAuctionUpdateArgs({ + startTimestamp: 100, + }); + await expect( + dutchAuction.updateVector(generativeVectorId, dutchAuctionData, packedPrices, updateConfig), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidUpdate); + }); + }); + }); + }); + + describe("Dutch auctions", function () { + describe("Mints + rebates + escrow funds withdrawal (logic / state / errors)", function () { + it("Cannot send too low of a mint fee", async function () { + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 1, "0x"), + ).to.be.revertedWithCustomError(mintManager, Errors.MintFeeTooLow); + }); + + it("Cannot send too low of a fee for the auction", async function () { + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 1, "0x", { value: mintFeeWei }), + ).to.be.revertedWithCustomError(mintManager, Errors.InvalidPaymentAmount); + }); + + it("Can only mint within the time bounds of the auction", async function () { + dutchAuction = dutchAuction.connect(generalOwner); + const currTime = Math.floor(Date.now() / 1000); + const startTimestamp = currTime + 1000; + const { + dutchAuction: dutchAuctionData, + updateConfig, + packedPrices, + } = dutchAuctionUpdateArgs({ + startTimestamp, + endTimestamp: startTimestamp + 300000, + }); + await expect(dutchAuction.updateVector(generativeVectorId, dutchAuctionData, packedPrices, updateConfig)).to.not + .be.reverted; + + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 1, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther("1")), + }), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidMint); + await ethers.provider.send("evm_mine", [currTime + 200000]); + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 1, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther("1")), + }), + ).not.be.reverted; + await ethers.provider.send("evm_mine", [currTime + 400000]); + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 1, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther("1")), + }), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidMint); + + const currentPrice = (await dutchAuction.getVectorState(generativeVectorId)).currentPrice; + const userInfo = await dutchAuction.getUserInfo(generativeVectorId, fan1.address); + const rebate = userInfo[0]; + const { totalPosted } = userInfo[1]; + expect(ethers.utils.formatEther(totalPosted)).to.equal("1.0"); + expect(totalPosted.sub(currentPrice).eq(rebate)).to.equal(true); + }); + + it("Cannot mint over maxUser, maxTotal, and tokenLimitPerTx bounds", async function () { + dutchAuction = dutchAuction.connect(generalOwner); + const currTime = Math.floor(Date.now() / 1000); + const { + dutchAuction: dutchAuctionData, + updateConfig, + packedPrices, + } = dutchAuctionUpdateArgs({ + maxTotalClaimableViaVector: 10, + maxUserClaimableViaVector: 5, + tokenLimitPerTx: 3, + startTimestamp: currTime + 1000, + }); + await expect(dutchAuction.updateVector(generativeVectorId, dutchAuctionData, packedPrices, updateConfig)).to.not + .be.reverted; + + // tokenLimitPerTx + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 4, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther("1")).mul(4), + }), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidMint); + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 3, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther("1")).mul(3), + }), + ).to.not.be.reverted; + // maxUserClaimableViaVector + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 3, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther("1")).mul(3), + }), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidMint); + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 2, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther("1")).mul(2), + }), + ).to.not.be.reverted; + await expect( + mintManager.mechanicMintNum(generativeVectorId, generalOwner.address, 3, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther("1")).mul(3), + }), + ).to.not.be.reverted; + // maxTotalClaimableViaVector + await expect( + mintManager.mechanicMintNum(generativeVectorId, editionsOwner.address, 3, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther("1")).mul(3), + }), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidMint); + await expect( + mintManager.mechanicMintNum(generativeVectorId, editionsOwner.address, 2, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther("1")).mul(2), + }), + ).to.not.be.reverted; + }); + + it("State updates properly through multiple mints at different prices", async function () { + dutchAuction = dutchAuction.connect(generalOwner); + const currTime = Math.floor(Date.now() / 1000); + const { + dutchAuction: dutchAuctionData, + updateConfig, + packedPrices, + } = dutchAuctionUpdateArgs({ + maxTotalClaimableViaVector: 15, + startTimestamp: currTime + 1000000, + }); + await expect(dutchAuction.updateVector(generativeVectorId, dutchAuctionData, packedPrices, updateConfig)).to.not + .be.reverted; + + await ethers.provider.send("evm_mine", [currTime + 1000000]); + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 3, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther(prices4[0])).mul(3), + }), + ).to.not.be.reverted; + await ethers.provider.send("evm_mine", [currTime + 1010000]); + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 3, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther(prices4[1])).mul(3), + }), + ).to.not.be.reverted; + await ethers.provider.send("evm_mine", [currTime + 1030000]); + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 3, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther(prices4[3])).mul(3), + }), + ).to.not.be.reverted; + + const { _vector, currentPrice, collectionSupply, payeePotentialEscrowedFunds, escrowedFundsAmountFinalized } = + await dutchAuction.getVectorState(generativeVectorId); + expect(_vector.currentSupply).to.equal(9); + expect(ethers.utils.formatEther(currentPrice)).to.equal(prices4[3]); + expect(collectionSupply.toString()).to.equal("9"); + expect(payeePotentialEscrowedFunds.toString()).to.eql(ethers.utils.parseEther(prices4[3]).mul(9).toString()); + expect(escrowedFundsAmountFinalized).to.equal(false); + expect(_vector.totalSales.toString()).to.equal( + ethers.utils + .parseEther(prices4[0]) + .add(ethers.utils.parseEther(prices4[1])) + .add(ethers.utils.parseEther(prices4[3])) + .mul(3) + .toString(), + ); + await ethers.provider.send("evm_mine", [currTime + 1040000]); + await expect( + mintManager.mechanicMintNum(generativeVectorId, generalOwner.address, 5, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther(prices4[4])).mul(5), + }), + ).to.not.be.reverted; + + expect((await dutchAuction.getVectorState(generativeVectorId))._vector.lowestPriceSoldAtIndex).to.equal(4); + + await ethers.provider.send("evm_mine", [currTime + 1090000]); + + await expect( + mintManager.mechanicMintNum(generativeVectorId, generalOwner.address, 1, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther(prices4[9])).mul(1), + }), + ).to.not.be.reverted; + + expect((await dutchAuction.getVectorState(generativeVectorId))._vector.lowestPriceSoldAtIndex).to.equal(9); + + const contractBalance = await ethers.provider.getBalance(dutchAuction.address); + const collectorBalance = await ethers.provider.getBalance(generalOwner.address); + const userInfo = await dutchAuction.getUserInfo(generativeVectorId, generalOwner.address); + expect(userInfo[0].toString()).to.equal( + ethers.utils.parseEther(prices4[4]).sub(ethers.utils.parseEther(prices4[9])).mul(5).toString(), + ); + const generalOwnerTotalPosted = ethers.utils + .parseEther(prices4[4]) + .mul(5) + .add(ethers.utils.parseEther(prices4[9])); + expect(userInfo[1].totalPosted.toString()).to.equal(generalOwnerTotalPosted.toString()); + + // collect rebate + await expect(dutchAuction.rebateCollector(generativeVectorId, generalOwner.address)) + .to.emit(dutchAuction, "DiscreteDutchAuctionCollectorRebate") + .withArgs( + generativeVectorId, + generalOwner.address, + userInfo[0], + ( + await dutchAuction.getVectorState(generativeVectorId) + ).currentPrice, + ); + + // validate 2 balances difference + expect((await ethers.provider.getBalance(dutchAuction.address)).eq(contractBalance.sub(userInfo[0]))).to.equal( + true, + ); + // over 90% of the rebate (consider ether lost to gas) + const newCollectorBalance = await ethers.provider.getBalance(generalOwner.address); + expect(newCollectorBalance.lt(collectorBalance.add(userInfo[0]))).to.equal(true); + expect(newCollectorBalance.gt(collectorBalance.add(userInfo[0]).mul(9).div(10))).to.equal(true); + + const state = await dutchAuction.getVectorState(generativeVectorId); + const [rebateGeneralOwner, newUserInfoGeneralOwner] = await dutchAuction.getUserInfo( + generativeVectorId, + generalOwner.address, + ); + const [rebateFan1, newUserInfoFan1] = await dutchAuction.getUserInfo(generativeVectorId, fan1.address); + expect(state.escrowedFundsAmountFinalized).to.equal(true); + expect(state.payeePotentialEscrowedFunds.toString()).to.equal( + ethers.utils.parseEther(prices4[9]).mul(15).toString(), + ); + expect(rebateGeneralOwner.eq(0)).to.equal(true); + expect(newUserInfoGeneralOwner.totalPosted.eq(generalOwnerTotalPosted.sub(userInfo[0]))).to.equal(true); // new totalPosted = old totalPosted - rebate paid out + expect(newUserInfoGeneralOwner.numRebates).to.equal(1); + expect(newUserInfoGeneralOwner.numTokensBought).to.equal(6); + const fan1TotalPosted = ethers.utils + .parseEther(prices4[0]) + .add(ethers.utils.parseEther(prices4[1])) + .add(ethers.utils.parseEther(prices4[3])) + .mul(3); + expect(newUserInfoFan1.totalPosted.eq(fan1TotalPosted)).to.equal(true); + expect(rebateFan1.eq(fan1TotalPosted.sub(ethers.utils.parseEther(prices4[9]).mul(9)))).to.equal(true); + expect(newUserInfoFan1.numRebates).to.equal(0); + expect(newUserInfoFan1.numTokensBought).to.equal(9); + + expect(state._vector.auctionExhausted).to.equal(true); + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 1, "0x", { + value: mintFeeWei.add(state.currentPrice), + }), + ).to.be.revertedWithCustomError(dutchAuction, Errors.InvalidMint); + }); + + it("Underlying collection is exhausted and we validly withdraws escrowed funds", async function () { + dutchAuction = dutchAuction.connect(generalOwner); + const currTime = Math.floor(Date.now() / 1000); + generative = generative.connect(generalOwner); + await expect(generative.setLimitSupply(1)).to.not.be.reverted; + + const { + dutchAuction: dutchAuctionData, + updateConfig, + packedPrices, + } = dutchAuctionUpdateArgs({ + maxTotalClaimableViaVector: 15, + startTimestamp: currTime + 2000000, + }); + await expect(dutchAuction.updateVector(generativeVectorId, dutchAuctionData, packedPrices, updateConfig)).to.not + .be.reverted; + + await ethers.provider.send("evm_mine", [currTime + 2000000]); + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 1, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther(prices4[0])), + }), + ).to.not.be.reverted; + + // cannot trigger a rebate if user isn't eligible + expect((await dutchAuction.getUserInfo(generativeVectorId, fan1.address))[0].eq(0)).to.equal(true); + await expect(dutchAuction.rebateCollector(generativeVectorId, fan1.address)).to.be.revertedWithCustomError( + dutchAuction, + Errors.CollectorNotOwedRebate, + ); + + const state = await dutchAuction.getVectorState(generativeVectorId); + expect(state.auctionExhausted).to.equal(true); + expect(state.escrowedFundsAmountFinalized).to.equal(true); + expect(state.payeePotentialEscrowedFunds.eq(ethers.utils.parseEther("1"))).to.equal(true); + expect(state._vector.lowestPriceSoldAtIndex).eq(0); + + dutchAuction = dutchAuction.connect(fan1); + const contractBalance = await ethers.provider.getBalance(dutchAuction.address); + const payeeBalance = await ethers.provider.getBalance(state._vector.paymentRecipient); + await expect(dutchAuction.withdrawDPPFunds(generativeVectorId)) + .to.emit(dutchAuction, "DiscreteDutchAuctionDPPFundsWithdrawn") + .withArgs(generativeVectorId, state._vector.paymentRecipient, ethers.utils.parseEther("1.0"), 1); + expect( + (await ethers.provider.getBalance(dutchAuction.address)).eq( + contractBalance.sub(ethers.utils.parseEther("1")), + ), + ).to.equal(true); + expect( + (await ethers.provider.getBalance(state._vector.paymentRecipient)).eq( + payeeBalance.add(ethers.utils.parseEther("1")), + ), + ).to.equal(true); + + // cannot re-trigger a withdrawal + expect((await dutchAuction.getVectorState(generativeVectorId))._vector.payeeRevenueHasBeenWithdrawn).to.equal( + true, + ); + await expect(dutchAuction.withdrawDPPFunds(generativeVectorId)).to.be.revertedWithCustomError( + dutchAuction, + Errors.InvalidDPPFundsWithdrawl, + ); + }); + + it("Cannot trigger a rebate for a vector with no tokens minted through it", async function () { + await expect(dutchAuction.rebateCollector(generativeVectorId, fan1.address)).to.be.revertedWithCustomError( + dutchAuction, + Errors.InvalidRebate, + ); + }); + + it("Cannot trigger a withdrawal when no tokens have been minted through the vector", async function () { + await expect(dutchAuction.withdrawDPPFunds(generativeVectorId)).to.be.revertedWithCustomError( + dutchAuction, + Errors.InvalidDPPFundsWithdrawl, + ); + }); + + it("Cannot trigger a withdrawal if an auction isn't exhausted or in the FPP", async function () { + dutchAuction = dutchAuction.connect(generalOwner); + const currTime = Math.floor(Date.now() / 1000); + + const { + dutchAuction: dutchAuctionData, + updateConfig, + packedPrices, + } = dutchAuctionUpdateArgs({ + maxTotalClaimableViaVector: 15, + startTimestamp: currTime + 3000000, + }); + await expect(dutchAuction.updateVector(generativeVectorId, dutchAuctionData, packedPrices, updateConfig)).to.not + .be.reverted; + await ethers.provider.send("evm_mine", [currTime + 3000000]); + + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 1, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther(prices4[0])), + }), + ).to.not.be.reverted; + + await expect(dutchAuction.withdrawDPPFunds(generativeVectorId)).to.be.revertedWithCustomError( + dutchAuction, + Errors.InvalidDPPFundsWithdrawl, + ); + }); + + it("Keep rebating as price drops, down to FPP (with excess amounts sent), withdraw funds, then payments go straight to payee", async function () { + dutchAuction = dutchAuction.connect(generalOwner); + const currTime = Math.floor(Date.now() / 1000); + + const prices = ["1", "0.8", "0.6", "0.4"]; + const { + dutchAuction: dutchAuctionData, + updateConfig, + packedPrices, + } = dutchAuctionUpdateArgs({ + maxTotalClaimableViaVector: 4, + startTimestamp: currTime + 4000000, + prices, + }); + await expect(dutchAuction.updateVector(generativeVectorId, dutchAuctionData, packedPrices, updateConfig)).to.not + .be.reverted; + await ethers.provider.send("evm_mine", [currTime + 4000000]); + + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 1, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther(prices[0])).add(ethers.utils.parseEther("0.5")), + }), + ).to.not.be.reverted; + await ethers.provider.send("evm_mine", [currTime + 4010000]); + await expect(dutchAuction.rebateCollector(generativeVectorId, fan1.address)) + .to.emit(dutchAuction, "DiscreteDutchAuctionCollectorRebate") + .withArgs( + generativeVectorId, + fan1.address, + ethers.utils.parseEther("0.7"), + ethers.utils.parseEther(prices[1]), + ); + expect((await dutchAuction.getUserInfo(generativeVectorId, fan1.address))[0].eq(0)).to.equal(true); + + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 1, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther(prices[1])).add(ethers.utils.parseEther("0.5")), + }), + ).to.not.be.reverted; + await ethers.provider.send("evm_mine", [currTime + 4020000]); + await expect(dutchAuction.rebateCollector(generativeVectorId, fan1.address)) + .to.emit(dutchAuction, "DiscreteDutchAuctionCollectorRebate") + .withArgs( + generativeVectorId, + fan1.address, + ethers.utils.parseEther("0.9"), + ethers.utils.parseEther(prices[2]), + ); + expect((await dutchAuction.getUserInfo(generativeVectorId, fan1.address))[0].eq(0)).to.equal(true); + + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 1, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther(prices[2])).add(ethers.utils.parseEther("0.5")), + }), + ).to.not.be.reverted; + await ethers.provider.send("evm_mine", [currTime + 4030000]); + await expect(dutchAuction.rebateCollector(generativeVectorId, fan1.address)) + .to.emit(dutchAuction, "DiscreteDutchAuctionCollectorRebate") + .withArgs( + generativeVectorId, + fan1.address, + ethers.utils.parseEther("1.1"), + ethers.utils.parseEther(prices[3]), + ); + expect((await dutchAuction.getUserInfo(generativeVectorId, fan1.address))[0].eq(0)).to.equal(true); + + const { _vector, auctionInFPP, auctionExhausted, escrowedFundsAmountFinalized, payeePotentialEscrowedFunds } = + await dutchAuction.getVectorState(generativeVectorId); + expect(_vector.currentSupply).to.equal(3); + expect(escrowedFundsAmountFinalized).to.equal(true); + expect(auctionExhausted).to.equal(false); + expect(auctionInFPP).to.equal(true); + expect(payeePotentialEscrowedFunds.eq(ethers.utils.parseEther("0.4").mul(3))); + + dutchAuction = dutchAuction.connect(fan1); + mintManager = mintManager.connect(fan1); + const payeeBalance = await ethers.provider.getBalance(_vector.paymentRecipient); + await expect(dutchAuction.withdrawDPPFunds(generativeVectorId)) + .to.emit(dutchAuction, "DiscreteDutchAuctionDPPFundsWithdrawn") + .withArgs(generativeVectorId, _vector.paymentRecipient, ethers.utils.parseEther("0.4"), 3); + const intermediaryPayeeBalance = await ethers.provider.getBalance(_vector.paymentRecipient); + expect(intermediaryPayeeBalance.sub(payeePotentialEscrowedFunds).eq(payeeBalance)); + + // payments now go straight to payee + await expect( + mintManager.mechanicMintNum(generativeVectorId, fan1.address, 1, "0x", { + value: mintFeeWei.add(ethers.utils.parseEther(prices[3])).add(ethers.utils.parseEther("0.5")), + }), + ).to.not.be.reverted; + expect( + (await ethers.provider.getBalance(_vector.paymentRecipient)).eq( + intermediaryPayeeBalance.add(ethers.utils.parseEther("0.4")), + ), + ); + + // can still collect rebate from overpay in FPP + expect( + (await dutchAuction.getUserInfo(generativeVectorId, fan1.address))[0].eq(ethers.utils.parseEther("0.5")), + ); + await expect(dutchAuction.rebateCollector(generativeVectorId, fan1.address)) + .to.emit(dutchAuction, "DiscreteDutchAuctionCollectorRebate") + .withArgs(generativeVectorId, fan1.address, ethers.utils.parseEther("0.5"), ethers.utils.parseEther("0.4")); + }); + }); + }); +}); diff --git a/test/MintManagerTest.ts b/test/MintManagerTest.ts index 8c2c885..84b6a69 100644 --- a/test/MintManagerTest.ts +++ b/test/MintManagerTest.ts @@ -1,14 +1,14 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { expect } from "chai"; import { ethers } from "hardhat"; -import keccak256 from "keccak256"; -import { MerkleTree } from "merkletreejs"; import { AuctionManager, ERC721Editions, + ERC721EditionsDFS, ERC721General, ERC721GeneralSequence, + ERC721GeneralSequence__factory, ERC721Generative, ERC721SingleEdition, ERC721SingleEditionDFS, @@ -21,10 +21,10 @@ import { import { SAMPLE_ABRIDGED_VECTOR, SAMPLE_ABRIDGED_VECTOR_UPDATE_CONFIG } from "./__utils__/data"; import { Errors } from "./__utils__/data"; import { - DEFAULT_ONCHAIN_MINT_VECTOR, generateClaim, generateClaimWithMetaTxPackets, generateSeriesClaim, + setupEditionsDFS, setupGeneral, setupGenerative, setupMultipleEdition, @@ -34,9 +34,6 @@ import { } from "./__utils__/helpers"; import { getExpiredClaimTimestamp, getValidClaimTimestamp } from "./__utils__/mint"; -//TODO: Gated MetaTx Tests -//TODO: Variations of Vector Mint - describe("Mint Manager", () => { let initialPlatformExecutor: SignerWithAddress, additionalPlatformExecutor: SignerWithAddress, @@ -60,6 +57,7 @@ describe("Mint Manager", () => { let generalImplementation: string; let generalSequenceImplementation: string; let generativeImplementation: string; + let editionsDFSImplementation: string; const mintFeeWei = ethers.BigNumber.from("800000000000000"); @@ -92,6 +90,7 @@ describe("Mint Manager", () => { generalSequenceImplementationAddress, generativeImplementationAddress, singleEditionDFSImplementationAddress, + editionsDFSImplementationAddress, } = await setupSystem( platformPaymentAddress.address, mintManagerOwner.address, @@ -105,6 +104,7 @@ describe("Mint Manager", () => { observability = observabilityInstance; emr = emrProxy; editionsImplementation = editionsImplementationAddress; + editionsDFSImplementation = editionsDFSImplementationAddress; singleEditionImplementation = singleEditionImplementationAddress; generalImplementation = generalImplementationAddress; generalSequenceImplementation = generalSequenceImplementationAddress; @@ -113,13 +113,11 @@ describe("Mint Manager", () => { const mintManagerOwnerBased = mintManager.connect(mintManagerOwner); - await expect(mintManagerOwnerBased.addPlatformExecutor(additionalPlatformExecutor.address)).to.emit( - mintManagerOwnerBased, - "PlatformExecutorChanged", - ); - expect(await mintManagerOwnerBased.platformExecutors()).to.include(additionalPlatformExecutor.address); + await expect(mintManagerOwnerBased.addOrDeprecatePlatformExecutor(additionalPlatformExecutor.address)).to.not.be + .reverted; + expect(await mintManagerOwnerBased.isPlatformExecutor(additionalPlatformExecutor.address)).to.be.true; }); - it("Should be able deprecate platform executor as Owner", async () => { + it("Should be able to deprecate platform executor as Owner", async () => { const { emrProxy, mintManagerProxy, @@ -146,18 +144,12 @@ describe("Mint Manager", () => { generalImplementation = generalImplementationAddress; //Add platform executor - await expect(mintManager.addPlatformExecutor(additionalPlatformExecutor.address)).to.emit( - mintManager, - "PlatformExecutorChanged", - ); - expect(await mintManager.platformExecutors()).to.include(additionalPlatformExecutor.address); + await expect(mintManager.addOrDeprecatePlatformExecutor(additionalPlatformExecutor.address)).to.not.be.reverted; + expect(await mintManager.isPlatformExecutor(additionalPlatformExecutor.address)).to.be.true; //deprecate platform executor - await expect(mintManager.deprecatePlatformExecutor(additionalPlatformExecutor.address)).to.emit( - mintManager, - "PlatformExecutorChanged", - ); - expect(await mintManager.platformExecutors()).to.not.include(additionalPlatformExecutor.address); + await expect(mintManager.addOrDeprecatePlatformExecutor(additionalPlatformExecutor.address)).to.not.be.reverted; + expect(await mintManager.isPlatformExecutor(additionalPlatformExecutor.address)).to.be.false; }); it("Should not be able to add Zero address as platform executor", async () => { const { @@ -184,11 +176,10 @@ describe("Mint Manager", () => { editionsImplementation = editionsImplementationAddress; singleEditionImplementation = singleEditionImplementationAddress; generalImplementation = generalImplementationAddress; - await expect(mintManager.addPlatformExecutor(ethers.constants.AddressZero)).to.be.revertedWithCustomError( - mintManager, - Errors.InvalidExecutorChanged, - ); - expect(await mintManager.platformExecutors()).to.not.include(ethers.constants.AddressZero); + await expect( + mintManager.addOrDeprecatePlatformExecutor(ethers.constants.AddressZero), + ).to.be.revertedWithCustomError(mintManager, Errors.InvalidExecutorChanged); + expect(await mintManager.isPlatformExecutor(ethers.constants.AddressZero)).to.be.false; }); it("Should not be able to add a platform executor that already exists", async () => { const { @@ -213,15 +204,8 @@ describe("Mint Manager", () => { editionsImplementation = editionsImplementationAddress; singleEditionImplementation = singleEditionImplementationAddress; generalImplementation = generalImplementationAddress; - await expect(mintManager.addPlatformExecutor(additionalPlatformExecutor.address)).to.emit( - mintManager, - "PlatformExecutorChanged", - ); - expect(await mintManager.platformExecutors()).to.include(additionalPlatformExecutor.address); - await expect(mintManager.addPlatformExecutor(additionalPlatformExecutor.address)).to.be.revertedWithCustomError( - mintManager, - Errors.InvalidExecutorChanged, - ); + await expect(mintManager.addOrDeprecatePlatformExecutor(additionalPlatformExecutor.address)).to.not.be.reverted; + expect(await mintManager.isPlatformExecutor(additionalPlatformExecutor.address)).to.be.true; }); it("Should reject all platform executor changes from non owner", async () => { const { @@ -248,17 +232,11 @@ describe("Mint Manager", () => { generalImplementation = generalImplementationAddress; const mintManagerForFan1 = await mintManager.connect(fan1); - //Add platform executor - await expect(mintManagerForFan1.addPlatformExecutor(additionalPlatformExecutor.address)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - expect(await mintManager.platformExecutors()).to.not.include(additionalPlatformExecutor.address); - - //deprecate platform executor - await expect(mintManagerForFan1.deprecatePlatformExecutor(initialPlatformExecutor.address)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - expect(await mintManager.platformExecutors()).to.include(initialPlatformExecutor.address); + //Add/deprecate + await expect( + mintManagerForFan1.addOrDeprecatePlatformExecutor(additionalPlatformExecutor.address), + ).to.be.revertedWith("Ownable: caller is not the owner"); + expect(await mintManager.isPlatformExecutor(additionalPlatformExecutor.address)).be.false; }); }); @@ -500,6 +478,8 @@ describe("Mint Manager", () => { mintManager.address, generalOwner, null, + null, + false, false, 0, ethers.constants.AddressZero, @@ -687,6 +667,8 @@ describe("Mint Manager", () => { mintManager.address, generalOwner, null, + null, + false, false, 10, ethers.constants.AddressZero, @@ -712,7 +694,7 @@ describe("Mint Manager", () => { offChainVectorId, claimNonce, ); - expect(await mintManager.verifySeriesClaim(claim, signature, fan1.address, [1])).to.be.true; + // expect(await mintManager.verifySeriesClaim(claim, signature, fan1.address, [1])).to.be.true; const mintManagerForFan1 = mintManager.connect(fan1); await expect( mintManagerForFan1.gatedSeriesMintChooseToken(claim, signature, fan1.address, [1]), @@ -775,7 +757,7 @@ describe("Mint Manager", () => { claimNonce, ); const mintManagerForFan1 = mintManager.connect(fan1); - expect(await mintManagerForFan1.verifySeriesClaim(claim, signature, fan1.address, [4, 2])).to.be.false; + // expect(await mintManagerForFan1.verifySeriesClaim(claim, signature, fan1.address, [4, 2])).to.be.false; await expect( mintManagerForFan1.gatedSeriesMintChooseToken(claim, signature, fan1.address, [4, 2], { value: mintFeeWei.mul(2), @@ -783,6 +765,7 @@ describe("Mint Manager", () => { ).to.be.revertedWith("ERC721: token minted"); }); + /* it("Invalid claim signer should fail", async function () { const claimNonce = "gatedMintGeneral721ClaimNonce4"; const { signature, claim } = await generateSeriesClaim( @@ -800,7 +783,7 @@ describe("Mint Manager", () => { claimNonce, ); const mintManagerForFan1 = mintManager.connect(fan1); - expect(await mintManagerForFan1.verifySeriesClaim(claim, signature, fan1.address, [4])).to.be.false; + // expect(await mintManagerForFan1.verifySeriesClaim(claim, signature, fan1.address, [4])).to.be.false; }); it("Hitting the max per user limit should fail", async function () { @@ -820,7 +803,7 @@ describe("Mint Manager", () => { claimNonce, ); const mintManagerForFan1 = mintManager.connect(fan1); - expect(await mintManagerForFan1.verifySeriesClaim(claim, signature, fan1.address, [4, 5])).to.be.false; + // expect(await mintManagerForFan1.verifySeriesClaim(claim, signature, fan1.address, [4, 5])).to.be.false; }); it("Hitting the max per vector limit should fail", async function () { @@ -840,7 +823,7 @@ describe("Mint Manager", () => { claimNonce, ); const mintManagerForFan1 = mintManager.connect(fan1); - expect(await mintManagerForFan1.verifySeriesClaim(claim, signature, fan1.address, [4, 5])).to.be.false; + // expect(await mintManagerForFan1.verifySeriesClaim(claim, signature, fan1.address, [4, 5])).to.be.false; }); it("Expired claim should fail", async function () { @@ -860,7 +843,7 @@ describe("Mint Manager", () => { claimNonce, ); const mintManagerForFan1 = mintManager.connect(fan1); - expect(await mintManagerForFan1.verifySeriesClaim(claim, signature, fan1.address, [4])).to.be.false; + // expect(await mintManagerForFan1.verifySeriesClaim(claim, signature, fan1.address, [4])).to.be.false; }); it("Claim with taken nonce should fail", async function () { @@ -880,7 +863,7 @@ describe("Mint Manager", () => { claimNonce, ); const mintManagerForFan1 = mintManager.connect(fan1); - expect(await mintManagerForFan1.verifySeriesClaim(claim, signature, fan1.address, [4])).to.be.false; + // expect(await mintManagerForFan1.verifySeriesClaim(claim, signature, fan1.address, [4])).to.be.false; }); it("Cannot mint more tokens than the maxPerTxn", async function () { const claimNonce = "gatedMintGeneral721ClaimNonce9"; @@ -899,8 +882,9 @@ describe("Mint Manager", () => { claimNonce, ); const mintManagerForFan1 = mintManager.connect(fan1); - expect(await mintManagerForFan1.verifySeriesClaim(claim, signature, fan1.address, [4])).to.be.false; + // expect(await mintManagerForFan1.verifySeriesClaim(claim, signature, fan1.address, [4])).to.be.false; }); + */ }); describe("Edition721", function () { describe("Single Edition", function () { @@ -1723,17 +1707,21 @@ describe("Mint Manager", () => { it("Should be able to update vector for contract by Owner", async () => { const mintManagerForEditionOwner = await mintManager.connect(editionsOwner); const vector = SAMPLE_ABRIDGED_VECTOR(singleEdition.address, editionsOwner.address, true, 0, 100); - const vectorUpdateConfig = SAMPLE_ABRIDGED_VECTOR_UPDATE_CONFIG({ updateMaxTotalClaimableViaVector: true }); + const vectorUpdateConfig = SAMPLE_ABRIDGED_VECTOR_UPDATE_CONFIG({ + updateMaxTotalClaimableViaVector: true, + updateTokenLimitPerTx: true, + }); await (await mintManagerForEditionOwner.createAbridgedVector(vector)).wait(); await expect( mintManagerForEditionOwner.updateAbridgedVector( vectorId, - { ...vector, maxUserClaimableViaVector: 57 }, + { ...vector, maxUserClaimableViaVector: 57, tokenLimitPerTx: 32938 }, vectorUpdateConfig, true, 10009, ), ).to.emit(mintManagerForEditionOwner, "VectorUpdated"); + expect((await mintManagerForEditionOwner.getAbridgedVector(vectorId)).tokenLimitPerTx).to.equal(32938); expect((await mintManagerForEditionOwner.getAbridgedVector(vectorId)).maxTotalClaimableViaVector).to.equal(100); expect((await mintManagerForEditionOwner.getAbridgedVector(vectorId)).maxUserClaimableViaVector).to.not.equal(57); expect((await mintManagerForEditionOwner.getAbridgedVector(vectorId)).maxUserClaimableViaVector).to.equal(0); @@ -1897,6 +1885,8 @@ describe("Mint Manager", () => { mintManagerProxy.address, generalOwner, null, + null, + false, false, 0, ethers.constants.AddressZero, @@ -1955,20 +1945,20 @@ describe("Mint Manager", () => { "EditionVectorCreated", ); const mintManagerForFan1 = await mintManagerWithOwner.connect(fan1); - await expect(mintManagerForFan1.vectorMintEdition721(1, 1, fan1.address)).to.be.revertedWithCustomError( + await expect(mintManagerForFan1.vectorMint721(1, 1, fan1.address)).to.be.revertedWithCustomError( mintManagerForFan1, Errors.InvalidPaymentAmount, ); await expect( - mintManagerForFan1.vectorMintEdition721(1, 1, fan1.address, { + mintManagerForFan1.vectorMint721(1, 1, fan1.address, { value: ethers.utils.parseEther("0.00000001"), }), ).to.be.revertedWithCustomError(mintManager, Errors.InvalidPaymentAmount); await expect( - mintManagerForFan1.vectorMintEdition721(1, 1, fan1.address, { value: mintFeeWei }), + mintManagerForFan1.vectorMint721(1, 1, fan1.address, { value: mintFeeWei }), ).to.be.revertedWithCustomError(mintManager, Errors.InvalidPaymentAmount); await expect( - mintManagerForFan1.vectorMintEdition721(1, 1, fan1.address, { + mintManagerForFan1.vectorMint721(1, 1, fan1.address, { value: mintFeeWei.add(ethers.utils.parseEther("0.00000001")), }), ).to.emit(singleEditionERC721, "Transfer"); @@ -1988,7 +1978,7 @@ describe("Mint Manager", () => { ); mintManagerForFan1 = await mintManagerWithOwner.connect(fan1); meERC721 = multipleEditionERC721; - await expect(mintManagerForFan1.vectorMintEdition721(1, 1, fan1.address)).to.be.revertedWithCustomError( + await expect(mintManagerForFan1.vectorMint721(1, 1, fan1.address)).to.be.revertedWithCustomError( mintManager, Errors.MintFeeTooLow, ); @@ -1996,15 +1986,17 @@ describe("Mint Manager", () => { it("Should be able to mint one to one recipient", async function () { await expect( - mintManagerForFan1.vectorMintEdition721(1, 1, fan1.address, { value: mintFeeWei.sub(1) }), + mintManagerForFan1.vectorMint721(1, 1, fan1.address, { value: mintFeeWei.sub(1) }), ).to.be.revertedWithCustomError(mintManager, Errors.MintFeeTooLow); const mintManagerForPlatform = mintManagerForFan1.connect(mintManagerOwner); - await expect(mintManagerForPlatform.updatePlatformMintFee(mintFeeWei.sub(1))).to.not.be.reverted; + await expect(mintManagerForPlatform.updatePlatformAndMintFee(mintManagerOwner.address, mintFeeWei.sub(1))).to + .not.be.reverted; - await expect( - mintManagerForFan1.vectorMintEdition721(1, 1, fan1.address, { value: mintFeeWei.sub(1) }), - ).to.emit(meERC721, "Transfer"); + await expect(mintManagerForFan1.vectorMint721(1, 1, fan1.address, { value: mintFeeWei.sub(1) })).to.emit( + meERC721, + "Transfer", + ); }); }); @@ -2013,19 +2005,19 @@ describe("Mint Manager", () => { const { mintManagerWithOwner } = await vectorMintsFixture(); let mintManagerUnauthorized = mintManagerWithOwner.connect(fan1); - await expect(mintManagerUnauthorized.updatePlatformMintFee(1)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); + await expect( + mintManagerUnauthorized.updatePlatformAndMintFee(mintManagerOwner.address, 1), + ).to.be.revertedWith("Ownable: caller is not the owner"); mintManagerUnauthorized = mintManagerUnauthorized.connect(editionsOwner); - await expect(mintManagerUnauthorized.updatePlatformMintFee(1)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); + await expect( + mintManagerUnauthorized.updatePlatformAndMintFee(mintManagerOwner.address, 1), + ).to.be.revertedWith("Ownable: caller is not the owner"); mintManagerUnauthorized = mintManagerUnauthorized.connect(editionsMetadataOwner); - await expect(mintManagerUnauthorized.updatePlatformMintFee(1)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); + await expect( + mintManagerUnauthorized.updatePlatformAndMintFee(mintManagerOwner.address, 1), + ).to.be.revertedWith("Ownable: caller is not the owner"); }); }); }); @@ -2051,25 +2043,25 @@ describe("Mint Manager", () => { "SeriesVectorCreated", ); const mintManagerForFan1 = await mintManagerWithOwner.connect(fan1); - await expect(mintManagerForFan1.vectorMintSeries721(1, 1, fan1.address)).to.be.revertedWithCustomError( + await expect(mintManagerForFan1.vectorMint721(1, 1, fan1.address)).to.be.revertedWithCustomError( mintManagerForFan1, Errors.InvalidPaymentAmount, ); await expect( - mintManagerForFan1.vectorMintSeries721(1, 1, fan1.address, { + mintManagerForFan1.vectorMint721(1, 1, fan1.address, { value: ethers.utils.parseEther("0.00000001"), }), ).to.be.revertedWithCustomError(mintManager, Errors.InvalidPaymentAmount); await expect( - mintManagerForFan1.vectorMintSeries721(1, 1, fan1.address, { value: mintFeeWei }), + mintManagerForFan1.vectorMint721(1, 1, fan1.address, { value: mintFeeWei }), ).to.be.revertedWithCustomError(mintManager, Errors.InvalidPaymentAmount); await expect( - mintManagerForFan1.vectorMintSeries721(1, 1, fan1.address, { + mintManagerForFan1.vectorMint721(1, 1, fan1.address, { value: mintFeeWei.add(ethers.utils.parseEther("0.00000001")), }), ).to.emit(generalERC721, "Transfer"); await expect( - mintManagerForFan1.vectorMintSeries721(1, 4, fan1.address, { + mintManagerForFan1.vectorMint721(1, 4, fan1.address, { value: mintFeeWei.mul(4).add(ethers.utils.parseEther("0.00000001").mul(4)), }), ) @@ -2079,7 +2071,7 @@ describe("Mint Manager", () => { .to.emit(generalERC721, "Transfer"); await expect( - mintManagerForFan1.vectorMintSeries721(1, 1, fan1.address, { + mintManagerForFan1.vectorMint721(1, 1, fan1.address, { value: mintFeeWei.add(ethers.utils.parseEther("0.00000001")), }), ).to.be.revertedWithCustomError(mintManager, Errors.OnchainVectorMintGuardFailed); @@ -2105,17 +2097,17 @@ describe("Mint Manager", () => { "SeriesVectorCreated", ); await expect( - mintManagerForGeneralOwner.vectorMintSeries721(1, 1, fan1.address, { + mintManagerForGeneralOwner.vectorMint721(1, 1, fan1.address, { value: mintFeeWei.add(ethers.utils.parseEther("0.00000001")), }), ).to.emit(generalERC721, "Transfer"); await expect( - mintManagerForGeneralOwner.vectorMintSeries721(1, 1, fan1.address, { + mintManagerForGeneralOwner.vectorMint721(1, 1, fan1.address, { value: mintFeeWei.add(ethers.utils.parseEther("0.00000001")), }), ).to.be.revertedWithCustomError(mintManager, Errors.OnchainVectorMintGuardFailed); await expect( - mintManagerForGeneralOwner.vectorMintSeries721(1, 2, generalOwner.address, { + mintManagerForGeneralOwner.vectorMint721(1, 2, generalOwner.address, { value: mintFeeWei.mul(2).add(ethers.utils.parseEther("0.00000002")), }), ).to.be.revertedWithCustomError(mintManager, Errors.OnchainVectorMintGuardFailed); @@ -2125,6 +2117,7 @@ describe("Mint Manager", () => { const { mintManagerWithOwner, generalERC721 } = await vectorMintsFixture(); const mintManagerForGeneralOwner = await mintManagerWithOwner.connect(generalOwner); + /* const allowlistedAddresses = [ fan1.address, editionsOwner.address, @@ -2136,6 +2129,7 @@ describe("Mint Manager", () => { const root = tree.getRoot().toString("hex"); const hashedFan1Address = keccak256(fan1.address); const proof = tree.getHexProof(hashedFan1Address); + */ const vector = SAMPLE_ABRIDGED_VECTOR( generalERC721.address, @@ -2148,64 +2142,46 @@ describe("Mint Manager", () => { 0, 5, ethers.utils.parseEther("0.00000001"), - "0x" + root, + ethers.constants.HashZero, ); await expect(mintManagerForGeneralOwner.createAbridgedVector(vector)).to.emit( mintManagerForGeneralOwner, "SeriesVectorCreated", ); const mintManagerForFan1 = await mintManagerWithOwner.connect(fan1); + await expect(mintManagerForFan1.vectorMint721(1, 1, fan1.address)).to.be.revertedWithCustomError( + mintManagerForFan1, + Errors.InvalidPaymentAmount, + ); await expect( - mintManagerForFan1.vectorMintSeries721WithAllowlist(1, 1, fan1.address, proof), - ).to.be.revertedWithCustomError(mintManagerForFan1, Errors.InvalidPaymentAmount); - await expect( - mintManagerForFan1.vectorMintSeries721WithAllowlist(1, 1, fan1.address, proof, { + mintManagerForFan1.vectorMint721(1, 1, fan1.address, { value: ethers.utils.parseEther("0.00000001"), }), ).to.be.revertedWithCustomError(mintManager, Errors.InvalidPaymentAmount); await expect( - mintManagerForFan1.vectorMintSeries721WithAllowlist(1, 1, fan1.address, proof, { value: mintFeeWei }), + mintManagerForFan1.vectorMint721(1, 1, fan1.address, { value: mintFeeWei }), ).to.be.revertedWithCustomError(mintManager, Errors.InvalidPaymentAmount); await expect( - mintManagerForFan1.vectorMintSeries721WithAllowlist(1, 1, fan1.address, proof, { + mintManagerForFan1.vectorMint721(1, 1, fan1.address, { value: mintFeeWei.add(ethers.utils.parseEther("0.00000001")), }), ).to.emit(generalERC721, "Transfer"); const mintManagerForNonAllowlistedAccount = mintManagerForFan1.connect(platformPaymentAddress); await expect( - mintManagerForNonAllowlistedAccount.vectorMintSeries721WithAllowlist( - 1, - 1, - platformPaymentAddress.address, - proof, - { - value: mintFeeWei, - }, - ), - ).to.be.revertedWithCustomError(mintManager, Errors.AllowlistInvalid); + mintManagerForNonAllowlistedAccount.vectorMint721(1, 1, platformPaymentAddress.address, { + value: mintFeeWei, + }), + ).to.be.revertedWithCustomError(mintManager, Errors.InvalidPaymentAmount); }); }); describe("Direct mint vectors metadata", function () { let mintManagerForEditionOwner: MintManager; let mintManagerForGeneralOwner: MintManager; - let proofForFan: string[] = []; let sampleVector: any; beforeEach(async function () { - const allowlistedAddresses = [ - fan1.address, - editionsOwner.address, - generalOwner.address, - editionsMetadataOwner.address, - ]; - const leaves = allowlistedAddresses.map(x => ethers.utils.keccak256(x)); - const tree = new MerkleTree(leaves, keccak256, { sortPairs: true }); - const root = tree.getRoot().toString("hex"); - const hashedFan1Address = keccak256(fan1.address); - proofForFan = tree.getHexProof(hashedFan1Address); - const { mintManagerWithOwner, generalERC721, singleEditionERC721 } = await vectorMintsFixture(); mintManagerForEditionOwner = await mintManagerWithOwner.connect(editionsOwner); mintManagerForGeneralOwner = await mintManagerWithOwner.connect(generalOwner); @@ -2234,7 +2210,7 @@ describe("Mint Manager", () => { 0, 5, ethers.utils.parseEther("0"), - "0x" + root, + ethers.constants.HashZero, ); await expect(mintManagerForGeneralOwner.createAbridgedVector(vector3)).to.emit( mintManagerForGeneralOwner, @@ -2252,7 +2228,7 @@ describe("Mint Manager", () => { 0, 5, ethers.utils.parseEther("0"), - "0x" + root, + ethers.constants.HashZero, ); await expect(mintManagerForEditionOwner.createAbridgedVector(vector4)).to.emit( mintManagerForEditionOwner, @@ -2324,24 +2300,27 @@ describe("Mint Manager", () => { .withArgs(4, true, 0); await expect( - mintManagerForGeneralOwner.vectorMintSeries721(1, 1, fan1.address, { value: mintFeeWei }), + mintManagerForGeneralOwner.vectorMint721(1, 1, fan1.address, { value: mintFeeWei }), ).to.be.revertedWithCustomError(mintManagerForGeneralOwner, Errors.MintPaused); await expect( - mintManagerForGeneralOwner.vectorMintSeries721WithAllowlist(3, 1, fan1.address, proofForFan, { + mintManagerForGeneralOwner.vectorMint721(3, 1, fan1.address, { value: mintFeeWei, }), ).to.be.revertedWithCustomError(mintManagerForGeneralOwner, Errors.MintPaused); await expect( - mintManagerForEditionOwner.vectorMintEdition721(2, 1, fan1.address, { value: mintFeeWei }), + mintManagerForEditionOwner.vectorMint721(2, 1, fan1.address, { value: mintFeeWei }), ).to.be.revertedWithCustomError(mintManagerForEditionOwner, Errors.MintPaused); + /* + vectorMint721WithAllowlist DEPRECATED await expect( - mintManagerForEditionOwner.vectorMintEdition721WithAllowlist(4, 1, fan1.address, proofForFan, { + mintManagerForEditionOwner.vectorMint721WithAllowlist(4, 1, fan1.address, proofForFan, { value: mintFeeWei, }), ).to.be.revertedWithCustomError(mintManagerForEditionOwner, Errors.MintPaused); + */ // mints unpaused await expect(mintManagerForGeneralOwner.setAbridgedVectorMetadata(1, false, 0)) @@ -2368,23 +2347,26 @@ describe("Mint Manager", () => { .to.emit(mintManagerForEditionOwner, "VectorMetadataSet") .withArgs(4, false, 0); - await expect(mintManagerForGeneralOwner.vectorMintSeries721(1, 1, fan1.address, { value: mintFeeWei })).to.not - .be.reverted; + await expect(mintManagerForGeneralOwner.vectorMint721(1, 1, fan1.address, { value: mintFeeWei })).to.not.be + .reverted; await expect( - mintManagerForGeneralOwner.vectorMintSeries721WithAllowlist(3, 1, fan1.address, proofForFan, { + mintManagerForGeneralOwner.vectorMint721(3, 1, fan1.address, { value: mintFeeWei, }), ).to.not.be.reverted; - await expect(mintManagerForEditionOwner.vectorMintEdition721(2, 1, fan1.address, { value: mintFeeWei })).to.not - .be.reverted; + await expect(mintManagerForEditionOwner.vectorMint721(2, 1, fan1.address, { value: mintFeeWei })).to.not.be + .reverted; + /* + vectorMint721WithAllowlist DEPRECATED await expect( - mintManagerForEditionOwner.vectorMintEdition721WithAllowlist(4, 1, fan1.address, proofForFan, { + mintManagerForEditionOwner.vectorMint721WithAllowlist(4, 1, fan1.address, proofForFan, { value: mintFeeWei, }), ).to.not.be.reverted; + */ }); }); }); @@ -2401,6 +2383,7 @@ describe("Mint Manager", () => { mintManager.address, generalOwner, SAMPLE_ABRIDGED_VECTOR(ethers.constants.AddressZero, generalOwner.address, false), + null, false, 0, ethers.constants.AddressZero, @@ -2413,10 +2396,10 @@ describe("Mint Manager", () => { it("Transfer bug is non-existent", async function () { mintManager = mintManager.connect(fan1); - await expect(mintManager.vectorMintSeries721(1, 1, fan1.address, { value: ethers.utils.parseEther("0.0008") })) - .to.not.be.reverted; + await expect(mintManager.vectorMint721(1, 1, fan1.address, { value: ethers.utils.parseEther("0.0008") })).to.not + .be.reverted; await expect( - mintManager.vectorMintSeries721(1, 2, generalOwner.address, { + mintManager.vectorMint721(1, 2, generalOwner.address, { value: ethers.utils.parseEther("0.0008").mul(2), }), ).to.not.be.reverted; @@ -2432,7 +2415,7 @@ describe("Mint Manager", () => { // can still mint after the last transfer await expect( - mintManager.vectorMintSeries721(1, 2, generalOwner.address, { + mintManager.vectorMint721(1, 2, generalOwner.address, { value: ethers.utils.parseEther("0.0008").mul(2), }), ).to.not.be.reverted; @@ -2449,7 +2432,7 @@ describe("Mint Manager", () => { signers.map(async signer => { for (let i = 1; i <= 5; i++) { await expect( - mintManager.vectorMintSeries721(1, i, signer.address, { + mintManager.vectorMint721(1, i, signer.address, { value: ethers.utils.parseEther("0.0008").mul(i), }), ).to.not.be.reverted; @@ -2469,22 +2452,27 @@ describe("Mint Manager", () => { let general: ERC721GeneralSequence; before(async function () { - general = await setupGeneral( - observability.address, - generalSequenceImplementation, - trustedForwarder.address, - mintManager.address, + general = ERC721GeneralSequence__factory.connect( + ( + await setupGeneral( + observability.address, + generalSequenceImplementation, + trustedForwarder.address, + mintManager.address, + generalOwner, + SAMPLE_ABRIDGED_VECTOR(ethers.constants.AddressZero, generalOwner.address, false), + ) + ).address, generalOwner, - SAMPLE_ABRIDGED_VECTOR(ethers.constants.AddressZero, generalOwner.address, false), ); }); it("Transfer bug is non-existent", async function () { mintManager = mintManager.connect(fan1); - await expect(mintManager.vectorMintSeries721(2, 1, fan1.address, { value: ethers.utils.parseEther("0.0008") })) - .to.not.be.reverted; + await expect(mintManager.vectorMint721(2, 1, fan1.address, { value: ethers.utils.parseEther("0.0008") })).to.not + .be.reverted; await expect( - mintManager.vectorMintSeries721(2, 2, generalOwner.address, { + mintManager.vectorMint721(2, 2, generalOwner.address, { value: ethers.utils.parseEther("0.0008").mul(2), }), ).to.not.be.reverted; @@ -2500,7 +2488,7 @@ describe("Mint Manager", () => { // can still mint after the last transfer await expect( - mintManager.vectorMintSeries721(2, 2, generalOwner.address, { + mintManager.vectorMint721(2, 2, generalOwner.address, { value: ethers.utils.parseEther("0.0008").mul(2), }), ).to.not.be.reverted; @@ -2517,7 +2505,7 @@ describe("Mint Manager", () => { signers.map(async signer => { for (let i = 1; i <= 5; i++) { await expect( - mintManager.vectorMintSeries721(2, i, signer.address, { + mintManager.vectorMint721(2, i, signer.address, { value: ethers.utils.parseEther("0.0008").mul(i), }), ).to.not.be.reverted; @@ -2553,10 +2541,10 @@ describe("Mint Manager", () => { it("Transfer bug is non-existent", async function () { mintManager = mintManager.connect(fan1); - await expect(mintManager.vectorMintEdition721(3, 1, fan1.address, { value: ethers.utils.parseEther("0.0008") })) - .to.not.be.reverted; + await expect(mintManager.vectorMint721(3, 1, fan1.address, { value: ethers.utils.parseEther("0.0008") })).to.not + .be.reverted; await expect( - mintManager.vectorMintEdition721(3, 2, editionsOwner.address, { + mintManager.vectorMint721(3, 2, editionsOwner.address, { value: ethers.utils.parseEther("0.0008").mul(2), }), ).to.not.be.reverted; @@ -2572,7 +2560,7 @@ describe("Mint Manager", () => { // can still mint after the last transfer await expect( - mintManager.vectorMintEdition721(3, 2, editionsOwner.address, { + mintManager.vectorMint721(3, 2, editionsOwner.address, { value: ethers.utils.parseEther("0.0008").mul(2), }), ).to.not.be.reverted; @@ -2589,7 +2577,7 @@ describe("Mint Manager", () => { signers.map(async signer => { for (let i = 1; i <= 5; i++) { await expect( - mintManager.vectorMintEdition721(3, i, signer.address, { + mintManager.vectorMint721(3, i, signer.address, { value: ethers.utils.parseEther("0.0008").mul(i), }), ).to.not.be.reverted; @@ -2624,10 +2612,10 @@ describe("Mint Manager", () => { it("Transfer bug is non-existent", async function () { mintManager = mintManager.connect(fan1); - await expect(mintManager.vectorMintEdition721(4, 1, fan1.address, { value: ethers.utils.parseEther("0.0008") })) - .to.not.be.reverted; + await expect(mintManager.vectorMint721(4, 1, fan1.address, { value: ethers.utils.parseEther("0.0008") })).to.not + .be.reverted; await expect( - mintManager.vectorMintEdition721(4, 2, editionsOwner.address, { + mintManager.vectorMint721(4, 2, editionsOwner.address, { value: ethers.utils.parseEther("0.0008").mul(2), }), ).to.not.be.reverted; @@ -2643,7 +2631,7 @@ describe("Mint Manager", () => { // can still mint after the last transfer await expect( - mintManager.vectorMintEdition721(4, 2, editionsOwner.address, { + mintManager.vectorMint721(4, 2, editionsOwner.address, { value: ethers.utils.parseEther("0.0008").mul(2), }), ).to.not.be.reverted; @@ -2660,7 +2648,7 @@ describe("Mint Manager", () => { signers.map(async signer => { for (let i = 1; i <= 5; i++) { await expect( - mintManager.vectorMintEdition721(4, i, signer.address, { + mintManager.vectorMint721(4, i, signer.address, { value: ethers.utils.parseEther("0.0008").mul(i), }), ).to.not.be.reverted; @@ -2676,4 +2664,175 @@ describe("Mint Manager", () => { }); }); }); + + describe("Creator reserve mints", function () { + describe("Series based", function () { + let generative: ERC721Generative; + let general: ERC721General; + + before(async function () { + generative = await setupGenerative( + observability.address, + generativeImplementation, + trustedForwarder.address, + mintManager.address, + generalOwner, + SAMPLE_ABRIDGED_VECTOR(ethers.constants.AddressZero, generalOwner.address, false), + null, + false, + 0, + ethers.constants.AddressZero, + ethers.constants.AddressZero, + 0, + "Test 1", + "T1", + ); + + general = await setupGeneral( + observability.address, + generalImplementation, + trustedForwarder.address, + mintManager.address, + generalOwner, + ); + }); + + it("Non-owner cannot mint creator reserves", async function () { + mintManager = mintManager.connect(fan1); + await expect( + mintManager.creatorReservesMint(generative.address, false, 0, 3, [], false, generalOwner.address), + ).to.be.revertedWithCustomError(mintManager, Errors.Unauthorized); + + await expect( + mintManager.creatorReservesMint(general.address, false, 0, 0, [4, 7], true, generalOwner.address), + ).to.be.revertedWithCustomError(mintManager, Errors.Unauthorized); + }); + + it("Cannot mint creator reserves with invalid mint fee", async function () { + mintManager = mintManager.connect(generalOwner); + await expect( + mintManager.creatorReservesMint(generative.address, false, 0, 3, [], false, generalOwner.address), + ).to.be.revertedWithCustomError(mintManager, Errors.InvalidPaymentAmount); + + await expect( + mintManager.creatorReservesMint(general.address, false, 0, 0, [4, 7], true, generalOwner.address), + ).to.be.revertedWithCustomError(mintManager, Errors.InvalidPaymentAmount); + }); + + it("Owner can validly mint creator reserves multiple times", async function () { + mintManager = mintManager.connect(generalOwner); + await expect( + mintManager.creatorReservesMint(generative.address, false, 0, 3, [], false, generalOwner.address, { + value: ethers.utils.parseEther("0.0008").mul(3), + }), + ) + .to.emit(mintManager, "CreatorReservesNumMint") + .withArgs(generative.address, false, 0, 3) + .to.emit(generative, "Transfer") + .withArgs(ethers.constants.AddressZero, generalOwner.address, 1) + .to.emit(generative, "Transfer") + .withArgs(ethers.constants.AddressZero, generalOwner.address, 2) + .to.emit(generative, "Transfer") + .withArgs(ethers.constants.AddressZero, generalOwner.address, 3); + + await expect( + mintManager.creatorReservesMint(general.address, false, 0, 0, [4, 7], true, generalOwner.address, { + value: ethers.utils.parseEther("0.0008").mul(2), + }), + ) + .to.emit(mintManager, "CreatorReservesChooseMint") + .withArgs(general.address, [4, 7]) + .to.emit(general, "Transfer") + .withArgs(ethers.constants.AddressZero, generalOwner.address, 4) + .to.emit(general, "Transfer") + .withArgs(ethers.constants.AddressZero, generalOwner.address, 7); + }); + }); + + describe("Editions based", function () { + let editions: ERC721EditionsDFS; + + before(async function () { + editions = await setupEditionsDFS( + observability.address, + editionsDFSImplementation, + mintManager.address, + auctionManager.address, + trustedForwarder.address, + editionsOwner, + ); + editions = editions.connect(editionsOwner); + await expect( + editions.createEdition( + "uri", + 100, + ethers.constants.AddressZero, + { + royaltyPercentageBPS: 0, + recipientAddress: ethers.constants.AddressZero, + }, + "0x", + ), + ).to.not.be.reverted; + + await expect( + editions.createEdition( + "uri", + 100, + ethers.constants.AddressZero, + { + royaltyPercentageBPS: 0, + recipientAddress: ethers.constants.AddressZero, + }, + "0x", + ), + ).to.not.be.reverted; + }); + + it("Non-owner cannot mint creator reserves", async function () { + mintManager = mintManager.connect(fan1); + await expect( + mintManager.creatorReservesMint(editions.address, true, 0, 3, [], false, editionsOwner.address), + ).to.be.revertedWithCustomError(mintManager, Errors.Unauthorized); + }); + + it("Cannot mint creator reserves with invalid mint fee", async function () { + mintManager = mintManager.connect(editionsOwner); + await expect( + mintManager.creatorReservesMint(editions.address, true, 0, 3, [], false, editionsOwner.address), + ).to.be.revertedWithCustomError(mintManager, Errors.InvalidPaymentAmount); + }); + + it("Owner can validly mint creator reserves multiple times on multiple editions on a contract", async function () { + mintManager = mintManager.connect(editionsOwner); + await expect( + mintManager.creatorReservesMint(editions.address, true, 0, 3, [], false, editionsOwner.address, { + value: ethers.utils.parseEther("0.0008").mul(3), + }), + ) + .to.emit(mintManager, "CreatorReservesNumMint") + .withArgs(editions.address, true, 0, 3) + .to.emit(editions, "Transfer") + .withArgs(ethers.constants.AddressZero, editionsOwner.address, 1) + .to.emit(editions, "Transfer") + .withArgs(ethers.constants.AddressZero, editionsOwner.address, 2) + .to.emit(editions, "Transfer") + .withArgs(ethers.constants.AddressZero, editionsOwner.address, 3); + + await expect( + mintManager.creatorReservesMint(editions.address, true, 1, 3, [], false, editionsOwner.address, { + value: ethers.utils.parseEther("0.0008").mul(3), + }), + ) + .to.emit(mintManager, "CreatorReservesNumMint") + .withArgs(editions.address, true, 1, 3) + .to.emit(editions, "Transfer") + .withArgs(ethers.constants.AddressZero, editionsOwner.address, 101) + .to.emit(editions, "Transfer") + .withArgs(ethers.constants.AddressZero, editionsOwner.address, 102) + .to.emit(editions, "Transfer") + .withArgs(ethers.constants.AddressZero, editionsOwner.address, 103); + }); + }); + }); }); diff --git a/test/UpgradesTest.ts b/test/UpgradesTest.ts index 4578170..a786989 100644 --- a/test/UpgradesTest.ts +++ b/test/UpgradesTest.ts @@ -4,11 +4,13 @@ import { ethers } from "hardhat"; import { AuctionManager, + DiscreteDutchAuctionMechanic, ERC721Editions, EditionsMetadataRenderer, MinimalForwarder, MintManager, Observability, + TestDiscreteDutchAuctionMechanic, TestEditionsMetadataRenderer, TestMintManager, } from "../types"; @@ -30,6 +32,7 @@ describe("Upgrades functionality", () => { let emr: EditionsMetadataRenderer; let mintManager: MintManager; + let dutchAuction: DiscreteDutchAuctionMechanic; let observability: Observability; let auctionManager: AuctionManager; let trustedForwarder: MinimalForwarder; @@ -50,6 +53,7 @@ describe("Upgrades functionality", () => { observability: observabilityInstance, auctionManagerProxy, editionsImplementationAddress, + daMechanic, } = await setupSystem( platformPaymentAddress.address, mintManagerOwner.address, @@ -59,6 +63,7 @@ describe("Upgrades functionality", () => { ); emr = emrProxy; + dutchAuction = daMechanic; mintManager = mintManagerProxy; trustedForwarder = minimalForwarder; observability = observabilityInstance; @@ -127,7 +132,7 @@ describe("Upgrades functionality", () => { .withArgs(testMintManager.address); const newMintManager = new ethers.Contract(mintManager.address, testMintManager.interface, mintManagerOwner); - expect(await newMintManager.test()).to.equal("test"); + expect(await newMintManager.test()).to.equal(true); // data after upgrade expect(await newMintManager.vectors(1)).to.eql(vectorOnOldImpl); @@ -200,4 +205,58 @@ describe("Upgrades functionality", () => { expect(await newEditionsMetadataRenderer.owner()).to.equal(ownerOnOldEMR); }); }); + + describe("DiscreteDutchAuctionMechanic", function () { + let testDiscreteDutchAuctionMechanic: TestDiscreteDutchAuctionMechanic; + + it("Non owner cannot upgrade DiscreteDutchAuctionMechanic", async function () { + testDiscreteDutchAuctionMechanic = await ( + await ethers.getContractFactory("TestDiscreteDutchAuctionMechanic") + ).deploy(); + await testDiscreteDutchAuctionMechanic.deployed(); + + dutchAuction = dutchAuction.connect(owner); + + await expect(dutchAuction.upgradeTo(testDiscreteDutchAuctionMechanic.address)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + + dutchAuction = dutchAuction.connect(editionsMetadataOwner); + + await expect(dutchAuction.upgradeTo(testDiscreteDutchAuctionMechanic.address)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + + dutchAuction = dutchAuction.connect(fan1); + + await expect(dutchAuction.upgradeTo(testDiscreteDutchAuctionMechanic.address)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + + it("Upgrade to TestDiscreteDutchAuctionMechanic retains original data and introduces new functionality", async function () { + dutchAuction = dutchAuction.connect(mintManagerOwner); + // data before upgrade + const ownerOnOldMechanic = await dutchAuction.owner(); + + testDiscreteDutchAuctionMechanic = await ( + await ethers.getContractFactory("TestDiscreteDutchAuctionMechanic") + ).deploy(); + await testDiscreteDutchAuctionMechanic.deployed(); + + await expect(dutchAuction.upgradeTo(testDiscreteDutchAuctionMechanic.address)) + .to.emit(dutchAuction, "Upgraded") + .withArgs(testDiscreteDutchAuctionMechanic.address); + + const newDiscreteDutchAuctionMechanic = new ethers.Contract( + dutchAuction.address, + testDiscreteDutchAuctionMechanic.interface, + mintManagerOwner, + ); + expect(await newDiscreteDutchAuctionMechanic.test()).to.equal(true); + + // data after upgrade + expect(await newDiscreteDutchAuctionMechanic.owner()).to.equal(ownerOnOldMechanic); + }); + }); }); diff --git a/test/__utils__/data.ts b/test/__utils__/data.ts index a606dc2..00f91fa 100644 --- a/test/__utils__/data.ts +++ b/test/__utils__/data.ts @@ -1,6 +1,8 @@ import { BigNumber } from "@ethersproject/contracts/node_modules/@ethersproject/bignumber"; import { ethers } from "hardhat"; +import { OnchainDutchAuctionParams } from "./helpers"; + export const SAMPLE_VECTOR_1 = ( address: string, paymentRecipient: string, @@ -98,6 +100,43 @@ export const SAMPLE_ABRIDGED_VECTOR_UPDATE_CONFIG = ({ }; }; +export const SAMPLE_DA_VECTOR = ( + mechanicAddress: string, + input: { + prices?: string[]; + periodDuration?: number; + maxTotalClaimableViaVector?: number; + maxUserClaimableViaVector?: number; + startTimestamp?: number; + endTimestamp?: number; + tokenLimitPerTx?: number; + seed?: string; + }, +): OnchainDutchAuctionParams => { + return { + mechanicAddress, + prices: input.prices ?? ["0.001", "0.0001"], + periodDuration: input.periodDuration ?? 100, + maxTotalClaimableViaVector: input.maxTotalClaimableViaVector ?? 0, + maxUserClaimableViaVector: input.maxUserClaimableViaVector ?? 0, + startTimestamp: input.startTimestamp ?? Math.floor(Date.now() / 1000), + endTimestamp: input.endTimestamp ?? 0, + tokenLimitPerTx: input.tokenLimitPerTx ?? 0, + seed: input.seed ?? Math.floor(Date.now() / 1000).toString(), + }; +}; + +export type DutchAuctionUpdateValues = { + prices?: string[]; + periodDuration?: number; + maxTotalClaimableViaVector?: number; + maxUserClaimableViaVector?: number; + startTimestamp?: number; + endTimestamp?: number; + tokenLimitPerTx?: number; + paymentRecipient?: string; +}; + export const SAMPLE_VECTOR_MUTABILITY_1 = (deleteFrozen = 0, pausesFrozen = 0, updatesFrozen = 0) => { return { deleteFrozen, @@ -144,4 +183,12 @@ export enum Errors { TokenMintedAlready = "TokenMintedAlready", UnsafeMintRecipient = "UnsafeMintRecipient", MintPaused = "MintPaused", + MechanicPaused = "MechanicPaused", + InvalidMechanic = "InvalidMechanic", + InvalidVectorConfig = "InvalidVectorConfig", + InvalidUpdate = "InvalidUpdate", + InvalidMint = "InvalidMint", + InvalidRebate = "InvalidRebate", + CollectorNotOwedRebate = "CollectorNotOwedRebate", + InvalidDPPFundsWithdrawl = "InvalidDPPFundsWithdrawl", } diff --git a/test/__utils__/helpers.ts b/test/__utils__/helpers.ts index 7c5e5f4..5fd559b 100644 --- a/test/__utils__/helpers.ts +++ b/test/__utils__/helpers.ts @@ -1,9 +1,11 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { BigNumber } from "ethers"; +import { BigNumber, BytesLike } from "ethers"; import { ethers } from "hardhat"; import { AuctionManager__factory, + DiscreteDutchAuctionMechanic, + DiscreteDutchAuctionMechanic__factory, ERC721Editions, ERC721EditionsDFS, ERC721EditionsDFS__factory, @@ -22,6 +24,7 @@ import { NativeMetaTransaction__factory, Observability__factory, } from "../../types"; +import { DutchAuctionUpdateValues } from "./data"; import { signGatedMint, signGatedMintWithMetaTxPacket, signGatedSeriesMint, signWETHMetaTxRequest } from "./mint"; export type OnchainMintVectorParams = { @@ -35,6 +38,18 @@ export type OnchainMintVectorParams = { editionId?: number; }; +export type OnchainDutchAuctionParams = { + startTimestamp: number; + endTimestamp: number; + prices: string[]; + periodDuration: number; + tokenLimitPerTx: number; + maxTotalClaimableViaVector: number; + maxUserClaimableViaVector: number; + mechanicAddress: string; + seed: string; +}; + export const DEFAULT_ONCHAIN_MINT_VECTOR: OnchainMintVectorParams = { startTimestamp: 0, endTimestamp: 0, @@ -56,6 +71,7 @@ export const setupSingleEdition = async ( name: string, symbol: string, directMint: OnchainMintVectorParams | null = null, + mechanicMint: OnchainDutchAuctionParams | null = null, useMarketplaceFilter = false, defaultTokenManager = ethers.constants.AddressZero, royaltyRecipient = ethers.constants.AddressZero, @@ -122,7 +138,13 @@ export const setupSingleEdition = async ( const SingleEdition = await ( await ethers.getContractFactory("SingleEdition") - ).deploy(singleImplementationAddress, initializeData, mintVectorData, observabilityAddress); + ).deploy( + singleImplementationAddress, + initializeData, + mintVectorData, + encodeMechanicVectorData(mintManagerAddress, creator.address, mechanicMint), + observabilityAddress, + ); const singleEdition = await SingleEdition.deployed(); return ERC721SingleEdition__factory.connect(singleEdition.address, creator); }; @@ -137,6 +159,7 @@ export const setupSingleEditionDFS = async ( name: string, symbol: string, directMint: OnchainMintVectorParams | null = null, + mechanicMint: OnchainDutchAuctionParams | null = null, useMarketplaceFilter = false, defaultTokenManager = ethers.constants.AddressZero, royaltyRecipient = ethers.constants.AddressZero, @@ -192,7 +215,13 @@ export const setupSingleEditionDFS = async ( const SingleEditionDFS = await ( await ethers.getContractFactory("SingleEditionDFS") - ).deploy(singleEditionDFSImplementationAddress, initializeData, mintVectorData, observabilityAddress); + ).deploy( + singleEditionDFSImplementationAddress, + initializeData, + mintVectorData, + encodeMechanicVectorData(mintManagerAddress, creator.address, mechanicMint), + observabilityAddress, + ); const singleEditionDFS = await SingleEditionDFS.deployed(); return ERC721SingleEditionDFS__factory.connect(singleEditionDFS.address, creator); }; @@ -207,6 +236,7 @@ export const setupEditions = async ( emrAddress: string, creator: SignerWithAddress, directMint: OnchainMintVectorParams | null = null, + mechanicMint: OnchainDutchAuctionParams | null = null, defaultTokenManager = ethers.constants.AddressZero, royaltyRecipient = ethers.constants.AddressZero, royaltyPercentage = 0, @@ -261,6 +291,7 @@ export const setupEditions = async ( }, ethers.utils.arrayify("0x"), mintVectorData, + encodeMechanicVectorData(mintManagerAddress, creator.address, mechanicMint), ); const multipleEditions = await MultipleEditions.deployed(); @@ -283,6 +314,7 @@ export const setupEditionsDFS = async ( trustedForwarderAddress: string, creator: SignerWithAddress, directMint: OnchainMintVectorParams | null = null, + mechanicMint: OnchainDutchAuctionParams | null = null, editionUri = "", defaultTokenManager = ethers.constants.AddressZero, royaltyRecipient = ethers.constants.AddressZero, @@ -337,6 +369,7 @@ export const setupEditionsDFS = async ( }, ethers.utils.arrayify("0x"), mintVectorData, + encodeMechanicVectorData(mintManagerAddress, creator.address, mechanicMint), ); const multipleEditionsDFS = await MultipleEditionsDFS.deployed(); @@ -363,6 +396,7 @@ export const setupMultipleEdition = async ( name: string, symbol: string, directMint: OnchainMintVectorParams | null = null, + mechanicMint: OnchainDutchAuctionParams | null = null, useMarketplaceFilter = false, contractName = "contractName", royaltyPercentage = 0, @@ -425,6 +459,7 @@ export const setupMultipleEdition = async ( }, ethers.utils.arrayify("0x"), mintVectorData, + encodeMechanicVectorData(mintVectorAddress, creator.address, mechanicMint), ); const multipleEditions = await MultipleEditions.deployed(); @@ -442,6 +477,7 @@ export const setupMultipleEditionDFS = async ( size: number, symbol: string, directMint: OnchainMintVectorParams | null = null, + mechanicMint: OnchainDutchAuctionParams | null = null, editionUri: string = "uri", useMarketplaceFilter = false, contractName = "contractName", @@ -494,6 +530,7 @@ export const setupMultipleEditionDFS = async ( }, ethers.utils.arrayify("0x"), mintVectorData, + encodeMechanicVectorData(mintVectorAddress, creator.address, mechanicMint), ); const multipleEditionsDFS = await MultipleEditionsDFS.deployed(); @@ -643,6 +680,8 @@ export const setupGeneral = async ( mintManagerAddress: string, creator: SignerWithAddress, directMint: OnchainMintVectorParams | null = null, + mechanicMint: OnchainDutchAuctionParams | null = null, + isCollectorsChoice: boolean = false, useMarketplaceFilter = false, limitSupply = 0, defaultTokenManager = ethers.constants.AddressZero, @@ -703,7 +742,13 @@ export const setupGeneral = async ( const Series = await ( await ethers.getContractFactory("Series", creator) - ).deploy(generalImplementationAddress, initializeData, vectorData); + ).deploy( + generalImplementationAddress, + initializeData, + vectorData, + encodeMechanicVectorData(mintManagerAddress, creator.address, mechanicMint), + isCollectorsChoice, + ); const series = await Series.deployed(); return ERC721General__factory.connect(series.address, creator); @@ -716,6 +761,7 @@ export const setupGenerative = async ( mintManagerAddress: string, creator: SignerWithAddress, directMint: OnchainMintVectorParams | null = null, + mechanicMint: OnchainDutchAuctionParams | null = null, useMarketplaceFilter = false, limitSupply = 0, defaultTokenManager = ethers.constants.AddressZero, @@ -777,7 +823,13 @@ export const setupGenerative = async ( const GenerativeSeries = await ( await ethers.getContractFactory("GenerativeSeries", creator) - ).deploy(generalImplementationAddress, initializeData, vectorData, observabilityAddress); + ).deploy( + generalImplementationAddress, + initializeData, + vectorData, + encodeMechanicVectorData(mintManagerAddress, creator.address, mechanicMint), + observabilityAddress, + ); const generativeSeries = await GenerativeSeries.deployed(); return ERC721Generative__factory.connect(generativeSeries.address, creator); @@ -853,6 +905,7 @@ export const setupEtherAuctionWithNewToken = async ( { recipientAddress: royaltyRecipient, royaltyPercentageBPS: royaltyPercentage }, auctionData, mintVectorData, + "0x", ); const multipleEditions = await MultipleEditions.deployed(); @@ -950,12 +1003,25 @@ export async function setupSystem( const observability = await observabilityFactory.deploy(); await observability.deployed(); + const dutchAuctionImplFactory = await ethers.getContractFactory("DiscreteDutchAuctionMechanic"); + const dutchAuctionImpl = await dutchAuctionImplFactory.deploy(); + await dutchAuctionImpl.deployed(); + + const dutchAuctionEncodedFn = dutchAuctionImpl.interface.encodeFunctionData("initialize", [ + mintManagerProxy.address, + mintManagerOwnerAddress, + ]); + const dutchAuctionFactory = await ethers.getContractFactory("ERC1967Proxy"); + const dutchAuction = await dutchAuctionFactory.deploy(dutchAuctionImpl.address, dutchAuctionEncodedFn); + await dutchAuction.deployed(); + return { emrProxy: EditionsMetadataRenderer__factory.connect(emrProxy.address, signer), mintManagerProxy: MintManager__factory.connect(mintManagerProxy.address, signer), auctionManagerProxy: AuctionManager__factory.connect(auctionManagerProxy.address, signer), minimalForwarder: MinimalForwarder__factory.connect(minimalForwarder.address, signer), observability: Observability__factory.connect(observability.address, signer), + daMechanic: DiscreteDutchAuctionMechanic__factory.connect(dutchAuction.address, signer), generalImplementationAddress: general.address, generalSequenceImplementationAddress: generalSequence.address, generativeImplementationAddress: generative.address, @@ -966,6 +1032,136 @@ export async function setupSystem( }; } +export const encodeMechanicVectorData = ( + mintManagerAddress: string, + paymentRecipient: string, + mechanicMint: OnchainDutchAuctionParams | null, +): BytesLike => { + let mechanicVectorData = "0x"; + if (mechanicMint) { + const dutchAuctionData = encodeDAVectorData(mechanicMint, paymentRecipient); + + mechanicVectorData = ethers.utils.defaultAbiCoder.encode( + ["uint96", "address", "address", "bytes"], + [mechanicMint.seed, mechanicMint.mechanicAddress, mintManagerAddress, dutchAuctionData], + ); + } + + return mechanicVectorData; +}; + +export const encodeDAVectorData = (mechanicMint: OnchainDutchAuctionParams, paymentRecipient: string): BytesLike => { + const { packedPrices, numPrices, bytesPerPrice } = encodeDutchAuctionPriceData(mechanicMint.prices); + + return ethers.utils.defaultAbiCoder.encode( + ["uint48", "uint48", "uint32", "uint32", "uint48", "uint32", "uint32", "uint8", "address", "bytes"], + [ + mechanicMint.startTimestamp, + mechanicMint.endTimestamp, + mechanicMint.periodDuration, + mechanicMint.maxUserClaimableViaVector, + mechanicMint.maxTotalClaimableViaVector, + mechanicMint.tokenLimitPerTx, + numPrices, + bytesPerPrice, + paymentRecipient, + packedPrices, + ], + ); +}; + +export const encodeDutchAuctionPriceData = ( + prices: string[], +): { packedPrices: BytesLike; numPrices: number; bytesPerPrice: number } => { + if (prices.length == 0) { + return { packedPrices: "0x", numPrices: 0, bytesPerPrice: 0 }; + } + + // expect in ether, expect 10^18, convert to wei + let biggestPrice = ethers.utils.parseEther(prices[0]); + for (const price of prices) { + if (ethers.utils.parseEther(price).gt(biggestPrice)) { + biggestPrice = ethers.utils.parseEther(price); + } + } + + const bytesPerPrice = numBytesNeeded(biggestPrice); + const packedPrices = ethers.utils.solidityPack( + new Array(prices.length).fill(`uint${bytesPerPrice * 8}`), + prices.map(price => { + return ethers.utils.parseEther(price); + }), + ); + + return { + packedPrices, + numPrices: prices.length, + bytesPerPrice, + }; +}; + +export const dutchAuctionUpdateArgs = ( + updateValues: DutchAuctionUpdateValues, +): { + dutchAuction: DiscreteDutchAuctionMechanic.DutchAuctionVectorStruct; + updateConfig: DiscreteDutchAuctionMechanic.DutchAuctionVectorUpdateConfigStruct; + packedPrices: BytesLike; +} => { + // if prices isn't updated, this returns 0 values for each field + const { numPrices, bytesPerPrice, packedPrices } = encodeDutchAuctionPriceData(updateValues.prices ?? []); + + const dutchAuction = { + startTimestamp: updateValues.startTimestamp ?? 0, + endTimestamp: updateValues.endTimestamp ?? 0, + periodDuration: updateValues.periodDuration ?? 0, + maxUserClaimableViaVector: updateValues.maxUserClaimableViaVector ?? 0, + maxTotalClaimableViaVector: updateValues.maxTotalClaimableViaVector ?? 0, + currentSupply: 0, + lowestPriceSoldAtIndex: 0, + tokenLimitPerTx: updateValues.tokenLimitPerTx ?? 0, + numPrices, + paymentRecipient: updateValues.paymentRecipient ?? ethers.constants.AddressZero, + totalSales: 0, + bytesPerPrice, + auctionExhausted: false, + payeeRevenueHasBeenWithdrawn: false, + }; + const updateConfig = { + updateStartTimestamp: updateValues.startTimestamp != undefined, + updateEndTimestamp: updateValues.endTimestamp != undefined, + updatePaymentRecipient: updateValues.paymentRecipient != undefined, + updateMaxTotalClaimableViaVector: updateValues.maxTotalClaimableViaVector != undefined, + updateTokenLimitPerTx: updateValues.tokenLimitPerTx != undefined, + updateMaxUserClaimableViaVector: updateValues.maxUserClaimableViaVector != undefined, + updatePrices: updateValues.prices != undefined, + updatePeriodDuration: updateValues.periodDuration != undefined, + }; + + return { + dutchAuction, + updateConfig, + packedPrices, + }; +}; + +const numBytesNeeded = (num: BigNumber) => { + const log10 = num.toString().length - 1; + const log2 = log10 / Math.log10(2); // convert log10 to log2 using the base change formula + return Math.floor(log2 / 8) + 1; +}; + +export const produceMechanicVectorId = ( + contractAddress: string, + mechanicAddress: string, + seed: number, + editionId?: number, +): string => { + return ethers.utils.solidityKeccak256( + ["address", "uint96", "address", "bool", "uint96"], + [contractAddress, editionId ?? 0, mechanicAddress, editionId != undefined, seed], + ); +}; + export const hourFromNow = () => { return Math.floor(Date.now() / 1000 + 3600); }; diff --git a/yarn.lock b/yarn.lock index 739647f..2e0363a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1124,10 +1124,10 @@ deep-eql "^4.0.1" ordinal "^1.0.3" -"@nomicfoundation/hardhat-network-helpers@^1.0.3": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.8.tgz#e4fe1be93e8a65508c46d73c41fa26c7e9f84931" - integrity sha512-MNqQbzUJZnCMIYvlniC3U+kcavz/PhhQSsY90tbEtUyMj/IQqsLwIRZa4ctjABh3Bz0KCh9OXUZ7Yk/d9hr45Q== +"@nomicfoundation/hardhat-network-helpers@^1.0.9": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.9.tgz#767449e8a2acda79306ac84626117583d95d25aa" + integrity sha512-OXWCv0cHpwLUO2u7bFxBna6dQtCC2Gg/aN/KtJLO7gmuuA28vgmVKYFRCDUqrbjujzgfwQ2aKyZ9Y3vSmDqS7Q== dependencies: ethereumjs-util "^7.1.4" @@ -2155,6 +2155,15 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3" integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== +axios@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.1.tgz#11fbaa11fc35f431193a9564109c88c1f27b585f" + integrity sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -2818,7 +2827,7 @@ colors@1.4.0, colors@^1.1.2: resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -3119,6 +3128,11 @@ crypto-js@^3.1.9-1: resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.3.0.tgz#846dd1cce2f68aacfa156c8578f926a609b7976b" integrity sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q== +csv-parse@^5.5.1: + version "5.5.1" + resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.5.1.tgz#ed08dc538c1b009c77428087470356830e6bbb41" + integrity sha512-A6DrzSnN7MuOjXOT2tbO08YyYnP9sNDn8zITMHbZN/qt3gUzdGcu3LacYKY7b3RHwKoPwkhhmLeP7SE30cRmgg== + cz-conventional-changelog@3.3.0, cz-conventional-changelog@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz#9246947c90404149b3fe2cf7ee91acad3b7d22d2" @@ -4210,6 +4224,11 @@ follow-redirects@^1.12.1: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +follow-redirects@^1.15.0: + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -4236,6 +4255,15 @@ form-data@^2.2.0: combined-stream "^1.0.6" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -5988,7 +6016,7 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@^2.1.16, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@^2.1.16, mime-types@^2.1.35, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -6855,6 +6883,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + psl@^1.1.28: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"