From 998f39a0a9bdff66b59196bfd5c54833bbbf44c6 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 21 Jul 2022 17:19:33 +0000 Subject: [PATCH 01/17] Add initial code --- contracts/SoundEdition/ISoundEditionV1.sol | 2 + contracts/SoundEdition/ISoundNftV1.sol | 40 ++++++++++++ contracts/SoundEdition/SoundEditionV1.sol | 27 +++++++- contracts/SoundEdition/SoundNftV1.sol | 65 +++++++++++++++++++ .../Minting/EditionMintControllers.sol | 54 +++++++++++++++ .../FixedPricePermissionedSaleMinter.sol | 49 ++++++++++++++ .../Minting/FixedPricePublicSaleMinter.sol | 53 +++++++++++++++ 7 files changed, 287 insertions(+), 3 deletions(-) create mode 100644 contracts/SoundEdition/ISoundNftV1.sol create mode 100644 contracts/SoundEdition/SoundNftV1.sol create mode 100644 contracts/modules/Minting/EditionMintControllers.sol create mode 100644 contracts/modules/Minting/FixedPricePermissionedSaleMinter.sol create mode 100644 contracts/modules/Minting/FixedPricePublicSaleMinter.sol diff --git a/contracts/SoundEdition/ISoundEditionV1.sol b/contracts/SoundEdition/ISoundEditionV1.sol index 5b83bfb1..ccea07b7 100644 --- a/contracts/SoundEdition/ISoundEditionV1.sol +++ b/contracts/SoundEdition/ISoundEditionV1.sol @@ -43,4 +43,6 @@ interface ISoundEditionV1 is IERC721AUpgradeable { string memory baseURI_, string memory _contractURI ) external; + + function mint(address _to, uint256 _quantity) external payable; } diff --git a/contracts/SoundEdition/ISoundNftV1.sol b/contracts/SoundEdition/ISoundNftV1.sol new file mode 100644 index 00000000..1abb2fa8 --- /dev/null +++ b/contracts/SoundEdition/ISoundNftV1.sol @@ -0,0 +1,40 @@ +// 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 b6888039..96ea7979 100644 --- a/contracts/SoundEdition/SoundEditionV1.sol +++ b/contracts/SoundEdition/SoundEditionV1.sol @@ -6,6 +6,7 @@ import "chiru-labs/ERC721A-Upgradeable/extensions/ERC721AQueryableUpgradeable.so import "openzeppelin-upgradeable/access/OwnableUpgradeable.sol"; import "openzeppelin-upgradeable/interfaces/IERC2981Upgradeable.sol"; import "../modules/Metadata/IMetadataModule.sol"; +import "openzeppelin-upgradeable/access/AccessControlUpgradeable.sol"; /* ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ @@ -35,7 +36,12 @@ import "../modules/Metadata/IMetadataModule.sol"; /// @title SoundEditionV1 /// @author Sound.xyz -contract SoundEditionV1 is ERC721AQueryableUpgradeable, IERC2981Upgradeable, OwnableUpgradeable { +contract SoundEditionV1 is ERC721AQueryableUpgradeable, IERC2981Upgradeable, OwnableUpgradeable, AccessControlUpgradeable { + // ================================ + // CONSTANTS + // ================================ + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + // ================================ // STORAGE // ================================ @@ -83,8 +89,13 @@ contract SoundEditionV1 is ERC721AQueryableUpgradeable, IERC2981Upgradeable, Own baseURI = baseURI_; contractURI = _contractURI; + __AccessControl_init(); + // Set ownership to owner transferOwnership(_owner); + + // Give owner the DEFAULT_ADMIN_ROLE + _grantRole(DEFAULT_ADMIN_ROLE, _owner); } function setMetadataModule(IMetadataModule _metadataModule) external onlyOwner { @@ -141,10 +152,12 @@ contract SoundEditionV1 is ERC721AQueryableUpgradeable, IERC2981Upgradeable, Own function supportsInterface(bytes4 _interfaceId) public view - override(ERC721AUpgradeable, IERC721AUpgradeable, IERC165Upgradeable) + override(ERC721AUpgradeable, IERC721AUpgradeable, AccessControlUpgradeable, IERC165Upgradeable) returns (bool) { - // todo + return + ERC721AUpgradeable.supportsInterface(_interfaceId) || + AccessControlUpgradeable.supportsInterface(_interfaceId); } /// @notice Get royalty information for token @@ -158,4 +171,12 @@ contract SoundEditionV1 is ERC721AQueryableUpgradeable, IERC2981Upgradeable, Own { // todo } + + /// @notice Mints `_quantity` tokens to addrress `_to` + /// Each token will be assigned a token ID that is consecutively increasing + /// @param _to Address to mint to + /// @param _quantity Number of tokens to mint + function mint(address _to, uint256 _quantity) public payable onlyRole(MINTER_ROLE) { + _mint(_to, _quantity); + } } diff --git a/contracts/SoundEdition/SoundNftV1.sol b/contracts/SoundEdition/SoundNftV1.sol new file mode 100644 index 00000000..597b6355 --- /dev/null +++ b/contracts/SoundEdition/SoundNftV1.sol @@ -0,0 +1,65 @@ +// 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 new file mode 100644 index 00000000..6397a9da --- /dev/null +++ b/contracts/modules/Minting/EditionMintControllers.sol @@ -0,0 +1,54 @@ +// 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/FixedPricePermissionedSaleMinter.sol b/contracts/modules/Minting/FixedPricePermissionedSaleMinter.sol new file mode 100644 index 00000000..5a9a49cb --- /dev/null +++ b/contracts/modules/Minting/FixedPricePermissionedSaleMinter.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.15; + +import "./EditionMintControllers.sol"; +import "../../SoundEdition/ISoundEditionV1.sol"; + +contract FixedPricePermissionedMinter is EditionMintControllers { + + struct EditionMintData { + // The price at which each token will be sold, in ETH. + uint256 price; + // Whitelist signer address. + address signer; + // The maximum number of tokens that can can be minted for this sale. + uint32 maxMinted; + // The total number of tokens minted so far for this sale. + uint32 totalMinted; + } + + mapping(address => EditionMintData) public editionMintData; + + function createEditionMint( + address edition, + uint256 price, + address signer, + uint32 maxMinted + ) public { + _initEditionMintController(edition); + EditionMintData storage data = editionMintData[edition]; + data.price = price; + data.signer = signer; + data.maxMinted = maxMinted; + } + + function deleteMintee(address edition) public onlyEditionMintController(edition) { + _deleteEditionMintController(); + 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); + } +} diff --git a/contracts/modules/Minting/FixedPricePublicSaleMinter.sol b/contracts/modules/Minting/FixedPricePublicSaleMinter.sol new file mode 100644 index 00000000..24013f8b --- /dev/null +++ b/contracts/modules/Minting/FixedPricePublicSaleMinter.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.15; + +import "./EditionMintControllers.sol"; +import "../../SoundEdition/ISoundEditionV1.sol"; + +contract FixedPricePublicSaleMinter is EditionMintControllers { + + struct EditionMintData { + // The price at which each token will be sold, in ETH. + uint256 price; + // Start timestamp of sale (in seconds since unix epoch). + uint32 startTime; + // End timestamp of sale (in seconds since unix epoch). + uint32 endTime; + // The maximum number of tokens that can can be minted for this sale. + uint32 maxMinted; + // The total number of tokens minted so far for this sale. + uint32 totalMinted; + } + + mapping(address => EditionMintData) public editionMintData; + + function createEditionMint( + address edition, + uint256 price, + uint32 startTime, + uint32 endTime, + uint32 maxMinted + ) public { + _initEditionMintController(edition); + EditionMintData storage data = editionMintData[edition]; + data.price = price; + data.startTime = startTime; + data.endTime = endTime; + data.maxMinted = maxMinted; + } + + function deleteEditionMint(address edition) public onlyEditionMintController(edition) { + _deleteEditionMintController(); + 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); + } +} From 9b22673e1672ff146fc02ed1e7d7eadadfa0b175 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 21 Jul 2022 21:11:45 +0000 Subject: [PATCH 02/17] 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 96ea7979..347b6368 100644 --- a/contracts/SoundEdition/SoundEditionV1.sol +++ b/contracts/SoundEdition/SoundEditionV1.sol @@ -90,7 +90,7 @@ contract SoundEditionV1 is ERC721AQueryableUpgradeable, IERC2981Upgradeable, Own contractURI = _contractURI; __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); + } +} From b0d7ec216a59d8713a4b5c5a63587fe7c471643f Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 21 Jul 2022 22:22:10 +0000 Subject: [PATCH 03/17] Add more tests --- contracts/modules/Minters/EditionMinter.sol | 55 +++++ .../FixedPricePermissionedSaleMinter.sol | 79 +++++++ .../Minters/FixedPricePublicSaleMinter.sol | 77 +++++++ tests/SoundCreator.t.sol | 10 +- tests/TestConfig.sol | 9 +- .../FixedPricePermissionedSaleMinter.t.sol | 197 +++++++++++++++++ .../Minters/FixedPricePublicSaleMinter.t.sol | 198 ++++++++++++++++++ .../Minting/FixedPricePublicSaleMinter.t.sol | 86 ++------ 8 files changed, 629 insertions(+), 82 deletions(-) create mode 100644 contracts/modules/Minters/EditionMinter.sol create mode 100644 contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol create mode 100644 contracts/modules/Minters/FixedPricePublicSaleMinter.sol create mode 100644 tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol create mode 100644 tests/modules/Minters/FixedPricePublicSaleMinter.t.sol diff --git a/contracts/modules/Minters/EditionMinter.sol b/contracts/modules/Minters/EditionMinter.sol new file mode 100644 index 00000000..4f754f7e --- /dev/null +++ b/contracts/modules/Minters/EditionMinter.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.15; + +abstract 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 virtual { + if (_controllers[edition] != address(0)) revert MintAlreadyExists(); + + _controllers[edition] = msg.sender; + + emit MintControllerUpdated(edition, msg.sender); + } + + function _deleteEditionMint(address edition) internal virtual { + 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/Minters/FixedPricePermissionedSaleMinter.sol b/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol new file mode 100644 index 00000000..48780f1d --- /dev/null +++ b/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.15; + +import "./EditionMinter.sol"; +import "../../SoundEdition/ISoundEditionV1.sol"; +import "solady/utils/ECDSA.sol"; + +contract FixedPricePermissionedSaleMinter 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. + uint256 price; + // Whitelist signer address. + address signer; + // The maximum number of tokens that can can be minted for this sale. + uint32 maxMinted; + // The total number of tokens minted so far for this sale. + uint32 totalMinted; + } + + mapping(address => EditionMintData) public editionMintData; + + function createEditionMint( + address edition, + uint256 price, + address signer, + uint32 maxMinted + ) public { + _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 deleteEditionMint(address edition) public { + _deleteEditionMint(edition); + delete editionMintData[edition]; + } + + 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/Minters/FixedPricePublicSaleMinter.sol b/contracts/modules/Minters/FixedPricePublicSaleMinter.sol new file mode 100644 index 00000000..07b3d9d4 --- /dev/null +++ b/contracts/modules/Minters/FixedPricePublicSaleMinter.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.15; + +import "./EditionMinter.sol"; +import "../../SoundEdition/ISoundEditionV1.sol"; + +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. + uint256 price; + // Start timestamp of sale (in seconds since unix epoch). + uint32 startTime; + // End timestamp of sale (in seconds since unix epoch). + uint32 endTime; + // The maximum number of tokens that can can be minted for this sale. + uint32 maxMinted; + // The total number of tokens minted so far for this sale. + uint32 totalMinted; + } + + mapping(address => EditionMintData) public editionMintData; + + function createEditionMint( + address edition, + uint256 price, + uint32 startTime, + uint32 endTime, + uint32 maxMinted + ) public { + _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 { + _deleteEditionMint(edition); + delete editionMintData[edition]; + } + + function mint(address edition, uint32 quantity) public payable { + EditionMintData storage data = editionMintData[edition]; + 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/tests/SoundCreator.t.sol b/tests/SoundCreator.t.sol index 8a30d963..dc5c0dbb 100644 --- a/tests/SoundCreator.t.sol +++ b/tests/SoundCreator.t.sol @@ -10,18 +10,12 @@ contract SoundCreatorTests is TestConfig { function test_deploysSoundCreator() public { SoundEditionV1 soundNftImplementation = new SoundEditionV1(); address soundRegistry = address(123); - SoundCreatorV1 _soundCreator = new SoundCreatorV1( - address(soundNftImplementation), - soundRegistry - ); + SoundCreatorV1 _soundCreator = new SoundCreatorV1(address(soundNftImplementation), soundRegistry); assert(address(_soundCreator) != address(0)); assertEq(address(_soundCreator.soundRegistry()), soundRegistry); - assertEq( - address(_soundCreator.nftImplementation()), - address(soundNftImplementation) - ); + assertEq(address(_soundCreator.nftImplementation()), address(soundNftImplementation)); } // Tests that the factory creates a new sound NFT diff --git a/tests/TestConfig.sol b/tests/TestConfig.sol index a585f26e..e2250b6f 100644 --- a/tests/TestConfig.sol +++ b/tests/TestConfig.sol @@ -26,17 +26,12 @@ contract TestConfig is Test { // todo: deploy registry here address soundRegistry = address(123); - soundCreator = new SoundCreatorV1( - address(soundNftImplementation), - soundRegistry - ); + soundCreator = new SoundCreatorV1(address(soundNftImplementation), soundRegistry); } // Returns a random address funded with ETH function getRandomAccount(uint256 num) public returns (address) { - address addr = address( - uint160(uint256(keccak256(abi.encodePacked(num)))) - ); + address addr = address(uint160(uint256(keccak256(abi.encodePacked(num))))); // Fund with some ETH vm.deal(addr, 1e19); diff --git a/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol b/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol new file mode 100644 index 00000000..6ce9f136 --- /dev/null +++ b/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol @@ -0,0 +1,197 @@ +pragma solidity ^0.8.15; + +import "../../TestConfig.sol"; +import "../../../contracts/SoundEdition/SoundEditionV1.sol"; +import "../../../contracts/SoundCreator/SoundCreatorV1.sol"; +import "../../../contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol"; + +contract FixedPricePermissionedSaleMinterTests is TestConfig { + uint256 constant PRICE = 1; + + uint32 constant START_TIME = 100; + + uint32 constant END_TIME = 200; + + uint32 constant MAX_MINTED = 5; + + // prettier-ignore + event FixedPricePermissionedMintCreated( + address indexed edition, + uint256 price, + address signer, + uint32 maxMinted + ); + + function _createEditionAndMinter() + internal + returns (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) + { + edition = SoundEditionV1( + soundCreator.createSound(SONG_NAME, SONG_SYMBOL) + ); + + minter = new FixedPricePermissionedSaleMinter(); + + edition.grantRole(edition.MINTER_ROLE(), address(minter)); + + minter.createEditionMint( + address(edition), + PRICE, + edition.owner(), + MAX_MINTED + ); + } + + function test_createEditionMintEmitsEvent() public { + SoundEditionV1 edition = SoundEditionV1( + soundCreator.createSound(SONG_NAME, SONG_SYMBOL) + ); + + FixedPricePermissionedSaleMinter minter = new FixedPricePermissionedSaleMinter(); + + vm.expectEmit(false, false, false, true); + + emit FixedPricePermissionedMintCreated( + address(edition), + PRICE, + edition.owner(), + MAX_MINTED + ); + + minter.createEditionMint( + address(edition), + PRICE, + edition.owner(), + MAX_MINTED + ); + } + + // function test_createEditionMintRevertsIfMintEditionExists() public { + // (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); + + // vm.expectRevert(EditionMinter.MintAlreadyExists.selector); + + // minter.createEditionMint( + // address(edition), + // PRICE, + // START_TIME, + // END_TIME, + // MAX_MINTED + // ); + // } + + // function test_deleteEditionMintRevertsIfCallerUnauthorized() public { + // (SoundEditionV1 edition, FixedPricePermissionedSaleMinter 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, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); + + // minter.deleteEditionMint(address(edition)); + + // vm.expectRevert(EditionMinter.MintNotFound.selector); + + // minter.deleteEditionMint(address(edition)); + // } + + // function test_mintBeforeStartTimeReverts() public { + // (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); + + // vm.warp(START_TIME - 1); + + // address caller = getRandomAccount(1); + // vm.prank(caller); + // vm.expectRevert(FixedPricePermissionedSaleMinter.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, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); + + // vm.warp(END_TIME + 1); + + // address caller = getRandomAccount(1); + // vm.prank(caller); + // vm.expectRevert(FixedPricePermissionedSaleMinter.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, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); + + // vm.warp(START_TIME); + + // address caller = getRandomAccount(1); + // vm.prank(caller); + // vm.expectRevert(FixedPricePermissionedSaleMinter.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(FixedPricePermissionedSaleMinter.MintOutOfStock.selector); + // minter.mint{ value: PRICE }(address(edition), 1); + // } + + // function test_mintWithWrongEtherValueReverts() public { + // (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); + + // vm.warp(START_TIME); + + // address caller = getRandomAccount(1); + // vm.prank(caller); + // vm.expectRevert(FixedPricePermissionedSaleMinter.MintWithWrongEtherValue.selector); + // minter.mint{ value: PRICE * 2 }(address(edition), 1); + // } + + // function test_mintWithUnauthorizedMinterReverts() public { + // (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); + + // vm.warp(START_TIME); + + // address caller = getRandomAccount(1); + + // bool status; + + // vm.prank(caller); + // (status, ) = address(minter).call{ value: PRICE }( + // abi.encodeWithSelector( + // FixedPricePermissionedSaleMinter.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( + // FixedPricePermissionedSaleMinter.mint.selector, + // address(edition), + // 1 + // ) + // ); + // assertFalse(status); + // } +} diff --git a/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol b/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol new file mode 100644 index 00000000..15a5315d --- /dev/null +++ b/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol @@ -0,0 +1,198 @@ +pragma solidity ^0.8.15; + +import "../../TestConfig.sol"; +import "../../../contracts/SoundEdition/SoundEditionV1.sol"; +import "../../../contracts/SoundCreator/SoundCreatorV1.sol"; +import "../../../contracts/modules/Minters/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_deleteEditionMintEmitsMintControllerUpdatedEvent() public { + (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); + + vm.expectEmit(false, false, false, true); + + emit MintControllerUpdated(address(edition), address(0)); + + minter.deleteEditionMint(address(edition)); + } + + function test_setEditionMintControllerEmitsMintControllerUpdatedEvent() public { + (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); + + vm.expectEmit(false, false, false, true); + + address newController = getRandomAccount(1); + + emit MintControllerUpdated(address(edition), newController); + + minter.setEditionMintController(address(edition), newController); + } + + 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); + } +} diff --git a/tests/modules/Minting/FixedPricePublicSaleMinter.t.sol b/tests/modules/Minting/FixedPricePublicSaleMinter.t.sol index 94a01378..9f0fa96f 100644 --- a/tests/modules/Minting/FixedPricePublicSaleMinter.t.sol +++ b/tests/modules/Minting/FixedPricePublicSaleMinter.t.sol @@ -6,9 +6,8 @@ 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; @@ -26,71 +25,38 @@ contract FixedPricePublicSaleMinterTests is TestConfig { event MintControllerUpdated(address indexed edition, address indexed controller); - function _createEditionAndMinter() - internal - returns (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) - { - edition = SoundEditionV1( - soundCreator.createSound(SONG_NAME, SONG_SYMBOL) - ); - + 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 - ); + minter.createEditionMint(address(edition), PRICE, START_TIME, END_TIME, MAX_MINTED); } function test_createEditionMintEmitsEvent() public { - SoundEditionV1 edition = SoundEditionV1( - soundCreator.createSound(SONG_NAME, SONG_SYMBOL) - ); - + 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 - ); + 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) - ); - + 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 - ); + minter.createEditionMint(address(edition), PRICE, START_TIME, END_TIME, MAX_MINTED); } function test_createEditionMintRevertsIfMintEditionExists() public { @@ -98,13 +64,7 @@ contract FixedPricePublicSaleMinterTests is TestConfig { vm.expectRevert(EditionMinter.MintAlreadyExists.selector); - minter.createEditionMint( - address(edition), - PRICE, - START_TIME, - END_TIME, - MAX_MINTED - ); + minter.createEditionMint(address(edition), PRICE, START_TIME, END_TIME, MAX_MINTED); } function test_deleteEditionMintRevertsIfCallerUnauthorized() public { @@ -191,20 +151,16 @@ contract FixedPricePublicSaleMinterTests is TestConfig { 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 - ) + abi.encodeWithSelector(FixedPricePublicSaleMinter.mint.selector, address(edition), 1) ); assertTrue(status); @@ -213,11 +169,7 @@ contract FixedPricePublicSaleMinterTests is TestConfig { vm.prank(caller); (status, ) = address(minter).call{ value: PRICE }( - abi.encodeWithSelector( - FixedPricePublicSaleMinter.mint.selector, - address(edition), - 1 - ) + abi.encodeWithSelector(FixedPricePublicSaleMinter.mint.selector, address(edition), 1) ); assertFalse(status); } From a790e16f0aac3a054338f02ea3775721fd20f65f Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 21 Jul 2022 22:29:23 +0000 Subject: [PATCH 04/17] Fix tests --- contracts/modules/Minting/EditionMinter.sol | 60 ------ .../FixedPricePermissionedSaleMinter.sol | 79 -------- .../Minting/FixedPricePublicSaleMinter.sol | 77 -------- .../FixedPricePermissionedSaleMinter.t.sol | 57 +++--- .../Minters/FixedPricePublicSaleMinter.t.sol | 95 +++++++--- .../Minting/FixedPricePublicSaleMinter.t.sol | 176 ------------------ 6 files changed, 97 insertions(+), 447 deletions(-) delete mode 100644 contracts/modules/Minting/EditionMinter.sol delete mode 100644 contracts/modules/Minting/FixedPricePermissionedSaleMinter.sol delete mode 100644 contracts/modules/Minting/FixedPricePublicSaleMinter.sol delete mode 100644 tests/modules/Minting/FixedPricePublicSaleMinter.t.sol diff --git a/contracts/modules/Minting/EditionMinter.sol b/contracts/modules/Minting/EditionMinter.sol deleted file mode 100644 index 3ea5851e..00000000 --- a/contracts/modules/Minting/EditionMinter.sol +++ /dev/null @@ -1,60 +0,0 @@ -// 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 deleted file mode 100644 index 239f6f89..00000000 --- a/contracts/modules/Minting/FixedPricePermissionedSaleMinter.sol +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -pragma solidity ^0.8.15; - -import "./EditionMinter.sol"; -import "../../SoundEdition/ISoundEditionV1.sol"; -import "solady/utils/ECDSA.sol"; - -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. - uint256 price; - // Whitelist signer address. - address signer; - // The maximum number of tokens that can can be minted for this sale. - uint32 maxMinted; - // The total number of tokens minted so far for this sale. - uint32 totalMinted; - } - - mapping(address => EditionMintData) public editionMintData; - - function createEditionMint( - address edition, - uint256 price, - address signer, - uint32 maxMinted - ) public { - _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 deleteEditionMint(address edition) public { - _deleteEditionMint(edition); - delete editionMintData[edition]; - } - - 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 deleted file mode 100644 index 07b3d9d4..00000000 --- a/contracts/modules/Minting/FixedPricePublicSaleMinter.sol +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -pragma solidity ^0.8.15; - -import "./EditionMinter.sol"; -import "../../SoundEdition/ISoundEditionV1.sol"; - -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. - uint256 price; - // Start timestamp of sale (in seconds since unix epoch). - uint32 startTime; - // End timestamp of sale (in seconds since unix epoch). - uint32 endTime; - // The maximum number of tokens that can can be minted for this sale. - uint32 maxMinted; - // The total number of tokens minted so far for this sale. - uint32 totalMinted; - } - - mapping(address => EditionMintData) public editionMintData; - - function createEditionMint( - address edition, - uint256 price, - uint32 startTime, - uint32 endTime, - uint32 maxMinted - ) public { - _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 { - _deleteEditionMint(edition); - delete editionMintData[edition]; - } - - function mint(address edition, uint32 quantity) public payable { - EditionMintData storage data = editionMintData[edition]; - 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/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol b/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol index 6ce9f136..e0103c40 100644 --- a/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol +++ b/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol @@ -7,10 +7,6 @@ import "../../../contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol" contract FixedPricePermissionedSaleMinterTests is TestConfig { uint256 constant PRICE = 1; - - uint32 constant START_TIME = 100; - - uint32 constant END_TIME = 200; uint32 constant MAX_MINTED = 5; @@ -66,42 +62,41 @@ contract FixedPricePermissionedSaleMinterTests is TestConfig { ); } - // function test_createEditionMintRevertsIfMintEditionExists() public { - // (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); + function test_createEditionMintRevertsIfMintEditionExists() public { + (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); - // vm.expectRevert(EditionMinter.MintAlreadyExists.selector); + vm.expectRevert(EditionMinter.MintAlreadyExists.selector); - // minter.createEditionMint( - // address(edition), - // PRICE, - // START_TIME, - // END_TIME, - // MAX_MINTED - // ); - // } + minter.createEditionMint( + address(edition), + PRICE, + edition.owner(), + MAX_MINTED + ); + } - // function test_deleteEditionMintRevertsIfCallerUnauthorized() public { - // (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); + function test_deleteEditionMintRevertsIfCallerUnauthorized() public { + (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); - // address caller = getRandomAccount(1); - // vm.prank(caller); - // vm.expectRevert(EditionMinter.MintControllerUnauthorized.selector); - // minter.deleteEditionMint(address(edition)); + 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)); - // } + minter.setEditionMintController(address(edition), caller); + vm.prank(caller); + minter.deleteEditionMint(address(edition)); + } - // function test_deleteEditionMintRevertsIfMintEditionDoesNotExist() public { - // (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); + function test_deleteEditionMintRevertsIfMintEditionDoesNotExist() public { + (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); - // minter.deleteEditionMint(address(edition)); + minter.deleteEditionMint(address(edition)); - // vm.expectRevert(EditionMinter.MintNotFound.selector); + vm.expectRevert(EditionMinter.MintNotFound.selector); - // minter.deleteEditionMint(address(edition)); - // } + minter.deleteEditionMint(address(edition)); + } // function test_mintBeforeStartTimeReverts() public { // (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); diff --git a/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol b/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol index 15a5315d..8eefcfed 100644 --- a/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol +++ b/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol @@ -7,7 +7,7 @@ import "../../../contracts/modules/Minters/FixedPricePublicSaleMinter.sol"; contract FixedPricePublicSaleMinterTests is TestConfig { uint256 constant PRICE = 1; - + uint32 constant START_TIME = 100; uint32 constant END_TIME = 200; @@ -25,38 +25,71 @@ contract FixedPricePublicSaleMinterTests is TestConfig { event MintControllerUpdated(address indexed edition, address indexed controller); - function _createEditionAndMinter() internal returns (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) { - edition = SoundEditionV1(soundCreator.createSound(SONG_NAME, SONG_SYMBOL)); - + 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); + minter.createEditionMint( + address(edition), + PRICE, + START_TIME, + END_TIME, + MAX_MINTED + ); } function test_createEditionMintEmitsEvent() public { - SoundEditionV1 edition = SoundEditionV1(soundCreator.createSound(SONG_NAME, SONG_SYMBOL)); - + 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 + ); - emit FixedPricePublicSaleMintCreated(address(edition), PRICE, START_TIME, END_TIME, MAX_MINTED); - - minter.createEditionMint(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)); - + 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); + minter.createEditionMint( + address(edition), + PRICE, + START_TIME, + END_TIME, + MAX_MINTED + ); } function test_createEditionMintRevertsIfMintEditionExists() public { @@ -64,14 +97,20 @@ contract FixedPricePublicSaleMinterTests is TestConfig { vm.expectRevert(EditionMinter.MintAlreadyExists.selector); - minter.createEditionMint(address(edition), PRICE, START_TIME, END_TIME, MAX_MINTED); + minter.createEditionMint( + address(edition), + PRICE, + START_TIME, + END_TIME, + MAX_MINTED + ); } function test_deleteEditionMintEmitsMintControllerUpdatedEvent() public { (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); - + vm.expectEmit(false, false, false, true); - + emit MintControllerUpdated(address(edition), address(0)); minter.deleteEditionMint(address(edition)); @@ -79,15 +118,15 @@ contract FixedPricePublicSaleMinterTests is TestConfig { function test_setEditionMintControllerEmitsMintControllerUpdatedEvent() public { (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); - + vm.expectEmit(false, false, false, true); - + address newController = getRandomAccount(1); emit MintControllerUpdated(address(edition), newController); minter.setEditionMintController(address(edition), newController); - } + } function test_deleteEditionMintRevertsIfCallerUnauthorized() public { (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); @@ -173,16 +212,20 @@ contract FixedPricePublicSaleMinterTests is TestConfig { 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) + abi.encodeWithSelector( + FixedPricePublicSaleMinter.mint.selector, + address(edition), + 1 + ) ); assertTrue(status); @@ -191,7 +234,11 @@ contract FixedPricePublicSaleMinterTests is TestConfig { vm.prank(caller); (status, ) = address(minter).call{ value: PRICE }( - abi.encodeWithSelector(FixedPricePublicSaleMinter.mint.selector, address(edition), 1) + abi.encodeWithSelector( + FixedPricePublicSaleMinter.mint.selector, + address(edition), + 1 + ) ); assertFalse(status); } diff --git a/tests/modules/Minting/FixedPricePublicSaleMinter.t.sol b/tests/modules/Minting/FixedPricePublicSaleMinter.t.sol deleted file mode 100644 index 9f0fa96f..00000000 --- a/tests/modules/Minting/FixedPricePublicSaleMinter.t.sol +++ /dev/null @@ -1,176 +0,0 @@ -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); - } -} From 7c74f006bf4621ac63ef4d5f88e67b1211d873b9 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 21 Jul 2022 23:06:39 +0000 Subject: [PATCH 05/17] Add more tests --- .../FixedPricePermissionedSaleMinter.t.sol | 225 ++++++++---------- .../Minters/FixedPricePublicSaleMinter.t.sol | 95 ++------ 2 files changed, 123 insertions(+), 197 deletions(-) diff --git a/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol b/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol index e0103c40..f78b70f1 100644 --- a/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol +++ b/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol @@ -6,10 +6,14 @@ import "../../../contracts/SoundCreator/SoundCreatorV1.sol"; import "../../../contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol"; contract FixedPricePermissionedSaleMinterTests is TestConfig { + using ECDSA for bytes32; + uint256 constant PRICE = 1; uint32 constant MAX_MINTED = 5; + uint256 SIGNER_PRIVATE_KEY = 1; + // prettier-ignore event FixedPricePermissionedMintCreated( address indexed edition, @@ -18,61 +22,56 @@ contract FixedPricePermissionedSaleMinterTests is TestConfig { uint32 maxMinted ); - function _createEditionAndMinter() + function _signerAddress() internal returns (address) { + return vm.addr(SIGNER_PRIVATE_KEY); + } + + function _getSignature(address caller, address edition) internal returns (bytes memory) { + bytes32 digest = keccak256(abi.encode(caller, address(edition))).toEthSignedMessageHash(); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_PRIVATE_KEY, digest); + return abi.encodePacked(r, s, v); + } + + function _createEditionAndMinter() internal returns (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) { - edition = SoundEditionV1( - soundCreator.createSound(SONG_NAME, SONG_SYMBOL) - ); - + edition = SoundEditionV1(soundCreator.createSound(SONG_NAME, SONG_SYMBOL)); + minter = new FixedPricePermissionedSaleMinter(); edition.grantRole(edition.MINTER_ROLE(), address(minter)); - minter.createEditionMint( - address(edition), - PRICE, - edition.owner(), - MAX_MINTED - ); + minter.createEditionMint(address(edition), PRICE, _signerAddress(), MAX_MINTED); } function test_createEditionMintEmitsEvent() public { - SoundEditionV1 edition = SoundEditionV1( - soundCreator.createSound(SONG_NAME, SONG_SYMBOL) - ); - + SoundEditionV1 edition = SoundEditionV1(soundCreator.createSound(SONG_NAME, SONG_SYMBOL)); + FixedPricePermissionedSaleMinter minter = new FixedPricePermissionedSaleMinter(); vm.expectEmit(false, false, false, true); - - emit FixedPricePermissionedMintCreated( - address(edition), - PRICE, - edition.owner(), - MAX_MINTED - ); - minter.createEditionMint( - address(edition), - PRICE, - edition.owner(), - MAX_MINTED - ); + emit FixedPricePermissionedMintCreated(address(edition), PRICE, _signerAddress(), MAX_MINTED); + + minter.createEditionMint(address(edition), PRICE, _signerAddress(), MAX_MINTED); } function test_createEditionMintRevertsIfMintEditionExists() public { (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); - vm.expectRevert(EditionMinter.MintAlreadyExists.selector); - - minter.createEditionMint( - address(edition), - PRICE, - edition.owner(), - MAX_MINTED + // Somehow `vm.expectRevert(EditionMinter.MintAlreadyExists.selector)` fails here. + // Even though running `minter.createEditionMint(...)` will revert with that selector. + (bool status, ) = address(minter).call{ value: PRICE }( + abi.encodeWithSelector( + FixedPricePermissionedSaleMinter.createEditionMint.selector, + address(edition), + PRICE, + _signerAddress(), + MAX_MINTED + ) ); + assertFalse(status); } function test_deleteEditionMintRevertsIfCallerUnauthorized() public { @@ -98,95 +97,69 @@ contract FixedPricePermissionedSaleMinterTests is TestConfig { minter.deleteEditionMint(address(edition)); } - // function test_mintBeforeStartTimeReverts() public { - // (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); - - // vm.warp(START_TIME - 1); - - // address caller = getRandomAccount(1); - // vm.prank(caller); - // vm.expectRevert(FixedPricePermissionedSaleMinter.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, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); - - // vm.warp(END_TIME + 1); - - // address caller = getRandomAccount(1); - // vm.prank(caller); - // vm.expectRevert(FixedPricePermissionedSaleMinter.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, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); - - // vm.warp(START_TIME); - - // address caller = getRandomAccount(1); - // vm.prank(caller); - // vm.expectRevert(FixedPricePermissionedSaleMinter.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(FixedPricePermissionedSaleMinter.MintOutOfStock.selector); - // minter.mint{ value: PRICE }(address(edition), 1); - // } - - // function test_mintWithWrongEtherValueReverts() public { - // (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); - - // vm.warp(START_TIME); - - // address caller = getRandomAccount(1); - // vm.prank(caller); - // vm.expectRevert(FixedPricePermissionedSaleMinter.MintWithWrongEtherValue.selector); - // minter.mint{ value: PRICE * 2 }(address(edition), 1); - // } - - // function test_mintWithUnauthorizedMinterReverts() public { - // (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); - - // vm.warp(START_TIME); - - // address caller = getRandomAccount(1); - - // bool status; - - // vm.prank(caller); - // (status, ) = address(minter).call{ value: PRICE }( - // abi.encodeWithSelector( - // FixedPricePermissionedSaleMinter.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( - // FixedPricePermissionedSaleMinter.mint.selector, - // address(edition), - // 1 - // ) - // ); - // assertFalse(status); - // } + function test_mintWithoutCorrectSignatureReverts() public { + (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); + + address caller = getRandomAccount(1); + bytes memory sig = _getSignature(caller, address(edition)); + + vm.prank(caller); + minter.mint{ value: PRICE }(address(edition), 1, sig); + + vm.expectRevert(FixedPricePermissionedSaleMinter.MintWithInvalidSignature.selector); + minter.mint{ value: PRICE }(address(edition), 1, sig); + } + + function test_mintWithWrongEtherValueReverts() public { + (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); + + address caller = getRandomAccount(1); + bytes memory sig = _getSignature(caller, address(edition)); + + vm.prank(caller); + vm.expectRevert(FixedPricePermissionedSaleMinter.MintWithWrongEtherValue.selector); + minter.mint{ value: PRICE * 2 }(address(edition), 1, sig); + } + + function test_mintDuringOutOfStockReverts() public { + (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); + + address caller = getRandomAccount(1); + bytes memory sig = _getSignature(caller, address(edition)); + + vm.prank(caller); + vm.expectRevert(FixedPricePermissionedSaleMinter.MintOutOfStock.selector); + minter.mint{ value: PRICE * (MAX_MINTED + 1) }(address(edition), MAX_MINTED + 1, sig); + + vm.prank(caller); + minter.mint{ value: PRICE * MAX_MINTED }(address(edition), MAX_MINTED, sig); + + vm.prank(caller); + vm.expectRevert(FixedPricePermissionedSaleMinter.MintOutOfStock.selector); + minter.mint{ value: PRICE }(address(edition), 1, sig); + } + + function test_mintWithUnauthorizedMinterReverts() public { + (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); + + address caller = getRandomAccount(1); + bytes memory sig = _getSignature(caller, address(edition)); + + bool status; + + vm.prank(caller); + (status, ) = address(minter).call{ value: PRICE }( + abi.encodeWithSelector(FixedPricePermissionedSaleMinter.mint.selector, address(edition), 1, sig) + ); + assertTrue(status); + + vm.prank(edition.owner()); + edition.revokeRole(edition.MINTER_ROLE(), address(minter)); + + vm.prank(caller); + (status, ) = address(minter).call{ value: PRICE }( + abi.encodeWithSelector(FixedPricePermissionedSaleMinter.mint.selector, address(edition), 1, sig) + ); + assertFalse(status); + } } diff --git a/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol b/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol index 8eefcfed..15a5315d 100644 --- a/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol +++ b/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol @@ -7,7 +7,7 @@ import "../../../contracts/modules/Minters/FixedPricePublicSaleMinter.sol"; contract FixedPricePublicSaleMinterTests is TestConfig { uint256 constant PRICE = 1; - + uint32 constant START_TIME = 100; uint32 constant END_TIME = 200; @@ -25,71 +25,38 @@ contract FixedPricePublicSaleMinterTests is TestConfig { event MintControllerUpdated(address indexed edition, address indexed controller); - function _createEditionAndMinter() - internal - returns (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) - { - edition = SoundEditionV1( - soundCreator.createSound(SONG_NAME, SONG_SYMBOL) - ); - + 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 - ); + minter.createEditionMint(address(edition), PRICE, START_TIME, END_TIME, MAX_MINTED); } function test_createEditionMintEmitsEvent() public { - SoundEditionV1 edition = SoundEditionV1( - soundCreator.createSound(SONG_NAME, SONG_SYMBOL) - ); - + 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 - ); + 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) - ); - + 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 - ); + minter.createEditionMint(address(edition), PRICE, START_TIME, END_TIME, MAX_MINTED); } function test_createEditionMintRevertsIfMintEditionExists() public { @@ -97,20 +64,14 @@ contract FixedPricePublicSaleMinterTests is TestConfig { vm.expectRevert(EditionMinter.MintAlreadyExists.selector); - minter.createEditionMint( - address(edition), - PRICE, - START_TIME, - END_TIME, - MAX_MINTED - ); + minter.createEditionMint(address(edition), PRICE, START_TIME, END_TIME, MAX_MINTED); } function test_deleteEditionMintEmitsMintControllerUpdatedEvent() public { (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); - + vm.expectEmit(false, false, false, true); - + emit MintControllerUpdated(address(edition), address(0)); minter.deleteEditionMint(address(edition)); @@ -118,15 +79,15 @@ contract FixedPricePublicSaleMinterTests is TestConfig { function test_setEditionMintControllerEmitsMintControllerUpdatedEvent() public { (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); - + vm.expectEmit(false, false, false, true); - + address newController = getRandomAccount(1); emit MintControllerUpdated(address(edition), newController); minter.setEditionMintController(address(edition), newController); - } + } function test_deleteEditionMintRevertsIfCallerUnauthorized() public { (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); @@ -212,20 +173,16 @@ contract FixedPricePublicSaleMinterTests is TestConfig { 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 - ) + abi.encodeWithSelector(FixedPricePublicSaleMinter.mint.selector, address(edition), 1) ); assertTrue(status); @@ -234,11 +191,7 @@ contract FixedPricePublicSaleMinterTests is TestConfig { vm.prank(caller); (status, ) = address(minter).call{ value: PRICE }( - abi.encodeWithSelector( - FixedPricePublicSaleMinter.mint.selector, - address(edition), - 1 - ) + abi.encodeWithSelector(FixedPricePublicSaleMinter.mint.selector, address(edition), 1) ); assertFalse(status); } From 855852ab30368874a013ac4ed742b2c39e07cef1 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 21 Jul 2022 23:27:22 +0000 Subject: [PATCH 06/17] Edit spec.md --- contracts/SoundEdition/SoundEditionV1.sol | 7 +- spec.md | 8 + tests/SoundCreator.t.sol | 8 +- tests/SoundNft/metadata.t.sol | 176 +++--------------- .../FixedPricePermissionedSaleMinter.t.sol | 8 +- .../Minters/FixedPricePublicSaleMinter.t.sol | 12 +- 6 files changed, 53 insertions(+), 166 deletions(-) diff --git a/contracts/SoundEdition/SoundEditionV1.sol b/contracts/SoundEdition/SoundEditionV1.sol index 347b6368..d0ad43dc 100644 --- a/contracts/SoundEdition/SoundEditionV1.sol +++ b/contracts/SoundEdition/SoundEditionV1.sol @@ -36,7 +36,12 @@ import "openzeppelin-upgradeable/access/AccessControlUpgradeable.sol"; /// @title SoundEditionV1 /// @author Sound.xyz -contract SoundEditionV1 is ERC721AQueryableUpgradeable, IERC2981Upgradeable, OwnableUpgradeable, AccessControlUpgradeable { +contract SoundEditionV1 is + ERC721AQueryableUpgradeable, + IERC2981Upgradeable, + OwnableUpgradeable, + AccessControlUpgradeable +{ // ================================ // CONSTANTS // ================================ diff --git a/spec.md b/spec.md index 88acdc95..5a30618a 100644 --- a/spec.md +++ b/spec.md @@ -30,8 +30,16 @@ Sound Protocol 2.0 enables creators to permissinonlessly deploy gas-efficient NF - `tokenURI` uses `baseURI` instead, if `metadataModule` is not present - Implements `contractURI` (https://docs.opensea.io/docs/contract-level-metadata) - Allows freezing of metadata, beyond which the variables can't be modified by `owner` + - Minters + - Allows authorized minting contracts (Minters) to call the `mint(address to, uint256 quantity)` function. + - To authorize a minter, the owner must call the `grantRole(MINTER_ROLE, minter)` function. - `SoundXyzRegistryV1.sol` - Upgradeable via [UUPSUpgradeable](https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable) - Stores registry of NFTs by sound.xyz artists - Requires signature from sound.xyz to register NFTs + +- `modules/Minters/**.sol` + - Currently only allows creation and deletion of edition mints. + - We may want to add a feature to restrict the total number of mints per wallet in the future. + - We may want to allow edition mint controllers to directly edit the fields in the future. diff --git a/tests/SoundCreator.t.sol b/tests/SoundCreator.t.sol index dc5c0dbb..872332be 100644 --- a/tests/SoundCreator.t.sol +++ b/tests/SoundCreator.t.sol @@ -21,13 +21,7 @@ contract SoundCreatorTests is TestConfig { // Tests that the factory creates a new sound NFT function test_createSound() public { SoundEditionV1 soundNft = SoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - METADATA_MODULE, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) ); assert(address(soundNft) != address(0)); diff --git a/tests/SoundNft/metadata.t.sol b/tests/SoundNft/metadata.t.sol index cdbb7084..d5568430 100644 --- a/tests/SoundNft/metadata.t.sol +++ b/tests/SoundNft/metadata.t.sol @@ -7,11 +7,7 @@ import "../TestConfig.sol"; import "../mocks/MockMetadataModule.sol"; contract SoundNft_metadata is TestConfig { - event MetadataFrozen( - IMetadataModule _metadataModule, - string baseURI_, - string _contractURI - ); + event MetadataFrozen(IMetadataModule _metadataModule, string baseURI_, string _contractURI); event BaseURISet(string baseURI_); event ContractURISet(string _contractURI); event MetadataModuleSet(IMetadataModule _metadataModule); @@ -23,23 +19,14 @@ contract SoundNft_metadata is TestConfig { function test_baseURIWhenNoMetadataModule() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - IMetadataModule(address(0)), - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, IMetadataModule(address(0)), BASE_URI, CONTRACT_URI) ); // mint NFTs soundNft.mint(2); uint256 tokenId = 1; - string memory expectedTokenURI = string.concat( - BASE_URI, - Strings.toString(tokenId) - ); + string memory expectedTokenURI = string.concat(BASE_URI, Strings.toString(tokenId)); assertEq(soundNft.tokenURI(tokenId), expectedTokenURI); } @@ -47,13 +34,7 @@ contract SoundNft_metadata is TestConfig { function test_contractURI() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - METADATA_MODULE, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) ); assertEq(soundNft.contractURI(), CONTRACT_URI); @@ -65,13 +46,7 @@ contract SoundNft_metadata is TestConfig { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - metadataModule, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, metadataModule, BASE_URI, CONTRACT_URI) ); // mint NFTs @@ -85,13 +60,7 @@ contract SoundNft_metadata is TestConfig { function test_tokenURIRevertsWhenTokenIdDoesntExist() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - METADATA_MODULE, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) ); vm.expectRevert(URIQueryForNonexistentToken.selector); @@ -101,13 +70,7 @@ contract SoundNft_metadata is TestConfig { function test_setBaseURIRevertsForNonOwner() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - METADATA_MODULE, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) ); string memory newBaseURI = "https://abc.com/"; @@ -121,13 +84,7 @@ contract SoundNft_metadata is TestConfig { function test_setBaseURIRevertsWhenMetadataFrozen() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - METADATA_MODULE, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) ); // Freeze Metadata soundNft.freezeMetadata(); @@ -141,13 +98,7 @@ contract SoundNft_metadata is TestConfig { function test_setBaseURISuccess() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - METADATA_MODULE, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) ); // mint NFTs soundNft.mint(2); @@ -156,23 +107,14 @@ contract SoundNft_metadata is TestConfig { string memory newBaseURI = "https://abc.com/"; soundNft.setBaseURI(newBaseURI); - string memory expectedTokenURI = string.concat( - newBaseURI, - Strings.toString(tokenId) - ); + string memory expectedTokenURI = string.concat(newBaseURI, Strings.toString(tokenId)); assertEq(soundNft.tokenURI(tokenId), expectedTokenURI); } function test_setBaseURIEmitsEvent() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - METADATA_MODULE, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) ); string memory newBaseURI = "https://abc.com/"; @@ -185,13 +127,7 @@ contract SoundNft_metadata is TestConfig { function test_setContractURIRevertsForNonOwner() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - METADATA_MODULE, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) ); string memory newContractURI = "https://abc.com/"; @@ -205,13 +141,7 @@ contract SoundNft_metadata is TestConfig { function test_setContractURIRevertsWhenMetadataFrozen() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - METADATA_MODULE, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) ); // Freeze Metadata soundNft.freezeMetadata(); @@ -225,13 +155,7 @@ contract SoundNft_metadata is TestConfig { function test_setContractURISuccess() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - METADATA_MODULE, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) ); string memory newContractURI = "https://abc.com/"; @@ -243,13 +167,7 @@ contract SoundNft_metadata is TestConfig { function test_setContractURIEmitsEvent() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - METADATA_MODULE, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) ); string memory newContractURI = "https://abc.com/"; @@ -262,13 +180,7 @@ contract SoundNft_metadata is TestConfig { function test_setMetadataModuleRevertsForNonOwner() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - METADATA_MODULE, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) ); MockMetadataModule newMetadataModule = new MockMetadataModule(); @@ -282,13 +194,7 @@ contract SoundNft_metadata is TestConfig { function test_setMetadataModuleRevertsWhenMetadataFrozen() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - METADATA_MODULE, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) ); // Freeze Metadata soundNft.freezeMetadata(); @@ -302,13 +208,7 @@ contract SoundNft_metadata is TestConfig { function test_setMetadataModuleSuccess() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - IMetadataModule(address(0)), - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, IMetadataModule(address(0)), BASE_URI, CONTRACT_URI) ); // mint NFTs soundNft.mint(2); @@ -324,13 +224,7 @@ contract SoundNft_metadata is TestConfig { function test_setMetadataModuleEmitsEvent() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - IMetadataModule(address(0)), - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, IMetadataModule(address(0)), BASE_URI, CONTRACT_URI) ); MockMetadataModule newMetadataModule = new MockMetadataModule(); @@ -343,13 +237,7 @@ contract SoundNft_metadata is TestConfig { function test_freezeMetadataRevertsForNonOwner() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - METADATA_MODULE, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) ); address caller = getRandomAccount(1); @@ -361,13 +249,7 @@ contract SoundNft_metadata is TestConfig { function test_freezeMetadataRevertsIfAlreadyFrozen() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - METADATA_MODULE, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) ); soundNft.freezeMetadata(); @@ -378,13 +260,7 @@ contract SoundNft_metadata is TestConfig { function test_freezeMetadataSuccess() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - METADATA_MODULE, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) ); soundNft.freezeMetadata(); @@ -395,13 +271,7 @@ contract SoundNft_metadata is TestConfig { function test_freezeMetadataEmitsEvent() public { // deploy new sound contract MockSoundEditionV1 soundNft = MockSoundEditionV1( - soundCreator.createSound( - SONG_NAME, - SONG_SYMBOL, - METADATA_MODULE, - BASE_URI, - CONTRACT_URI - ) + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) ); vm.expectEmit(false, false, false, true); diff --git a/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol b/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol index f78b70f1..7c421b76 100644 --- a/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol +++ b/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol @@ -36,7 +36,9 @@ contract FixedPricePermissionedSaleMinterTests is TestConfig { internal returns (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) { - edition = SoundEditionV1(soundCreator.createSound(SONG_NAME, SONG_SYMBOL)); + edition = SoundEditionV1( + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) + ); minter = new FixedPricePermissionedSaleMinter(); @@ -46,7 +48,9 @@ contract FixedPricePermissionedSaleMinterTests is TestConfig { } function test_createEditionMintEmitsEvent() public { - SoundEditionV1 edition = SoundEditionV1(soundCreator.createSound(SONG_NAME, SONG_SYMBOL)); + SoundEditionV1 edition = SoundEditionV1( + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) + ); FixedPricePermissionedSaleMinter minter = new FixedPricePermissionedSaleMinter(); diff --git a/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol b/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol index 15a5315d..3f1709a6 100644 --- a/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol +++ b/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol @@ -26,7 +26,9 @@ contract FixedPricePublicSaleMinterTests is TestConfig { event MintControllerUpdated(address indexed edition, address indexed controller); function _createEditionAndMinter() internal returns (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) { - edition = SoundEditionV1(soundCreator.createSound(SONG_NAME, SONG_SYMBOL)); + edition = SoundEditionV1( + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) + ); minter = new FixedPricePublicSaleMinter(); @@ -36,7 +38,9 @@ contract FixedPricePublicSaleMinterTests is TestConfig { } function test_createEditionMintEmitsEvent() public { - SoundEditionV1 edition = SoundEditionV1(soundCreator.createSound(SONG_NAME, SONG_SYMBOL)); + SoundEditionV1 edition = SoundEditionV1( + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) + ); FixedPricePublicSaleMinter minter = new FixedPricePublicSaleMinter(); @@ -48,7 +52,9 @@ contract FixedPricePublicSaleMinterTests is TestConfig { } function test_createEditionMintEmitsMintControllerUpdatedEvent() public { - SoundEditionV1 edition = SoundEditionV1(soundCreator.createSound(SONG_NAME, SONG_SYMBOL)); + SoundEditionV1 edition = SoundEditionV1( + soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) + ); FixedPricePublicSaleMinter minter = new FixedPricePublicSaleMinter(); From 69bf024d764ac96164244c6bf4f1688d61d0f9dc Mon Sep 17 00:00:00 2001 From: Vectorized Date: Wed, 27 Jul 2022 08:54:07 +0000 Subject: [PATCH 07/17] Add suggested changes --- contracts/modules/Minters/EditionMinter.sol | 56 +++++++++++++------ .../FixedPricePermissionedSaleMinter.sol | 6 +- .../Minters/FixedPricePublicSaleMinter.sol | 5 +- .../FixedPricePermissionedSaleMinter.t.sol | 16 +----- .../Minters/FixedPricePublicSaleMinter.t.sol | 4 +- 5 files changed, 50 insertions(+), 37 deletions(-) diff --git a/contracts/modules/Minters/EditionMinter.sol b/contracts/modules/Minters/EditionMinter.sol index 4f754f7e..a3d77704 100644 --- a/contracts/modules/Minters/EditionMinter.sol +++ b/contracts/modules/Minters/EditionMinter.sol @@ -2,53 +2,75 @@ pragma solidity ^0.8.15; +/// @dev The `EditionMinter` class provides common bookkeeping functions +/// for managing edition mint data in deriving contracts. +/// +/// A controller can create, edit, update, delete the mint data for an edition. +/// It is up to the deriving contract to restrict editing, updating, deleting +/// of mint data. A typical use case is to only allow the controller of the edition. +/// +/// Deriving contracts may use the existence of a controller to prevent overriding +/// of mint data. +/// +/// An edition can only have one controller at any time for a single deriving contract. +/// +/// An edition may have multiple controllers across different deriving contracts. abstract contract EditionMinter { + /// @dev The caller must be the the controller of this edition to perform this action. error MintControllerUnauthorized(); - error MintControllerSetToZeroAddress(); + /// @dev There is no controller assigned to this edition. + error MintControllerNotFound(); - error MintNotFound(); - - error MintAlreadyExists(); + /// @dev A mint controller is already assigned to this edition. + error MintControllerAlreadyExists(); + /// @dev Emitted when the mint `controller` for `edition` is changed. event MintControllerUpdated(address indexed edition, address indexed controller); + /// @dev Maps an edition to a controller. mapping(address => address) private _controllers; + /// @dev Restricts the function to be only callable by the controller of `edition`. modifier onlyEditionMintController(address edition) virtual { address controller = _controllers[edition]; - if (controller == address(0)) revert MintNotFound(); + if (controller == address(0)) revert MintControllerNotFound(); if (msg.sender != controller) revert MintControllerUnauthorized(); _; } - function _createEditionMint(address edition) internal virtual { - if (_controllers[edition] != address(0)) revert MintAlreadyExists(); - + /// @dev Assigns the current caller as the controller to `edition`. + /// + /// Calling conditions: + /// + /// - The `edition` must not have a controller. + function _createEditionMintController(address edition) internal { + if (_controllers[edition] != address(0)) revert MintControllerAlreadyExists(); _controllers[edition] = msg.sender; - emit MintControllerUpdated(edition, msg.sender); } - function _deleteEditionMint(address edition) internal virtual { - address controller = _controllers[edition]; - if (controller == address(0)) revert MintNotFound(); - if (msg.sender != controller) revert MintControllerUnauthorized(); - delete _controllers[edition]; - emit MintControllerUpdated(edition, address(0)); + /// @dev Convenience function for deleting a mint controller. + /// Equivalent to `setEditionMintController(edition, address(0))`. + function _deleteEditionMintController(address edition) internal { + setEditionMintController(edition, address(0)); } + /// @dev Returns the mint controller for `edition`. function editionMintController(address edition) public view returns (address) { return _controllers[edition]; } + /// @dev Sets the new `controller` for `edition`. + /// + /// Calling conditions: + /// + /// - The caller must be the current controller for `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/Minters/FixedPricePermissionedSaleMinter.sol b/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol index 48780f1d..e5bb0014 100644 --- a/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol +++ b/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol @@ -6,9 +6,9 @@ import "./EditionMinter.sol"; import "../../SoundEdition/ISoundEditionV1.sol"; import "solady/utils/ECDSA.sol"; +/// @dev Minter class for sales approved with signatures. contract FixedPricePermissionedSaleMinter is EditionMinter { using ECDSA for bytes32; - error MintWithWrongEtherValue(); error MintOutOfStock(); @@ -42,7 +42,7 @@ contract FixedPricePermissionedSaleMinter is EditionMinter { address signer, uint32 maxMinted ) public { - _createEditionMint(edition); + _createEditionMintController(edition); EditionMintData storage data = editionMintData[edition]; data.price = price; data.signer = signer; @@ -57,7 +57,7 @@ contract FixedPricePermissionedSaleMinter is EditionMinter { } function deleteEditionMint(address edition) public { - _deleteEditionMint(edition); + _deleteEditionMintController(edition); delete editionMintData[edition]; } diff --git a/contracts/modules/Minters/FixedPricePublicSaleMinter.sol b/contracts/modules/Minters/FixedPricePublicSaleMinter.sol index 07b3d9d4..d23eaa2a 100644 --- a/contracts/modules/Minters/FixedPricePublicSaleMinter.sol +++ b/contracts/modules/Minters/FixedPricePublicSaleMinter.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.15; import "./EditionMinter.sol"; import "../../SoundEdition/ISoundEditionV1.sol"; +/// @dev Minter class for sales at a fixed price within a time range. contract FixedPricePublicSaleMinter is EditionMinter { error MintWithWrongEtherValue(); @@ -45,7 +46,7 @@ contract FixedPricePublicSaleMinter is EditionMinter { uint32 endTime, uint32 maxMinted ) public { - _createEditionMint(edition); + _createEditionMintController(edition); EditionMintData storage data = editionMintData[edition]; data.price = price; data.startTime = startTime; @@ -62,7 +63,7 @@ contract FixedPricePublicSaleMinter is EditionMinter { } function deleteEditionMint(address edition) public { - _deleteEditionMint(edition); + _deleteEditionMintController(edition); delete editionMintData[edition]; } diff --git a/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol b/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol index 7c421b76..b4980dd1 100644 --- a/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol +++ b/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol @@ -64,18 +64,8 @@ contract FixedPricePermissionedSaleMinterTests is TestConfig { function test_createEditionMintRevertsIfMintEditionExists() public { (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); - // Somehow `vm.expectRevert(EditionMinter.MintAlreadyExists.selector)` fails here. - // Even though running `minter.createEditionMint(...)` will revert with that selector. - (bool status, ) = address(minter).call{ value: PRICE }( - abi.encodeWithSelector( - FixedPricePermissionedSaleMinter.createEditionMint.selector, - address(edition), - PRICE, - _signerAddress(), - MAX_MINTED - ) - ); - assertFalse(status); + vm.expectRevert(EditionMinter.MintControllerAlreadyExists.selector); + minter.createEditionMint(address(edition), PRICE, _signerAddress(), MAX_MINTED); } function test_deleteEditionMintRevertsIfCallerUnauthorized() public { @@ -96,7 +86,7 @@ contract FixedPricePermissionedSaleMinterTests is TestConfig { minter.deleteEditionMint(address(edition)); - vm.expectRevert(EditionMinter.MintNotFound.selector); + vm.expectRevert(EditionMinter.MintControllerNotFound.selector); minter.deleteEditionMint(address(edition)); } diff --git a/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol b/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol index 3f1709a6..cf62669e 100644 --- a/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol +++ b/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol @@ -68,7 +68,7 @@ contract FixedPricePublicSaleMinterTests is TestConfig { function test_createEditionMintRevertsIfMintEditionExists() public { (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); - vm.expectRevert(EditionMinter.MintAlreadyExists.selector); + vm.expectRevert(EditionMinter.MintControllerAlreadyExists.selector); minter.createEditionMint(address(edition), PRICE, START_TIME, END_TIME, MAX_MINTED); } @@ -113,7 +113,7 @@ contract FixedPricePublicSaleMinterTests is TestConfig { minter.deleteEditionMint(address(edition)); - vm.expectRevert(EditionMinter.MintNotFound.selector); + vm.expectRevert(EditionMinter.MintControllerNotFound.selector); minter.deleteEditionMint(address(edition)); } From 1fa1b97db3281e786c7ef6b3b86b0fd338b21364 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Wed, 27 Jul 2022 09:43:01 +0000 Subject: [PATCH 08/17] Edit mint comments --- contracts/SoundEdition/ISoundEditionV1.sol | 7 +++++++ contracts/SoundEdition/SoundEditionV1.sol | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/contracts/SoundEdition/ISoundEditionV1.sol b/contracts/SoundEdition/ISoundEditionV1.sol index ccea07b7..0d3bb64b 100644 --- a/contracts/SoundEdition/ISoundEditionV1.sol +++ b/contracts/SoundEdition/ISoundEditionV1.sol @@ -44,5 +44,12 @@ interface ISoundEditionV1 is IERC721AUpgradeable { string memory _contractURI ) external; + /// @notice Mints `_quantity` tokens to addrress `_to` + /// Each token will be assigned a token ID that is consecutively increasing. + /// The caller must have the `MINTER_ROLE`, which can be granted via + /// {grantRole}. Multiple minters, such as different minter contracts, + /// can be authorized simultaneously. + /// @param _to Address to mint to + /// @param _quantity Number of tokens to mint function mint(address _to, uint256 _quantity) external payable; } diff --git a/contracts/SoundEdition/SoundEditionV1.sol b/contracts/SoundEdition/SoundEditionV1.sol index d0ad43dc..837d3ea7 100644 --- a/contracts/SoundEdition/SoundEditionV1.sol +++ b/contracts/SoundEdition/SoundEditionV1.sol @@ -178,7 +178,10 @@ contract SoundEditionV1 is } /// @notice Mints `_quantity` tokens to addrress `_to` - /// Each token will be assigned a token ID that is consecutively increasing + /// Each token will be assigned a token ID that is consecutively increasing. + /// The caller must have the `MINTER_ROLE`, which can be granted via + /// {grantRole}. Multiple minters, such as different minter contracts, + /// can be authorized simultaneously. /// @param _to Address to mint to /// @param _quantity Number of tokens to mint function mint(address _to, uint256 _quantity) public payable onlyRole(MINTER_ROLE) { From 7ce8981cc6de546e3cfbc0b2f14380a0b09db0fe Mon Sep 17 00:00:00 2001 From: Vectorized Date: Wed, 27 Jul 2022 14:23:35 +0000 Subject: [PATCH 09/17] Add changeset --- .changeset/lazy-rockets-glow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lazy-rockets-glow.md diff --git a/.changeset/lazy-rockets-glow.md b/.changeset/lazy-rockets-glow.md new file mode 100644 index 00000000..239a3809 --- /dev/null +++ b/.changeset/lazy-rockets-glow.md @@ -0,0 +1,5 @@ +--- +"sound-protocol": minor +--- + +Add Auction Modules and Base Implementation From 04a980e4794eae9ae5d0c50bf6950bde6b4a9f87 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Fri, 29 Jul 2022 23:28:08 +0800 Subject: [PATCH 10/17] Update tests/modules/Minters/FixedPricePublicSaleMinter.t.sol Co-authored-by: Matt Masurka --- tests/modules/Minters/FixedPricePublicSaleMinter.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol b/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol index cf62669e..eef53960 100644 --- a/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol +++ b/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol @@ -133,7 +133,7 @@ contract FixedPricePublicSaleMinterTests is TestConfig { minter.mint{ value: PRICE }(address(edition), 1); } - function test_mintAfterStartTimeReverts() public { + function test_mintAfterEndTimeReverts() public { (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); vm.warp(END_TIME + 1); From 2360de977d3a61dce9199de34bf8aa7c82e6ce2d Mon Sep 17 00:00:00 2001 From: Vectorized Date: Fri, 29 Jul 2022 23:28:37 +0800 Subject: [PATCH 11/17] Update contracts/modules/Minters/FixedPricePublicSaleMinter.sol Co-authored-by: Matt Masurka --- contracts/modules/Minters/FixedPricePublicSaleMinter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/modules/Minters/FixedPricePublicSaleMinter.sol b/contracts/modules/Minters/FixedPricePublicSaleMinter.sol index d23eaa2a..584a0415 100644 --- a/contracts/modules/Minters/FixedPricePublicSaleMinter.sol +++ b/contracts/modules/Minters/FixedPricePublicSaleMinter.sol @@ -9,7 +9,7 @@ import "../../SoundEdition/ISoundEditionV1.sol"; contract FixedPricePublicSaleMinter is EditionMinter { error MintWithWrongEtherValue(); - error MintOutOfStock(); + error OutOfStock(); error MintNotStarted(); From 05694b62b9bb9e9fe07626e061ce24b947743c8c Mon Sep 17 00:00:00 2001 From: Vectorized Date: Fri, 29 Jul 2022 23:29:25 +0800 Subject: [PATCH 12/17] Update contracts/modules/Minters/FixedPricePublicSaleMinter.sol Co-authored-by: Matt Masurka --- contracts/modules/Minters/FixedPricePublicSaleMinter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/modules/Minters/FixedPricePublicSaleMinter.sol b/contracts/modules/Minters/FixedPricePublicSaleMinter.sol index 584a0415..605ba2a3 100644 --- a/contracts/modules/Minters/FixedPricePublicSaleMinter.sol +++ b/contracts/modules/Minters/FixedPricePublicSaleMinter.sol @@ -7,7 +7,7 @@ import "../../SoundEdition/ISoundEditionV1.sol"; /// @dev Minter class for sales at a fixed price within a time range. contract FixedPricePublicSaleMinter is EditionMinter { - error MintWithWrongEtherValue(); + error WrongEtherValue(); error OutOfStock(); From 78cd50893a5ac68bcbfcf195e7dd79a3ebe775d2 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Fri, 29 Jul 2022 23:29:49 +0800 Subject: [PATCH 13/17] Update contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol Co-authored-by: Matt Masurka --- contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol b/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol index e5bb0014..916a4f2b 100644 --- a/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol +++ b/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol @@ -9,7 +9,7 @@ import "solady/utils/ECDSA.sol"; /// @dev Minter class for sales approved with signatures. contract FixedPricePermissionedSaleMinter is EditionMinter { using ECDSA for bytes32; - error MintWithWrongEtherValue(); + error WrongEtherValue(); error MintOutOfStock(); From ef21e2ee22dcb0e68eabe0caa25f62f655615179 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Fri, 29 Jul 2022 23:30:16 +0800 Subject: [PATCH 14/17] Update contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol Co-authored-by: Matt Masurka --- contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol b/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol index 916a4f2b..e5343b2d 100644 --- a/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol +++ b/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol @@ -13,7 +13,7 @@ contract FixedPricePermissionedSaleMinter is EditionMinter { error MintOutOfStock(); - error MintWithInvalidSignature(); + error InvalidSignature(); // prettier-ignore event FixedPricePermissionedMintCreated( From 1285ea7804859c4a1b1f4917a75addfc5c37618a Mon Sep 17 00:00:00 2001 From: Vectorized Date: Fri, 29 Jul 2022 15:45:51 +0000 Subject: [PATCH 15/17] Add suggested changes --- contracts/modules/Minters/EditionMinter.sol | 4 +- .../FixedPricePermissionedSaleMinter.sol | 8 +- .../Minters/FixedPricePublicSaleMinter.sol | 6 +- tests/modules/Minters/EditionMinter.t.sol | 99 +++++++++++++++++++ .../FixedPricePermissionedSaleMinter.t.sol | 40 +------- .../Minters/FixedPricePublicSaleMinter.t.sol | 77 +-------------- 6 files changed, 117 insertions(+), 117 deletions(-) create mode 100644 tests/modules/Minters/EditionMinter.t.sol diff --git a/contracts/modules/Minters/EditionMinter.sol b/contracts/modules/Minters/EditionMinter.sol index a3d77704..9f822af3 100644 --- a/contracts/modules/Minters/EditionMinter.sol +++ b/contracts/modules/Minters/EditionMinter.sol @@ -23,7 +23,7 @@ abstract contract EditionMinter { error MintControllerNotFound(); /// @dev A mint controller is already assigned to this edition. - error MintControllerAlreadyExists(); + error MintControllerAlreadyExists(address controller); /// @dev Emitted when the mint `controller` for `edition` is changed. event MintControllerUpdated(address indexed edition, address indexed controller); @@ -45,7 +45,7 @@ abstract contract EditionMinter { /// /// - The `edition` must not have a controller. function _createEditionMintController(address edition) internal { - if (_controllers[edition] != address(0)) revert MintControllerAlreadyExists(); + if (_controllers[edition] != address(0)) revert MintControllerAlreadyExists(_controllers[edition]); _controllers[edition] = msg.sender; emit MintControllerUpdated(edition, msg.sender); } diff --git a/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol b/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol index e5343b2d..c27d5e41 100644 --- a/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol +++ b/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol @@ -11,7 +11,7 @@ contract FixedPricePermissionedSaleMinter is EditionMinter { using ECDSA for bytes32; error WrongEtherValue(); - error MintOutOfStock(); + error SoldOut(); error InvalidSignature(); @@ -67,12 +67,12 @@ contract FixedPricePermissionedSaleMinter is EditionMinter { 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(); + if ((data.totalMinted += quantity) > data.maxMinted) revert SoldOut(); + if (data.price * quantity != msg.value) revert WrongEtherValue(); bytes32 hash = keccak256(abi.encode(msg.sender, edition)); hash = hash.toEthSignedMessageHash(); - if (hash.recover(signature) != data.signer) revert MintWithInvalidSignature(); + if (hash.recover(signature) != data.signer) revert InvalidSignature(); ISoundEditionV1(edition).mint{ value: msg.value }(edition, quantity); } diff --git a/contracts/modules/Minters/FixedPricePublicSaleMinter.sol b/contracts/modules/Minters/FixedPricePublicSaleMinter.sol index 605ba2a3..bf755a76 100644 --- a/contracts/modules/Minters/FixedPricePublicSaleMinter.sol +++ b/contracts/modules/Minters/FixedPricePublicSaleMinter.sol @@ -9,7 +9,7 @@ import "../../SoundEdition/ISoundEditionV1.sol"; contract FixedPricePublicSaleMinter is EditionMinter { error WrongEtherValue(); - error OutOfStock(); + error SoldOut(); error MintNotStarted(); @@ -69,8 +69,8 @@ contract FixedPricePublicSaleMinter is EditionMinter { function mint(address edition, uint32 quantity) public payable { EditionMintData storage data = editionMintData[edition]; - if ((data.totalMinted += quantity) > data.maxMinted) revert MintOutOfStock(); - if (data.price * quantity != msg.value) revert MintWithWrongEtherValue(); + if ((data.totalMinted += quantity) > data.maxMinted) revert SoldOut(); + if (data.price * quantity != msg.value) revert WrongEtherValue(); 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/tests/modules/Minters/EditionMinter.t.sol b/tests/modules/Minters/EditionMinter.t.sol new file mode 100644 index 00000000..49bf90ed --- /dev/null +++ b/tests/modules/Minters/EditionMinter.t.sol @@ -0,0 +1,99 @@ +pragma solidity ^0.8.15; + +import "../../TestConfig.sol"; +import "../../../contracts/SoundEdition/SoundEditionV1.sol"; +import "../../../contracts/SoundCreator/SoundCreatorV1.sol"; +import "../../../contracts/modules/Minters/FixedPricePublicSaleMinter.sol"; + +contract EditionMinterTests is TestConfig, EditionMinter { + function createEditionMintController(address edition) external { + _createEditionMintController(edition); + } + + function deleteEditionMintController(address edition) external { + _deleteEditionMintController(edition); + } + + function test_createEditionMintControllerEmitsEvent(address edition) external { + address controller = getRandomAccount(1); + vm.expectEmit(false, false, false, true); + emit MintControllerUpdated(address(edition), controller); + vm.prank(controller); + this.createEditionMintController(address(edition)); + } + + function test_createEditionMintControllerChangesController(address edition) external { + address controller = getRandomAccount(1); + assertEq(this.editionMintController(edition), address(0)); + vm.prank(controller); + this.createEditionMintController(address(edition)); + assertEq(this.editionMintController(edition), controller); + } + + function test_createEditionMintControllerRevertsWhenAlreadyExists(address edition) external { + address controller0 = getRandomAccount(0); + address controller1 = getRandomAccount(1); + vm.prank(controller0); + this.createEditionMintController(address(edition)); + vm.expectRevert(abi.encodeWithSelector(EditionMinter.MintControllerAlreadyExists.selector, controller0)); + vm.prank(controller0); + this.createEditionMintController(address(edition)); + vm.prank(controller1); + vm.expectRevert(abi.encodeWithSelector(EditionMinter.MintControllerAlreadyExists.selector, controller0)); + this.createEditionMintController(address(edition)); + } + + function test_setEditionMintControllerEmitsEvent(address edition) external { + address controller0 = getRandomAccount(0); + address controller1 = getRandomAccount(1); + vm.prank(controller0); + this.createEditionMintController(address(edition)); + vm.expectEmit(false, false, false, true); + emit MintControllerUpdated(address(edition), controller1); + vm.prank(controller0); + this.setEditionMintController(address(edition), controller1); + } + + function test_setEditionMintControllerChangesController(address edition) external { + address controller0 = getRandomAccount(0); + address controller1 = getRandomAccount(1); + vm.prank(controller0); + this.createEditionMintController(address(edition)); + vm.prank(controller0); + this.setEditionMintController(address(edition), controller1); + assertEq(this.editionMintController(edition), controller1); + } + + function test_deleteEditionMintControllerEmitsEvent(address edition) external { + address controller = getRandomAccount(0); + vm.prank(controller); + this.createEditionMintController(address(edition)); + vm.expectEmit(false, false, false, true); + emit MintControllerUpdated(address(edition), address(0)); + vm.prank(controller); + this.deleteEditionMintController(address(edition)); + } + + function test_deleteEditionMintRevertsIfCallerUnauthorized(address edition) public { + address controller0 = getRandomAccount(0); + address controller1 = getRandomAccount(1); + vm.prank(controller0); + this.createEditionMintController(address(edition)); + + vm.prank(controller1); + vm.expectRevert(EditionMinter.MintControllerUnauthorized.selector); + this.deleteEditionMintController(address(edition)); + } + + function test_deleteEditionMintRevertsIfMintEditionDoesNotExist(address edition0, address edition1) public { + vm.assume(edition0 != edition1); + + address controller = getRandomAccount(0); + vm.prank(controller); + this.createEditionMintController(address(edition0)); + + vm.prank(controller); + vm.expectRevert(EditionMinter.MintControllerNotFound.selector); + this.deleteEditionMintController(address(edition1)); + } +} diff --git a/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol b/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol index b4980dd1..0337c777 100644 --- a/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol +++ b/tests/modules/Minters/FixedPricePermissionedSaleMinter.t.sol @@ -61,36 +61,6 @@ contract FixedPricePermissionedSaleMinterTests is TestConfig { minter.createEditionMint(address(edition), PRICE, _signerAddress(), MAX_MINTED); } - function test_createEditionMintRevertsIfMintEditionExists() public { - (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); - - vm.expectRevert(EditionMinter.MintControllerAlreadyExists.selector); - minter.createEditionMint(address(edition), PRICE, _signerAddress(), MAX_MINTED); - } - - function test_deleteEditionMintRevertsIfCallerUnauthorized() public { - (SoundEditionV1 edition, FixedPricePermissionedSaleMinter 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, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); - - minter.deleteEditionMint(address(edition)); - - vm.expectRevert(EditionMinter.MintControllerNotFound.selector); - - minter.deleteEditionMint(address(edition)); - } - function test_mintWithoutCorrectSignatureReverts() public { (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); @@ -100,7 +70,7 @@ contract FixedPricePermissionedSaleMinterTests is TestConfig { vm.prank(caller); minter.mint{ value: PRICE }(address(edition), 1, sig); - vm.expectRevert(FixedPricePermissionedSaleMinter.MintWithInvalidSignature.selector); + vm.expectRevert(FixedPricePermissionedSaleMinter.InvalidSignature.selector); minter.mint{ value: PRICE }(address(edition), 1, sig); } @@ -111,25 +81,25 @@ contract FixedPricePermissionedSaleMinterTests is TestConfig { bytes memory sig = _getSignature(caller, address(edition)); vm.prank(caller); - vm.expectRevert(FixedPricePermissionedSaleMinter.MintWithWrongEtherValue.selector); + vm.expectRevert(FixedPricePermissionedSaleMinter.WrongEtherValue.selector); minter.mint{ value: PRICE * 2 }(address(edition), 1, sig); } - function test_mintDuringOutOfStockReverts() public { + function test_mintWhenSoldOutReverts() public { (SoundEditionV1 edition, FixedPricePermissionedSaleMinter minter) = _createEditionAndMinter(); address caller = getRandomAccount(1); bytes memory sig = _getSignature(caller, address(edition)); vm.prank(caller); - vm.expectRevert(FixedPricePermissionedSaleMinter.MintOutOfStock.selector); + vm.expectRevert(FixedPricePermissionedSaleMinter.SoldOut.selector); minter.mint{ value: PRICE * (MAX_MINTED + 1) }(address(edition), MAX_MINTED + 1, sig); vm.prank(caller); minter.mint{ value: PRICE * MAX_MINTED }(address(edition), MAX_MINTED, sig); vm.prank(caller); - vm.expectRevert(FixedPricePermissionedSaleMinter.MintOutOfStock.selector); + vm.expectRevert(FixedPricePermissionedSaleMinter.SoldOut.selector); minter.mint{ value: PRICE }(address(edition), 1, sig); } diff --git a/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol b/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol index eef53960..7200bd62 100644 --- a/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol +++ b/tests/modules/Minters/FixedPricePublicSaleMinter.t.sol @@ -23,8 +23,6 @@ contract FixedPricePublicSaleMinterTests is TestConfig { 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, METADATA_MODULE, BASE_URI, CONTRACT_URI) @@ -51,73 +49,6 @@ contract FixedPricePublicSaleMinterTests is TestConfig { minter.createEditionMint(address(edition), PRICE, START_TIME, END_TIME, MAX_MINTED); } - function test_createEditionMintEmitsMintControllerUpdatedEvent() public { - SoundEditionV1 edition = SoundEditionV1( - soundCreator.createSound(SONG_NAME, SONG_SYMBOL, METADATA_MODULE, BASE_URI, CONTRACT_URI) - ); - - 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.MintControllerAlreadyExists.selector); - - minter.createEditionMint(address(edition), PRICE, START_TIME, END_TIME, MAX_MINTED); - } - - function test_deleteEditionMintEmitsMintControllerUpdatedEvent() public { - (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); - - vm.expectEmit(false, false, false, true); - - emit MintControllerUpdated(address(edition), address(0)); - - minter.deleteEditionMint(address(edition)); - } - - function test_setEditionMintControllerEmitsMintControllerUpdatedEvent() public { - (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); - - vm.expectEmit(false, false, false, true); - - address newController = getRandomAccount(1); - - emit MintControllerUpdated(address(edition), newController); - - minter.setEditionMintController(address(edition), newController); - } - - 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.MintControllerNotFound.selector); - - minter.deleteEditionMint(address(edition)); - } - function test_mintBeforeStartTimeReverts() public { (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); @@ -148,21 +79,21 @@ contract FixedPricePublicSaleMinterTests is TestConfig { minter.mint{ value: PRICE }(address(edition), 1); } - function test_mintDuringOutOfStockReverts() public { + function test_mintWhenSoldOutReverts() public { (SoundEditionV1 edition, FixedPricePublicSaleMinter minter) = _createEditionAndMinter(); vm.warp(START_TIME); address caller = getRandomAccount(1); vm.prank(caller); - vm.expectRevert(FixedPricePublicSaleMinter.MintOutOfStock.selector); + vm.expectRevert(FixedPricePublicSaleMinter.SoldOut.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); + vm.expectRevert(FixedPricePublicSaleMinter.SoldOut.selector); minter.mint{ value: PRICE }(address(edition), 1); } @@ -173,7 +104,7 @@ contract FixedPricePublicSaleMinterTests is TestConfig { address caller = getRandomAccount(1); vm.prank(caller); - vm.expectRevert(FixedPricePublicSaleMinter.MintWithWrongEtherValue.selector); + vm.expectRevert(FixedPricePublicSaleMinter.WrongEtherValue.selector); minter.mint{ value: PRICE * 2 }(address(edition), 1); } From b6feb7caf6e05a14ec349847564b2a7f71232dd1 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Sat, 30 Jul 2022 01:19:07 +0000 Subject: [PATCH 16/17] Add requested changes --- contracts/SoundEdition/ISoundEditionV1.sol | 9 ++++++- contracts/SoundEdition/SoundEditionV1.sol | 25 +++++-------------- .../FixedPricePermissionedSaleMinter.sol | 4 +-- .../Minters/FixedPricePublicSaleMinter.sol | 4 +-- ...itionMinter.sol => MintControllerBase.sol} | 16 ++---------- ...nMinter.t.sol => MintControllerBase.t.sol} | 12 ++++----- 6 files changed, 26 insertions(+), 44 deletions(-) rename contracts/modules/Minters/{EditionMinter.sol => MintControllerBase.sol} (77%) rename tests/modules/Minters/{EditionMinter.t.sol => MintControllerBase.t.sol} (87%) diff --git a/contracts/SoundEdition/ISoundEditionV1.sol b/contracts/SoundEdition/ISoundEditionV1.sol index 0d3bb64b..983d8647 100644 --- a/contracts/SoundEdition/ISoundEditionV1.sol +++ b/contracts/SoundEdition/ISoundEditionV1.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.15; import "chiru-labs/ERC721A-Upgradeable/interfaces/IERC721AUpgradeable.sol"; import "openzeppelin-upgradeable/access/OwnableUpgradeable.sol"; +import "openzeppelin-upgradeable/interfaces/IERC2981Upgradeable.sol"; import "../modules/Metadata/IMetadataModule.sol"; /* @@ -34,7 +35,7 @@ import "../modules/Metadata/IMetadataModule.sol"; /// @title ISoundEditionV1 /// @author Sound.xyz -interface ISoundEditionV1 is IERC721AUpgradeable { +interface ISoundEditionV1 is IERC721AUpgradeable, IERC2981Upgradeable { function initialize( address _owner, string memory _name, @@ -52,4 +53,10 @@ interface ISoundEditionV1 is IERC721AUpgradeable { /// @param _to Address to mint to /// @param _quantity Number of tokens to mint function mint(address _to, uint256 _quantity) external payable; + + function supportsInterface(bytes4 interfaceId) + external + view + override(IERC721AUpgradeable, IERC165Upgradeable) + returns (bool); } diff --git a/contracts/SoundEdition/SoundEditionV1.sol b/contracts/SoundEdition/SoundEditionV1.sol index 837d3ea7..09bad497 100644 --- a/contracts/SoundEdition/SoundEditionV1.sol +++ b/contracts/SoundEdition/SoundEditionV1.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.15; import "chiru-labs/ERC721A-Upgradeable/extensions/ERC721AQueryableUpgradeable.sol"; import "openzeppelin-upgradeable/access/OwnableUpgradeable.sol"; -import "openzeppelin-upgradeable/interfaces/IERC2981Upgradeable.sol"; +import "./ISoundEditionV1.sol"; import "../modules/Metadata/IMetadataModule.sol"; import "openzeppelin-upgradeable/access/AccessControlUpgradeable.sol"; @@ -36,12 +36,7 @@ import "openzeppelin-upgradeable/access/AccessControlUpgradeable.sol"; /// @title SoundEditionV1 /// @author Sound.xyz -contract SoundEditionV1 is - ERC721AQueryableUpgradeable, - IERC2981Upgradeable, - OwnableUpgradeable, - AccessControlUpgradeable -{ +contract SoundEditionV1 is ISoundEditionV1, ERC721AQueryableUpgradeable, OwnableUpgradeable, AccessControlUpgradeable { // ================================ // CONSTANTS // ================================ @@ -151,13 +146,11 @@ contract SoundEditionV1 is return bytes(baseURI_).length != 0 ? string.concat(baseURI_, _toString(tokenId)) : ""; } - /// @notice Informs other contracts which interfaces this contract supports - /// @param _interfaceId The interface id to check - /// @dev https://eips.ethereum.org/EIPS/eip-165 + /// @inheritdoc ISoundEditionV1 function supportsInterface(bytes4 _interfaceId) public view - override(ERC721AUpgradeable, IERC721AUpgradeable, AccessControlUpgradeable, IERC165Upgradeable) + override(ISoundEditionV1, ERC721AUpgradeable, IERC721AUpgradeable, AccessControlUpgradeable) returns (bool) { return @@ -171,19 +164,13 @@ contract SoundEditionV1 is function royaltyInfo(uint256 _tokenId, uint256 _salePrice) external view - override + override(IERC2981Upgradeable) returns (address fundingRecipient, uint256 royaltyAmount) { // todo } - /// @notice Mints `_quantity` tokens to addrress `_to` - /// Each token will be assigned a token ID that is consecutively increasing. - /// The caller must have the `MINTER_ROLE`, which can be granted via - /// {grantRole}. Multiple minters, such as different minter contracts, - /// can be authorized simultaneously. - /// @param _to Address to mint to - /// @param _quantity Number of tokens to mint + /// @inheritdoc ISoundEditionV1 function mint(address _to, uint256 _quantity) public payable onlyRole(MINTER_ROLE) { _mint(_to, _quantity); } diff --git a/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol b/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol index c27d5e41..fafdd3c3 100644 --- a/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol +++ b/contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8.15; -import "./EditionMinter.sol"; +import "./MintControllerBase.sol"; import "../../SoundEdition/ISoundEditionV1.sol"; import "solady/utils/ECDSA.sol"; /// @dev Minter class for sales approved with signatures. -contract FixedPricePermissionedSaleMinter is EditionMinter { +contract FixedPricePermissionedSaleMinter is MintControllerBase { using ECDSA for bytes32; error WrongEtherValue(); diff --git a/contracts/modules/Minters/FixedPricePublicSaleMinter.sol b/contracts/modules/Minters/FixedPricePublicSaleMinter.sol index bf755a76..ae57c050 100644 --- a/contracts/modules/Minters/FixedPricePublicSaleMinter.sol +++ b/contracts/modules/Minters/FixedPricePublicSaleMinter.sol @@ -2,11 +2,11 @@ pragma solidity ^0.8.15; -import "./EditionMinter.sol"; +import "./MintControllerBase.sol"; import "../../SoundEdition/ISoundEditionV1.sol"; /// @dev Minter class for sales at a fixed price within a time range. -contract FixedPricePublicSaleMinter is EditionMinter { +contract FixedPricePublicSaleMinter is MintControllerBase { error WrongEtherValue(); error SoldOut(); diff --git a/contracts/modules/Minters/EditionMinter.sol b/contracts/modules/Minters/MintControllerBase.sol similarity index 77% rename from contracts/modules/Minters/EditionMinter.sol rename to contracts/modules/Minters/MintControllerBase.sol index 9f822af3..df7172c4 100644 --- a/contracts/modules/Minters/EditionMinter.sol +++ b/contracts/modules/Minters/MintControllerBase.sol @@ -2,20 +2,8 @@ pragma solidity ^0.8.15; -/// @dev The `EditionMinter` class provides common bookkeeping functions -/// for managing edition mint data in deriving contracts. -/// -/// A controller can create, edit, update, delete the mint data for an edition. -/// It is up to the deriving contract to restrict editing, updating, deleting -/// of mint data. A typical use case is to only allow the controller of the edition. -/// -/// Deriving contracts may use the existence of a controller to prevent overriding -/// of mint data. -/// -/// An edition can only have one controller at any time for a single deriving contract. -/// -/// An edition may have multiple controllers across different deriving contracts. -abstract contract EditionMinter { +/// @dev The `MintControllerBase` class maintains a central storage record of mint controllers. +abstract contract MintControllerBase { /// @dev The caller must be the the controller of this edition to perform this action. error MintControllerUnauthorized(); diff --git a/tests/modules/Minters/EditionMinter.t.sol b/tests/modules/Minters/MintControllerBase.t.sol similarity index 87% rename from tests/modules/Minters/EditionMinter.t.sol rename to tests/modules/Minters/MintControllerBase.t.sol index 49bf90ed..bc4dbaaa 100644 --- a/tests/modules/Minters/EditionMinter.t.sol +++ b/tests/modules/Minters/MintControllerBase.t.sol @@ -3,9 +3,9 @@ pragma solidity ^0.8.15; import "../../TestConfig.sol"; import "../../../contracts/SoundEdition/SoundEditionV1.sol"; import "../../../contracts/SoundCreator/SoundCreatorV1.sol"; -import "../../../contracts/modules/Minters/FixedPricePublicSaleMinter.sol"; +import "../../../contracts/modules/Minters/MintControllerBase.sol"; -contract EditionMinterTests is TestConfig, EditionMinter { +contract MintControllerBaseTests is TestConfig, MintControllerBase { function createEditionMintController(address edition) external { _createEditionMintController(edition); } @@ -35,11 +35,11 @@ contract EditionMinterTests is TestConfig, EditionMinter { address controller1 = getRandomAccount(1); vm.prank(controller0); this.createEditionMintController(address(edition)); - vm.expectRevert(abi.encodeWithSelector(EditionMinter.MintControllerAlreadyExists.selector, controller0)); + vm.expectRevert(abi.encodeWithSelector(MintControllerBase.MintControllerAlreadyExists.selector, controller0)); vm.prank(controller0); this.createEditionMintController(address(edition)); vm.prank(controller1); - vm.expectRevert(abi.encodeWithSelector(EditionMinter.MintControllerAlreadyExists.selector, controller0)); + vm.expectRevert(abi.encodeWithSelector(MintControllerBase.MintControllerAlreadyExists.selector, controller0)); this.createEditionMintController(address(edition)); } @@ -81,7 +81,7 @@ contract EditionMinterTests is TestConfig, EditionMinter { this.createEditionMintController(address(edition)); vm.prank(controller1); - vm.expectRevert(EditionMinter.MintControllerUnauthorized.selector); + vm.expectRevert(MintControllerBase.MintControllerUnauthorized.selector); this.deleteEditionMintController(address(edition)); } @@ -93,7 +93,7 @@ contract EditionMinterTests is TestConfig, EditionMinter { this.createEditionMintController(address(edition0)); vm.prank(controller); - vm.expectRevert(EditionMinter.MintControllerNotFound.selector); + vm.expectRevert(MintControllerBase.MintControllerNotFound.selector); this.deleteEditionMintController(address(edition1)); } } From bf75d88e9127dc3a0d1ec511b3f56141402103e2 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Sat, 30 Jul 2022 01:24:56 +0000 Subject: [PATCH 17/17] More inheritdocs --- contracts/SoundEdition/ISoundEditionV1.sol | 10 ++++++++++ contracts/SoundEdition/SoundEditionV1.sol | 13 +++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/contracts/SoundEdition/ISoundEditionV1.sol b/contracts/SoundEdition/ISoundEditionV1.sol index 983d8647..72983f94 100644 --- a/contracts/SoundEdition/ISoundEditionV1.sol +++ b/contracts/SoundEdition/ISoundEditionV1.sol @@ -36,6 +36,13 @@ import "../modules/Metadata/IMetadataModule.sol"; /// @title ISoundEditionV1 /// @author Sound.xyz interface ISoundEditionV1 is IERC721AUpgradeable, IERC2981Upgradeable { + /// @notice Initializes the contract + /// @param _owner Owner of contract (artist) + /// @param _name Name of the token + /// @param _symbol Symbol of the token + /// @param _metadataModule Address of metadata module, address(0x00) if not used + /// @param baseURI_ Base URI + /// @param _contractURI Contract URI for OpenSea storefront function initialize( address _owner, string memory _name, @@ -54,6 +61,9 @@ interface ISoundEditionV1 is IERC721AUpgradeable, IERC2981Upgradeable { /// @param _quantity Number of tokens to mint function mint(address _to, uint256 _quantity) external payable; + /// @notice Informs other contracts which interfaces this contract supports. + /// @param interfaceId The interface id to check. + /// @dev https://eips.ethereum.org/EIPS/eip-165 function supportsInterface(bytes4 interfaceId) external view diff --git a/contracts/SoundEdition/SoundEditionV1.sol b/contracts/SoundEdition/SoundEditionV1.sol index 09bad497..7077b2b1 100644 --- a/contracts/SoundEdition/SoundEditionV1.sol +++ b/contracts/SoundEdition/SoundEditionV1.sol @@ -66,13 +66,7 @@ contract SoundEditionV1 is ISoundEditionV1, ERC721AQueryableUpgradeable, Ownable // PUBLIC & EXTERNAL WRITABLE FUNCTIONS // ================================ - /// @notice Initializes the contract - /// @param _owner Owner of contract (artist) - /// @param _name Name of the token - /// @param _symbol Symbol of the token - /// @param _metadataModule Address of metadata module, address(0x00) if not used - /// @param baseURI_ Base URI - /// @param _contractURI Contract URI for OpenSea storefront + /// @inheritdoc ISoundEditionV1 function initialize( address _owner, string memory _name, @@ -130,6 +124,7 @@ contract SoundEditionV1 is ISoundEditionV1, ERC721AQueryableUpgradeable, Ownable // VIEW FUNCTIONS // ================================ + /// @inheritdoc IERC721AUpgradeable function tokenURI(uint256 tokenId) public view @@ -158,9 +153,7 @@ contract SoundEditionV1 is ISoundEditionV1, ERC721AQueryableUpgradeable, Ownable AccessControlUpgradeable.supportsInterface(_interfaceId); } - /// @notice Get royalty information for token - /// @param _tokenId token id - /// @param _salePrice Sale price for the token + /// @inheritdoc IERC2981Upgradeable function royaltyInfo(uint256 _tokenId, uint256 _salePrice) external view