// SPDX-License-Identifier: MIT pragma solidity 0.8.19; import "@openzeppelin/contracts/finance/PaymentSplitter.sol"; import "@openzeppelin/contracts/interfaces/IERC2981.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "../../lib/ERC721A/contracts/extensions/ERC721AQueryable.sol"; interface IEventhub { function emitMintedEvent() external; function emitStartingIndexSetEvent() external; function emitSetActionEvent() external; } interface IERC4906 { event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); } contract MagnetErc721aBase is Ownable, ERC721AQueryable, IERC4906, IERC2981, PaymentSplitter { using Strings for uint256; struct Metadata { string baseURI; string previewURI; string cid; } struct PublicSale { uint256 price; uint256 start; } struct Royalty { uint256 feeBps; address recipient; } uint256 public immutable MAX_SUPPLY; string public PROVENANCE_HASH = ""; uint256 public startingIndexBlock; uint256 public startingIndex; Metadata public metadata; PublicSale public publicSale; Royalty public royalty; bool public paused; uint256 private immutable _teamLength; /// revealStrategy has 3 states: (1: auto reveal) _ (2: restricted manual reveal) _ (3: unrestricted manual reveal) uint256 public revealStrategy; uint256 private constant AUTO_REVEAL_STRATEGY = 1; /// owner will trigger reveal process uint256 private constant RESTRICTED_MANUAL_REVEAL_STRATEGY = 2; /// community will trigger reveal process uint256 private constant UNRESTRICTED_MANUAL_REVEAL_STRATEGY = 3; address private constant EVENTHUB_ADDR = 0xD8Ba6Eca2eBC98eC096616B3f164E74Dd15dCF82; IEventhub internal eventhub = IEventhub(EVENTHUB_ADDR); error NotAllowed(); error SalesPaused(); error SaleHasNotStarted(); error MaxSupplyExceeded(); error InsufficientFunds(); error PublicSaleHasStarted(); error StartingIndexSet(); error StartingIndexBlockNotSet(); error TokenNotFound(); error InvalidParameter(); constructor( string memory _name, string memory _symbol, address[] memory _team, uint256[] memory _teamShares, Royalty memory _royalty, PublicSale memory _publicSale, Metadata memory _metadata, string memory _provenanceHash, uint256 _revealStrategy, uint256 _maxSupply ) ERC721A(_name, _symbol) PaymentSplitter(_team, _teamShares) { royalty = _royalty; publicSale = _publicSale; metadata = _metadata; _teamLength = _team.length; revealStrategy = _revealStrategy; PROVENANCE_HASH = _provenanceHash; MAX_SUPPLY = _maxSupply; } modifier onlyEoa() { if (tx.origin != msg.sender) revert NotAllowed(); _; } modifier live() { if (paused == true) revert SalesPaused(); _; } /// @notice public sale mint function /// @param _quantity number of nfts to mint function mintPublicSale(uint256 _quantity) external payable onlyEoa live { if(publicSale.start > block.timestamp) revert SaleHasNotStarted(); uint256 mintedSupply_ = _mintedSalableSupply(); uint256 maxSalableSupply_ = _maxSalableSupply(); if(mintedSupply_ + _quantity > maxSalableSupply_) revert MaxSupplyExceeded(); if(publicSale.price * _quantity > msg.value) revert InsufficientFunds(); _safeMint(msg.sender, _quantity); _postPublicMint(); } /// @notice update public sale start time before public sale starts - only callable by owner /// @param _saleStartTime new timestamp function setPublicSaleStartTime(uint256 _saleStartTime) public virtual onlyOwner { if(publicSale.start < block.timestamp) revert PublicSaleHasStarted(); publicSale.start = _saleStartTime; eventhub.emitSetActionEvent(); } /// @notice update public sale price before public sale starts - only callable by owner /// @param _salePrice new sale price function setPublicSalePrice(uint256 _salePrice) external onlyOwner { if(publicSale.start < block.timestamp) revert PublicSaleHasStarted(); publicSale.price = _salePrice; eventhub.emitSetActionEvent(); } /// @notice update reveal strategy before starting index is set - only callable by owner /// @param _revealStrategy new state of revealStrategy - possible values: (1: auto reveal) - (2: restricted manual reveal) - (3: unrestricted manual reveal) function setRevealStrategy(uint256 _revealStrategy) external onlyOwner { if(startingIndex != 0) revert StartingIndexSet(); if(_revealStrategy < AUTO_REVEAL_STRATEGY || _revealStrategy > UNRESTRICTED_MANUAL_REVEAL_STRATEGY) revert InvalidParameter(); revealStrategy = _revealStrategy; eventhub.emitSetActionEvent(); } /// @notice pause/resume sales - only callable by owner /// @param _paused new state of sales function setPaused(bool _paused) external onlyOwner { paused = _paused; eventhub.emitSetActionEvent(); } /// @notice update base uri - only callable by owner /// @param _newBaseURI new base uri function setBaseUri(string memory _newBaseURI) external onlyOwner { metadata.baseURI = _newBaseURI; if(startingIndex != 0) { emit BatchMetadataUpdate(_startTokenId(), MAX_SUPPLY); } eventhub.emitSetActionEvent(); } /// @notice set the starting index of the nft collection after startingIndexBlock has been set - only callable by owner if revealStrategy is set to 2 function setStartingIndex() public { if(startingIndex != 0) revert StartingIndexSet(); if(startingIndexBlock == 0) revert StartingIndexBlockNotSet(); if(revealStrategy == RESTRICTED_MANUAL_REVEAL_STRATEGY && msg.sender != owner()) revert NotAllowed(); _setStartingIndex(); } /// @notice set starting index block - to be used in emergency situation - only callable by owner function emergencySetStartingIndexBlock() public onlyOwner { if(startingIndex != 0) revert StartingIndexSet(); // set the startingIndexBlock with the previous block number startingIndexBlock = block.number - 1; // if revealStrategy is set to 1 (auto reveal), set the starting index right away if(revealStrategy == AUTO_REVEAL_STRATEGY) { _setStartingIndex(); } } function _setStartingIndex() private { startingIndex = uint256(blockhash(startingIndexBlock)) % MAX_SUPPLY; // Just a sanity case in the worst case if this function is called late (EVM only stores last 256 block hashes) if (block.number - startingIndexBlock > 255) { startingIndex = uint256(blockhash(block.number - 1)) % MAX_SUPPLY; } // Prevent default sequence if (startingIndex == 0) { startingIndex = startingIndex + 1; } eventhub.emitStartingIndexSetEvent(); emit BatchMetadataUpdate(_startTokenId(), MAX_SUPPLY); } /// @dev hook called afer public mint, if last token is minted, set starting index block, if reveal is automated, trigger reveal process function _postPublicMint() private { uint256 maxSalableSupply_ = _maxSalableSupply(); uint256 mintedSalableSupply_ = _mintedSalableSupply(); if(startingIndexBlock == 0 && (maxSalableSupply_ == mintedSalableSupply_)) { // set the startingIndexBlock with the previous block number startingIndexBlock = block.number - 1; // if revealStrategy is set to 1 (auto reveal), set the starting index right away if(revealStrategy == AUTO_REVEAL_STRATEGY) { _setStartingIndex(); } } _postMint(); } /// @dev hook called after mint, used for emiting event through event hub function _postMint() internal { eventhub.emitMintedEvent(); } /// @dev return max salable supply function _maxSalableSupply() internal view virtual returns(uint256) { return MAX_SUPPLY; } /// @dev return minted salable supply function _mintedSalableSupply() internal view virtual returns(uint256) { return super._totalMinted(); } /// @notice returns a token uri for the given initial id /// @param _initialId tokenId function tokenURI(uint256 _initialId) public view virtual override(IERC721A, ERC721A) returns (string memory) { if(_exists(_initialId) == false) revert TokenNotFound(); if(startingIndex == 0) { return metadata.previewURI; } //compute the final token id using the starting index string memory finalId_ = ( (_initialId + startingIndex) % MAX_SUPPLY ).toString(); return string(abi.encodePacked(metadata.baseURI, metadata.cid, "/final_id_", finalId_, ".json")); } /// @notice returns total minted nfts function totalMinted() external view returns (uint256) { return super._totalMinted(); } function supportsInterface(bytes4 _interfaceId) public view virtual override(IERC721A, ERC721A, IERC165) returns (bool) { return ( _interfaceId == 0x49064906 || // ERC165 interface ID for ERC4906 _interfaceId == type(IERC2981).interfaceId || ERC721A.supportsInterface(_interfaceId) || super.supportsInterface(_interfaceId) ); } function royaltyInfo(uint256, uint256 _salePrice) external view virtual override returns (address receiver, uint256 royaltyAmount) { return (royalty.recipient, (_salePrice * royalty.feeBps) / 10_000); } /// @notice update royalty recipient - only callable by owner /// @param _recipient new recipient function setRoyaltyRecipient(address _recipient) external onlyOwner { if(_recipient == address(0)) revert InvalidParameter(); royalty.recipient = _recipient; eventhub.emitSetActionEvent(); } /// @notice update royalty bps - only callable by owner /// @param _royaltyBps new royalty bps function setRoyaltyBps(uint256 _royaltyBps) external onlyOwner { royalty.feeBps = _royaltyBps; eventhub.emitSetActionEvent(); } /// @notice to be called to ditribute contract revenues to all payees - only callable by owner function releaseAll() public virtual onlyOwner { for(uint256 i = 0 ; i < _teamLength ; i++) { address account_ = payee(i); uint256 payment_ = releasable(account_); if(payment_ > 0) { super.release(payable(account_)); } } } /// @notice release funds for a payee - only callable by payee or owner function release(address payable _account) public virtual override { if(msg.sender != owner() && msg.sender != _account) revert NotAllowed(); uint256 payment_ = releasable(_account); if(payment_ > 0) { super.release(_account); } } /// @dev overrides the `_startTokenId` function from ERC721A to start mint at token id 1 instead of 0 function _startTokenId() internal view virtual override returns (uint256) { return 1; } }