From c9a21d2cb3e5bda1d9ae01c84fa30bc011a20065 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 21 Jul 2022 21:11:45 +0000 Subject: [PATCH] Add FixedPricePublicSaleMinter tests --- .gitmodules | 3 + contracts/SoundEdition/ISoundNftV1.sol | 40 ---- contracts/SoundEdition/SoundEditionV1.sol | 2 +- contracts/SoundEdition/SoundNftV1.sol | 65 ----- .../Minting/EditionMintControllers.sol | 54 ----- contracts/modules/Minting/EditionMinter.sol | 60 +++++ .../FixedPricePermissionedSaleMinter.sol | 56 ++++- .../Minting/FixedPricePublicSaleMinter.sol | 50 +++- lib/solady | 1 + remappings.txt | 3 +- .../Minting/FixedPricePublicSaleMinter.t.sol | 224 ++++++++++++++++++ 11 files changed, 371 insertions(+), 187 deletions(-) delete mode 100644 contracts/SoundEdition/ISoundNftV1.sol delete mode 100644 contracts/SoundEdition/SoundNftV1.sol delete mode 100644 contracts/modules/Minting/EditionMintControllers.sol create mode 100644 contracts/modules/Minting/EditionMinter.sol create mode 160000 lib/solady create mode 100644 tests/modules/Minting/FixedPricePublicSaleMinter.t.sol diff --git a/.gitmodules b/.gitmodules index 86c7aad0..c17bc015 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ path = lib/ERC721A-Upgradeable url = https://github.com/chiru-labs/ERC721A-Upgradeable branch = main +[submodule "lib/solady"] + path = lib/solady + url = https://github.com/vectorized/solady diff --git a/contracts/SoundEdition/ISoundNftV1.sol b/contracts/SoundEdition/ISoundNftV1.sol deleted file mode 100644 index 1abb2fa8..00000000 --- a/contracts/SoundEdition/ISoundNftV1.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -pragma solidity ^0.8.15; - -import "chiru-labs/ERC721A-Upgradeable/IERC721AUpgradeable.sol"; -import "openzeppelin-upgradeable/access/OwnableUpgradeable.sol"; - -/* - ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ - ▒███████████████████████████████████████████████████████████ - ▒███████████████████████████████████████████████████████████ - ▒▓▓▓▓▓▓▓▓▓▓▓▓▓████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████████████████████████████▓▒▒▒▒▒▒▒▒▒▒▒▒▒ - █████████████████████████████▓ ████████████████████████████████████████████ - █████████████████████████████▓ ████████████████████████████████████████████ - █████████████████████████████▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒██████████████████████████████ - █████████████████████████████▓ ▒█████████████████████████████ - █████████████████████████████▓ ▒████████████████████████████ - █████████████████████████████████████████████████████████▓ - ███████████████████████████████████████████████████████████ - ███████████████████████████████████████████████████████████▒ - ███████████████████████████████████████████████████████████▒ - ▓██████████████████████████████████████████████████████████▒ - ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓███████████████████████████████▒ - █████████████████████████████ ▒█████████████████████████████▒ - ██████████████████████████████ ▒█████████████████████████████▒ - ██████████████████████████████▓▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒█████████████████████████████▒ - ████████████████████████████████████████████▒ ▒█████████████████████████████▒ - ████████████████████████████████████████████▒ ▒█████████████████████████████▒ - ▒▒▒▒▒▒▒▒▒▒▒▒▒▒███████████████████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓███████████████▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒ - ▓██████████████████████████████████████████████████████████▒ - ▓██████████████████████████████████████████████████████████ -*/ - -/// @title ISoundNftV1 -/// @author Sound.xyz -interface ISoundNftV1 is IERC721AUpgradeable { - function initialize(string memory _name, string memory _symbol) external; - - function mint(address to, uint256 quantity) external payable; -} diff --git a/contracts/SoundEdition/SoundEditionV1.sol b/contracts/SoundEdition/SoundEditionV1.sol index 74cb07f6..fcd5b6d7 100644 --- a/contracts/SoundEdition/SoundEditionV1.sol +++ b/contracts/SoundEdition/SoundEditionV1.sol @@ -51,7 +51,7 @@ contract SoundEditionV1 is ERC721AUpgradeable, IERC2981Upgradeable, OwnableUpgra __ERC721A_init(_name, _symbol); __Ownable_init(); __AccessControl_init(); - + // Set ownership to owner transferOwnership(_owner); diff --git a/contracts/SoundEdition/SoundNftV1.sol b/contracts/SoundEdition/SoundNftV1.sol deleted file mode 100644 index 597b6355..00000000 --- a/contracts/SoundEdition/SoundNftV1.sol +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -pragma solidity ^0.8.15; - -import "chiru-labs/ERC721A-Upgradeable/ERC721AUpgradeable.sol"; -import "openzeppelin-upgradeable/access/OwnableUpgradeable.sol"; -import "openzeppelin-upgradeable/access/AccessControlUpgradeable.sol"; - -/* - ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ - ▒███████████████████████████████████████████████████████████ - ▒███████████████████████████████████████████████████████████ - ▒▓▓▓▓▓▓▓▓▓▓▓▓▓████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████████████████████████████▓▒▒▒▒▒▒▒▒▒▒▒▒▒ - █████████████████████████████▓ ████████████████████████████████████████████ - █████████████████████████████▓ ████████████████████████████████████████████ - █████████████████████████████▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒██████████████████████████████ - █████████████████████████████▓ ▒█████████████████████████████ - █████████████████████████████▓ ▒████████████████████████████ - █████████████████████████████████████████████████████████▓ - ███████████████████████████████████████████████████████████ - ███████████████████████████████████████████████████████████▒ - ███████████████████████████████████████████████████████████▒ - ▓██████████████████████████████████████████████████████████▒ - ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓███████████████████████████████▒ - █████████████████████████████ ▒█████████████████████████████▒ - ██████████████████████████████ ▒█████████████████████████████▒ - ██████████████████████████████▓▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒█████████████████████████████▒ - ████████████████████████████████████████████▒ ▒█████████████████████████████▒ - ████████████████████████████████████████████▒ ▒█████████████████████████████▒ - ▒▒▒▒▒▒▒▒▒▒▒▒▒▒███████████████████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓███████████████▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒ - ▓██████████████████████████████████████████████████████████▒ - ▓██████████████████████████████████████████████████████████ -*/ - -/// @title SoundNftV1 -/// @author Sound.xyz -contract SoundNftV1 is ERC721AUpgradeable, OwnableUpgradeable, AccessControlUpgradeable { - bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); - - function initialize(string memory _name, string memory _symbol) - public - initializerERC721A - initializer - { - __ERC721A_init(_name, _symbol); - __Ownable_init(); - __AccessControl_init(); - _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); - } - - function mint(address to, uint256 quantity) public payable onlyRole(MINTER_ROLE) { - _mint(to, quantity); - } - - function supportsInterface(bytes4 interfaceId) - public - view - override(ERC721AUpgradeable, AccessControlUpgradeable) - returns (bool) - { - return - ERC721AUpgradeable.supportsInterface(interfaceId) || - AccessControlUpgradeable.supportsInterface(interfaceId); - } -} diff --git a/contracts/modules/Minting/EditionMintControllers.sol b/contracts/modules/Minting/EditionMintControllers.sol deleted file mode 100644 index 6397a9da..00000000 --- a/contracts/modules/Minting/EditionMintControllers.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -pragma solidity ^0.8.15; - -contract EditionMintControllers { - - event EditionMintControllerUpdated(address indexed edition, address indexed newController); - - mapping(address => address) private _controllers; - - modifier onlyEditionMintController(address edition) virtual { - require(msg.sender == _controllers[edition], "Unauthorized."); - _; - } - - function _initEditionMintController(address edition) internal { - _initEditionMintController(edition, msg.sender); - } - - function _initEditionMintController(address edition, address editionMintController) internal { - require(editionMintController != address(0), "Edition mint controller cannot be the zero address."); - require(_controllers[edition] == address(0), "Edition mint controller already exists."); - - _controllers[edition] = editionMintController; - - emit EditionMintControllerUpdated(edition, editionMintController); - } - - function _deleteEditionMintController(address edition) internal { - require(_controllers[edition] != address(0), "Edition mint controller does not exist."); - - delete _controllers[edition]; - - emit EditionMintControllerUpdated(edition, address(0)); - } - - function _deleteEditionMintController() internal { - _deleteEditionMintController(msg.sender); - } - - function _editionMintController(address edition) internal view returns (address) { - return _controllers[edition]; - } - - function setEditionMintController( - address edition, - address newController - ) public virtual onlyEditionMintController(edition) { - require(newController != address(0), ""); - - _controllers[edition] = newController; - emit EditionMintControllerUpdated(edition, newController); - } -} diff --git a/contracts/modules/Minting/EditionMinter.sol b/contracts/modules/Minting/EditionMinter.sol new file mode 100644 index 00000000..3ea5851e --- /dev/null +++ b/contracts/modules/Minting/EditionMinter.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.15; + +contract EditionMinter { + error MintControllerUnauthorized(); + + error MintControllerSetToZeroAddress(); + + error MintNotFound(); + + error MintAlreadyExists(); + + event MintControllerUpdated(address indexed edition, address indexed controller); + + mapping(address => address) private _controllers; + + modifier onlyEditionMintController(address edition) virtual { + address controller = _controllers[edition]; + if (controller == address(0)) revert MintNotFound(); + if (msg.sender != controller) revert MintControllerUnauthorized(); + _; + } + + function _createEditionMint(address edition) internal { + _createEditionMint(edition, msg.sender); + } + + function _createEditionMint(address edition, address controller) internal { + if (controller == address(0)) revert MintControllerSetToZeroAddress(); + if (_controllers[edition] != address(0)) revert MintAlreadyExists(); + + _controllers[edition] = controller; + + emit MintControllerUpdated(edition, controller); + } + + function _deleteEditionMint(address edition) internal { + address controller = _controllers[edition]; + if (controller == address(0)) revert MintNotFound(); + if (msg.sender != controller) revert MintControllerUnauthorized(); + delete _controllers[edition]; + emit MintControllerUpdated(edition, address(0)); + } + + function editionMintController(address edition) public view returns (address) { + return _controllers[edition]; + } + + function setEditionMintController(address edition, address controller) + public + virtual + onlyEditionMintController(edition) + { + if (controller == address(0)) revert MintControllerSetToZeroAddress(); + + _controllers[edition] = controller; + emit MintControllerUpdated(edition, controller); + } +} diff --git a/contracts/modules/Minting/FixedPricePermissionedSaleMinter.sol b/contracts/modules/Minting/FixedPricePermissionedSaleMinter.sol index 5a9a49cb..239f6f89 100644 --- a/contracts/modules/Minting/FixedPricePermissionedSaleMinter.sol +++ b/contracts/modules/Minting/FixedPricePermissionedSaleMinter.sol @@ -2,10 +2,26 @@ pragma solidity ^0.8.15; -import "./EditionMintControllers.sol"; +import "./EditionMinter.sol"; import "../../SoundEdition/ISoundEditionV1.sol"; +import "solady/utils/ECDSA.sol"; -contract FixedPricePermissionedMinter is EditionMintControllers { +contract FixedPricePermissionedMinter is EditionMinter { + using ECDSA for bytes32; + + error MintWithWrongEtherValue(); + + error MintOutOfStock(); + + error MintWithInvalidSignature(); + + // prettier-ignore + event FixedPricePermissionedMintCreated( + address indexed edition, + uint256 price, + address signer, + uint32 maxMinted + ); struct EditionMintData { // The price at which each token will be sold, in ETH. @@ -19,31 +35,45 @@ contract FixedPricePermissionedMinter is EditionMintControllers { } mapping(address => EditionMintData) public editionMintData; - + function createEditionMint( address edition, uint256 price, address signer, uint32 maxMinted ) public { - _initEditionMintController(edition); + _createEditionMint(edition); EditionMintData storage data = editionMintData[edition]; data.price = price; data.signer = signer; data.maxMinted = maxMinted; + // prettier-ignore + emit FixedPricePermissionedMintCreated( + edition, + price, + signer, + maxMinted + ); } - function deleteMintee(address edition) public onlyEditionMintController(edition) { - _deleteEditionMintController(); + function deleteEditionMint(address edition) public { + _deleteEditionMint(edition); delete editionMintData[edition]; } - function mint(address edition, uint256 quantity) public payable { - // EditionMintData storage data = editionMintData[edition]; - // require(data.startTime <= block.timestamp, "Mint not started."); - // require(data.endTime > block.timestamp, "Mint has ended."); - // require(data.price * quantity == msg.value, "Wrong ether value."); - // require((data.totalMinted += quantity) <= data.maxMinted, "No more mints."); - ISoundEditionV1(edition).mint{value: msg.value}(edition, quantity); + function mint( + address edition, + uint32 quantity, + bytes calldata signature + ) public payable { + EditionMintData storage data = editionMintData[edition]; + if ((data.totalMinted += quantity) > data.maxMinted) revert MintOutOfStock(); + if (data.price * quantity != msg.value) revert MintWithWrongEtherValue(); + + bytes32 hash = keccak256(abi.encode(msg.sender, edition)); + hash = hash.toEthSignedMessageHash(); + if (hash.recover(signature) != data.signer) revert MintWithInvalidSignature(); + + ISoundEditionV1(edition).mint{ value: msg.value }(edition, quantity); } } diff --git a/contracts/modules/Minting/FixedPricePublicSaleMinter.sol b/contracts/modules/Minting/FixedPricePublicSaleMinter.sol index 24013f8b..07b3d9d4 100644 --- a/contracts/modules/Minting/FixedPricePublicSaleMinter.sol +++ b/contracts/modules/Minting/FixedPricePublicSaleMinter.sol @@ -2,10 +2,26 @@ pragma solidity ^0.8.15; -import "./EditionMintControllers.sol"; +import "./EditionMinter.sol"; import "../../SoundEdition/ISoundEditionV1.sol"; -contract FixedPricePublicSaleMinter is EditionMintControllers { +contract FixedPricePublicSaleMinter is EditionMinter { + error MintWithWrongEtherValue(); + + error MintOutOfStock(); + + error MintNotStarted(); + + error MintHasEnded(); + + // prettier-ignore + event FixedPricePublicSaleMintCreated( + address indexed edition, + uint256 price, + uint32 startTime, + uint32 endTime, + uint32 maxMinted + ); struct EditionMintData { // The price at which each token will be sold, in ETH. @@ -21,33 +37,41 @@ contract FixedPricePublicSaleMinter is EditionMintControllers { } mapping(address => EditionMintData) public editionMintData; - + function createEditionMint( address edition, uint256 price, - uint32 startTime, - uint32 endTime, + uint32 startTime, + uint32 endTime, uint32 maxMinted ) public { - _initEditionMintController(edition); + _createEditionMint(edition); EditionMintData storage data = editionMintData[edition]; data.price = price; data.startTime = startTime; data.endTime = endTime; data.maxMinted = maxMinted; + // prettier-ignore + emit FixedPricePublicSaleMintCreated( + edition, + price, + startTime, + endTime, + maxMinted + ); } - function deleteEditionMint(address edition) public onlyEditionMintController(edition) { - _deleteEditionMintController(); + function deleteEditionMint(address edition) public { + _deleteEditionMint(edition); delete editionMintData[edition]; } function mint(address edition, uint32 quantity) public payable { EditionMintData storage data = editionMintData[edition]; - require(data.startTime <= block.timestamp, "Mint not started."); - require(data.endTime > block.timestamp, "Mint has ended."); - require(data.price * quantity == msg.value, "Wrong ether value."); - require((data.totalMinted += quantity) <= data.maxMinted, "No more mints."); - ISoundEditionV1(edition).mint{value: msg.value}(edition, quantity); + if ((data.totalMinted += quantity) > data.maxMinted) revert MintOutOfStock(); + if (data.price * quantity != msg.value) revert MintWithWrongEtherValue(); + if (block.timestamp < data.startTime) revert MintNotStarted(); + if (data.endTime < block.timestamp) revert MintHasEnded(); + ISoundEditionV1(edition).mint{ value: msg.value }(edition, quantity); } } diff --git a/lib/solady b/lib/solady new file mode 160000 index 00000000..581b9f3e --- /dev/null +++ b/lib/solady @@ -0,0 +1 @@ +Subproject commit 581b9f3e19131c82f2d341f173acc4ca222daa27 diff --git a/remappings.txt b/remappings.txt index f7562c20..a94c0e78 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,4 @@ openzeppelin/=./lib/openzeppelin-contracts/contracts/ openzeppelin-upgradeable/=./lib/openzeppelin-contracts-upgradeable/contracts/ -chiru-labs/ERC721A-Upgradeable/=./lib/ERC721A-Upgradeable/contracts/ \ No newline at end of file +chiru-labs/ERC721A-Upgradeable/=./lib/ERC721A-Upgradeable/contracts/ +solady/=./lib/solady/src/ \ No newline at end of file diff --git a/tests/modules/Minting/FixedPricePublicSaleMinter.t.sol b/tests/modules/Minting/FixedPricePublicSaleMinter.t.sol new file mode 100644 index 00000000..94a01378 --- /dev/null +++ b/tests/modules/Minting/FixedPricePublicSaleMinter.t.sol @@ -0,0 +1,224 @@ +pragma solidity ^0.8.15; + +import "../../TestConfig.sol"; +import "../../../contracts/SoundEdition/SoundEditionV1.sol"; +import "../../../contracts/SoundCreator/SoundCreatorV1.sol"; +import "../../../contracts/modules/Minting/FixedPricePublicSaleMinter.sol"; + +contract FixedPricePublicSaleMinterTests is TestConfig { + + uint256 constant PRICE = 1; + + uint32 constant START_TIME = 100; + + uint32 constant END_TIME = 200; + + uint32 constant MAX_MINTED = 5; + + // prettier-ignore + event FixedPricePublicSaleMintCreated( + address indexed edition, + uint256 price, + uint32 startTime, + uint32 endTime, + uint32 maxMinted + ); + + event MintControllerUpdated(address indexed edition, address indexed controller); + + function _createEditionAndMinter() + internal + returns (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) + { + edition = SoundEditionV1( + soundCreator.createSound(SONG_NAME, SONG_SYMBOL) + ); + + minter = new FixedPricePublicSaleMinter(); + + edition.grantRole(edition.MINTER_ROLE(), address(minter)); + + minter.createEditionMint( + address(edition), + PRICE, + START_TIME, + END_TIME, + MAX_MINTED + ); + } + + function test_createEditionMintEmitsEvent() public { + SoundEditionV1 edition = SoundEditionV1( + soundCreator.createSound(SONG_NAME, SONG_SYMBOL) + ); + + FixedPricePublicSaleMinter minter = new FixedPricePublicSaleMinter(); + + vm.expectEmit(false, false, false, true); + + emit FixedPricePublicSaleMintCreated( + address(edition), + PRICE, + START_TIME, + END_TIME, + MAX_MINTED + ); + + minter.createEditionMint( + address(edition), + PRICE, + START_TIME, + END_TIME, + MAX_MINTED + ); + } + + function test_createEditionMintEmitsMintControllerUpdatedEvent() public { + SoundEditionV1 edition = SoundEditionV1( + soundCreator.createSound(SONG_NAME, SONG_SYMBOL) + ); + + FixedPricePublicSaleMinter minter = new FixedPricePublicSaleMinter(); + + vm.expectEmit(false, false, false, true); + + emit MintControllerUpdated(address(edition), edition.owner()); + + minter.createEditionMint( + address(edition), + PRICE, + START_TIME, + END_TIME, + MAX_MINTED + ); + } + + function test_createEditionMintRevertsIfMintEditionExists() public { + (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); + + vm.expectRevert(EditionMinter.MintAlreadyExists.selector); + + minter.createEditionMint( + address(edition), + PRICE, + START_TIME, + END_TIME, + MAX_MINTED + ); + } + + function test_deleteEditionMintRevertsIfCallerUnauthorized() public { + (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); + + address caller = getRandomAccount(1); + vm.prank(caller); + vm.expectRevert(EditionMinter.MintControllerUnauthorized.selector); + minter.deleteEditionMint(address(edition)); + + minter.setEditionMintController(address(edition), caller); + vm.prank(caller); + minter.deleteEditionMint(address(edition)); + } + + function test_deleteEditionMintRevertsIfMintEditionDoesNotExist() public { + (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); + + minter.deleteEditionMint(address(edition)); + + vm.expectRevert(EditionMinter.MintNotFound.selector); + + minter.deleteEditionMint(address(edition)); + } + + function test_mintBeforeStartTimeReverts() public { + (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); + + vm.warp(START_TIME - 1); + + address caller = getRandomAccount(1); + vm.prank(caller); + vm.expectRevert(FixedPricePublicSaleMinter.MintNotStarted.selector); + minter.mint{ value: PRICE }(address(edition), 1); + + vm.warp(START_TIME); + vm.prank(caller); + minter.mint{ value: PRICE }(address(edition), 1); + } + + function test_mintAfterStartTimeReverts() public { + (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); + + vm.warp(END_TIME + 1); + + address caller = getRandomAccount(1); + vm.prank(caller); + vm.expectRevert(FixedPricePublicSaleMinter.MintHasEnded.selector); + minter.mint{ value: PRICE }(address(edition), 1); + + vm.warp(END_TIME); + vm.prank(caller); + minter.mint{ value: PRICE }(address(edition), 1); + } + + function test_mintDuringOutOfStockReverts() public { + (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); + + vm.warp(START_TIME); + + address caller = getRandomAccount(1); + vm.prank(caller); + vm.expectRevert(FixedPricePublicSaleMinter.MintOutOfStock.selector); + minter.mint{ value: PRICE * (MAX_MINTED + 1) }(address(edition), MAX_MINTED + 1); + + vm.prank(caller); + minter.mint{ value: PRICE * MAX_MINTED }(address(edition), MAX_MINTED); + + vm.prank(caller); + vm.expectRevert(FixedPricePublicSaleMinter.MintOutOfStock.selector); + minter.mint{ value: PRICE }(address(edition), 1); + } + + function test_mintWithWrongEtherValueReverts() public { + (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); + + vm.warp(START_TIME); + + address caller = getRandomAccount(1); + vm.prank(caller); + vm.expectRevert(FixedPricePublicSaleMinter.MintWithWrongEtherValue.selector); + minter.mint{ value: PRICE * 2 }(address(edition), 1); + } + + function test_mintWithUnauthorizedMinterReverts() public { + (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); + + vm.warp(START_TIME); + + address caller = getRandomAccount(1); + + bool status; + + vm.prank(caller); + (status, ) = address(minter).call{ value: PRICE }( + abi.encodeWithSelector( + FixedPricePublicSaleMinter.mint.selector, + address(edition), + 1 + ) + ); + assertTrue(status); + + vm.prank(edition.owner()); + edition.revokeRole(edition.MINTER_ROLE(), address(minter)); + + vm.prank(caller); + (status, ) = address(minter).call{ value: PRICE }( + abi.encodeWithSelector( + FixedPricePublicSaleMinter.mint.selector, + address(edition), + 1 + ) + ); + assertFalse(status); + } +}