Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auction Modules and Base Implementation #18

Merged
merged 19 commits into from
Aug 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lazy-rockets-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"sound-protocol": minor
---

Add Auction Modules and Base Implementation
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 27 additions & 1 deletion contracts/SoundEdition/ISoundEditionV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/*
Expand Down Expand Up @@ -34,7 +35,14 @@ import "../modules/Metadata/IMetadataModule.sol";

/// @title ISoundEditionV1
/// @author Sound.xyz
interface ISoundEditionV1 is IERC721AUpgradeable {
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,
Expand All @@ -43,4 +51,22 @@ interface ISoundEditionV1 is IERC721AUpgradeable {
string memory baseURI_,
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;

/// @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
override(IERC721AUpgradeable, IERC165Upgradeable)
returns (bool);
}
45 changes: 27 additions & 18 deletions contracts/SoundEdition/SoundEditionV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ 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";

/*
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
Expand Down Expand Up @@ -35,7 +36,12 @@ import "../modules/Metadata/IMetadataModule.sol";

/// @title SoundEditionV1
/// @author Sound.xyz
contract SoundEditionV1 is ERC721AQueryableUpgradeable, IERC2981Upgradeable, OwnableUpgradeable {
contract SoundEditionV1 is ISoundEditionV1, ERC721AQueryableUpgradeable, OwnableUpgradeable, AccessControlUpgradeable {
// ================================
// CONSTANTS
// ================================
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

// ================================
// STORAGE
// ================================
Expand All @@ -60,13 +66,7 @@ contract SoundEditionV1 is ERC721AQueryableUpgradeable, IERC2981Upgradeable, Own
// 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,
Expand All @@ -83,8 +83,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 {
Expand Down Expand Up @@ -119,6 +124,7 @@ contract SoundEditionV1 is ERC721AQueryableUpgradeable, IERC2981Upgradeable, Own
// VIEW FUNCTIONS
// ================================

/// @inheritdoc IERC721AUpgradeable
function tokenURI(uint256 tokenId)
public
view
Expand All @@ -135,27 +141,30 @@ contract SoundEditionV1 is ERC721AQueryableUpgradeable, IERC2981Upgradeable, Own
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, IERC165Upgradeable)
override(ISoundEditionV1, ERC721AUpgradeable, IERC721AUpgradeable, AccessControlUpgradeable)
returns (bool)
{
// todo
return
ERC721AUpgradeable.supportsInterface(_interfaceId) ||
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
override
override(IERC2981Upgradeable)
returns (address fundingRecipient, uint256 royaltyAmount)
{
// todo
}

/// @inheritdoc ISoundEditionV1
function mint(address _to, uint256 _quantity) public payable onlyRole(MINTER_ROLE) {
_mint(_to, _quantity);
}
}
79 changes: 79 additions & 0 deletions contracts/modules/Minters/FixedPricePermissionedSaleMinter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.15;

import "./MintControllerBase.sol";
import "../../SoundEdition/ISoundEditionV1.sol";
import "solady/utils/ECDSA.sol";

/// @dev Minter class for sales approved with signatures.
contract FixedPricePermissionedSaleMinter is MintControllerBase {
using ECDSA for bytes32;
error WrongEtherValue();

error SoldOut();

error InvalidSignature();

// 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 {
_createEditionMintController(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 {
_deleteEditionMintController(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 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 InvalidSignature();

ISoundEditionV1(edition).mint{ value: msg.value }(edition, quantity);
}
}
78 changes: 78 additions & 0 deletions contracts/modules/Minters/FixedPricePublicSaleMinter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.15;

import "./MintControllerBase.sol";
import "../../SoundEdition/ISoundEditionV1.sol";

/// @dev Minter class for sales at a fixed price within a time range.
contract FixedPricePublicSaleMinter is MintControllerBase {
error WrongEtherValue();

error SoldOut();

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 {
_createEditionMintController(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 {
_deleteEditionMintController(edition);
delete editionMintData[edition];
}

function mint(address edition, uint32 quantity) public payable {
EditionMintData storage data = editionMintData[edition];
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);
}
}
65 changes: 65 additions & 0 deletions contracts/modules/Minters/MintControllerBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.15;

/// @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();

/// @dev There is no controller assigned to this edition.
error MintControllerNotFound();

/// @dev A mint controller is already assigned to this edition.
error MintControllerAlreadyExists(address controller);

/// @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 MintControllerNotFound();
if (msg.sender != controller) revert MintControllerUnauthorized();
_;
}

/// @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]);
_controllers[edition] = msg.sender;
emit MintControllerUpdated(edition, msg.sender);
}

/// @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)
{
_controllers[edition] = controller;
emit MintControllerUpdated(edition, controller);
}
}
1 change: 1 addition & 0 deletions lib/solady
Submodule solady added at 581b9f
3 changes: 2 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
openzeppelin/=./lib/openzeppelin-contracts/contracts/
openzeppelin-upgradeable/=./lib/openzeppelin-contracts-upgradeable/contracts/
chiru-labs/ERC721A-Upgradeable/=./lib/ERC721A-Upgradeable/contracts/
chiru-labs/ERC721A-Upgradeable/=./lib/ERC721A-Upgradeable/contracts/
solady/=./lib/solady/src/
Loading