// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "https://github.com/chiru-labs/ERC721A/blob/main/contracts/extensions/ERC721AQueryable.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Strings.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.2/contracts/token/common/ERC2981.sol";
enum SalePhase {
CLOSED,
FREE,
PAID,
OPEN
}
error HashAlreadyUsed(bytes32 messageHash);
error HashDoesNotMatch(bytes32 messageHash);
error InsufficientPayment(uint256 weiSent, uint256 weiRequired, uint256 quantity);
error MaxSupplyReached();
error RejectZeroAddress();
error SaleNotActive(SalePhase salePhase, SalePhase attemptedPhase);
error SignerDoesNotMatchServer();
error InvalidQuantity(uint256 quantity);
contract DBYClubPass is ERC2981, ERC721AQueryable, Ownable {
uint16 public constant MAX_SUPPLY = 15000;
uint256 public constant RESERVED_SUPPLY = 2200;
string public constant TOKEN_NAME = "DBY Club Pass";
string public constant TOKEN_SYMBOL = "DBYPASS";
string private tokenBaseURI;
address private serverAddress;
address private withdrawalAddress;
mapping(bytes32 => bool) public usedHashes;
SalePhase public salePhase = SalePhase.CLOSED;
uint256 public mintPrice = 0.025 ether;
// Require externally-owned accounts
modifier onlyEOA() {
require(msg.sender == tx.origin, "Not externally owned account");
_;
}
constructor(string memory tokenBaseURI_, address serverAddress_, address withdrawalAddress_)
ERC721A(TOKEN_NAME, TOKEN_SYMBOL)
{
tokenBaseURI = tokenBaseURI_;
serverAddress = serverAddress_;
withdrawalAddress = withdrawalAddress_;
_mintERC2309(withdrawalAddress_, RESERVED_SUPPLY);
_setDefaultRoyalty(withdrawalAddress_, 500);
}
/// @notice Set the token URI
/// @param newTokenBaseURI New URI
function setTokenBaseURI(string memory newTokenBaseURI) external onlyOwner {
tokenBaseURI = newTokenBaseURI;
}
/// @dev View function used in ERC721A's 'tokenURI()' function
function _baseURI() internal view override returns (string memory) {
return tokenBaseURI;
}
/// @notice Set the current sale phase
/// @param phase New sale phase
function setSalePhase(SalePhase phase) external onlyOwner {
salePhase = phase;
}
/// @notice Set the fee numerator for default royalty
/// @param feeNumerator New fee numerator
function setDefaultRoyalty(uint96 feeNumerator) external onlyOwner {
_setDefaultRoyalty(withdrawalAddress, feeNumerator);
}
/// @notice Set new royalties
/// @param price New mint price in wei
function setMintPrice(uint256 price) external onlyOwner {
mintPrice = price;
}
/// @notice Get whether or not address has minted
/// @param owner Address to check
function hasMinted(address owner) external view returns (bool) {
return _numberMinted(owner) > 0;
}
function batchMint(uint256 quantity) external onlyOwner {
_safeMint(msg.sender, quantity);
}
/// @notice Free mint
/// @param v Parity of the y-coordinate of r
/// @param r X-coordinate of r
/// @param s S value of the signature
/// @param msgLen Length of the unhashed message
function freeMint(bytes32 messageHash, uint8 v, bytes32 r, bytes32 s, uint256 msgLen)
external
onlyEOA
{
if (salePhase != SalePhase.FREE && salePhase != SalePhase.OPEN) {
revert SaleNotActive(salePhase, SalePhase.FREE);
}
if (totalSupply() + 1 > MAX_SUPPLY) {
revert MaxSupplyReached();
}
if (!verifySignature(messageHash, v, r, s, msgLen, true)) {
revert HashDoesNotMatch(messageHash);
}
if (usedHashes[messageHash]) {
revert HashAlreadyUsed(messageHash);
}
usedHashes[messageHash] = true;
_mint(msg.sender, 1);
}
/// @notice Paid mint
/// @param v Parity of the y-coordinate of r
/// @param r X-coordinate of r
/// @param s S value of the signature
/// @param msgLen Length of the unhashed message
function mint(
bytes32 messageHash,
uint8 v,
bytes32 r,
bytes32 s,
uint256 msgLen,
uint256 quantity
) external payable onlyEOA {
if (salePhase != SalePhase.PAID && salePhase != SalePhase.OPEN) {
revert SaleNotActive(salePhase, SalePhase.PAID);
}
if (quantity != 1 && quantity != 2) {
revert InvalidQuantity(quantity);
}
if (totalSupply() + quantity > MAX_SUPPLY) {
revert MaxSupplyReached();
}
if (msg.value != mintPrice * quantity) {
revert InsufficientPayment(msg.value, mintPrice, quantity);
}
if (!verifySignature(messageHash, v, r, s, msgLen, false)) {
revert HashDoesNotMatch(messageHash);
}
if (usedHashes[messageHash]) {
revert HashAlreadyUsed(messageHash);
}
usedHashes[messageHash] = true;
_mint(msg.sender, quantity);
}
/// @notice Set the withdrawal address and set royalties to go to new withdrawal address
/// @param _withdrawalAddress New address to send withdrawals
function setWithdrawalAddress(address _withdrawalAddress) external onlyOwner {
if (_withdrawalAddress == address(0)) {
revert RejectZeroAddress();
}
withdrawalAddress = _withdrawalAddress;
_setDefaultRoyalty(_withdrawalAddress, 500);
}
/// @notice Withdraw the ETH from the contract
function withdrawETH() external onlyOwner {
(bool sent,) = payable(withdrawalAddress).call{value: address(this).balance}("");
require(sent, "Withdraw failed");
}
/// @notice Verify the incoming hash from the server
function verifySignature(
bytes32 messageHash,
uint8 v,
bytes32 r,
bytes32 s,
uint256 msgLen,
bool isFree
) private view returns (bool) {
bytes memory prefix = "\x19Ethereum Signed Message:\n";
bytes32 contractHash = keccak256(
abi.encodePacked(
prefix,
Strings.toString(msgLen),
string.concat(
Strings.toHexString(uint256(uint160(msg.sender)), 20), isFree ? "free" : "paid"
)
)
);
address signer = ecrecover(contractHash, v, r, s);
if (signer != serverAddress) {
revert SignerDoesNotMatchServer();
}
return contractHash == messageHash;
}
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(IERC721A, ERC721A, ERC2981)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
/// @notice Set server address
/// @param _serverAddress New server address
function setServerAddress(address _serverAddress) external onlyOwner {
serverAddress = _serverAddress;
}
}
{
"compilationTarget": {
"src/DBYClubPass.sol": "DBYClubPass"
},
"evmVersion": "paris",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": false,
"runs": 200
},
"remappings": []
}
[{"inputs":[{"internalType":"string","name":"tokenBaseURI_","type":"string"},{"internalType":"address","name":"serverAddress_","type":"address"},{"internalType":"address","name":"withdrawalAddress_","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"ApprovalCallerNotOwnerNorApproved","type":"error"},{"inputs":[],"name":"ApprovalQueryForNonexistentToken","type":"error"},{"inputs":[],"name":"BalanceQueryForZeroAddress","type":"error"},{"inputs":[{"internalType":"bytes32","name":"messageHash","type":"bytes32"}],"name":"HashAlreadyUsed","type":"error"},{"inputs":[{"internalType":"bytes32","name":"messageHash","type":"bytes32"}],"name":"HashDoesNotMatch","type":"error"},{"inputs":[{"internalType":"uint256","name":"weiSent","type":"uint256"},{"internalType":"uint256","name":"weiRequired","type":"uint256"},{"internalType":"uint256","name":"quantity","type":"uint256"}],"name":"InsufficientPayment","type":"error"},{"inputs":[{"internalType":"uint256","name":"quantity","type":"uint256"}],"name":"InvalidQuantity","type":"error"},{"inputs":[],"name":"InvalidQueryRange","type":"error"},{"inputs":[],"name":"MaxSupplyReached","type":"error"},{"inputs":[],"name":"MintERC2309QuantityExceedsLimit","type":"error"},{"inputs":[],"name":"MintToZeroAddress","type":"error"},{"inputs":[],"name":"MintZeroQuantity","type":"error"},{"inputs":[],"name":"OwnerQueryForNonexistentToken","type":"error"},{"inputs":[],"name":"OwnershipNotInitializedForExtraData","type":"error"},{"inputs":[],"name":"RejectZeroAddress","type":"error"},{"inputs":[{"internalType":"enum SalePhase","name":"salePhase","type":"uint8"},{"internalType":"enum SalePhase","name":"attemptedPhase","type":"uint8"}],"name":"SaleNotActive","type":"error"},{"inputs":[],"name":"SignerDoesNotMatchServer","type":"error"},{"inputs":[],"name":"TransferCallerNotOwnerNorApproved","type":"error"},{"inputs":[],"name":"TransferFromIncorrectOwner","type":"error"},{"inputs":[],"name":"TransferToNonERC721ReceiverImplementer","type":"error"},{"inputs":[],"name":"TransferToZeroAddress","type":"error"},{"inputs":[],"name":"URIQueryForNonexistentToken","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"fromTokenId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"toTokenId","type":"uint256"},{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"}],"name":"ConsecutiveTransfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[],"name":"MAX_SUPPLY","outputs":[{"internalType":"uint16","name":"","type":"uint16"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RESERVED_SUPPLY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TOKEN_NAME","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TOKEN_SYMBOL","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"quantity","type":"uint256"}],"name":"batchMint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"explicitOwnershipOf","outputs":[{"components":[{"internalType":"address","name":"addr","type":"address"},{"internalType":"uint64","name":"startTimestamp","type":"uint64"},{"internalType":"bool","name":"burned","type":"bool"},{"internalType":"uint24","name":"extraData","type":"uint24"}],"internalType":"struct IERC721A.TokenOwnership","name":"ownership","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"}],"name":"explicitOwnershipsOf","outputs":[{"components":[{"internalType":"address","name":"addr","type":"address"},{"internalType":"uint64","name":"startTimestamp","type":"uint64"},{"internalType":"bool","name":"burned","type":"bool"},{"internalType":"uint24","name":"extraData","type":"uint24"}],"internalType":"struct IERC721A.TokenOwnership[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"messageHash","type":"bytes32"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"},{"internalType":"uint256","name":"msgLen","type":"uint256"}],"name":"freeMint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"hasMinted","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"messageHash","type":"bytes32"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"},{"internalType":"uint256","name":"msgLen","type":"uint256"},{"internalType":"uint256","name":"quantity","type":"uint256"}],"name":"mint","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"mintPrice","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_tokenId","type":"uint256"},{"internalType":"uint256","name":"_salePrice","type":"uint256"}],"name":"royaltyInfo","outputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"salePhase","outputs":[{"internalType":"enum SalePhase","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint96","name":"feeNumerator","type":"uint96"}],"name":"setDefaultRoyalty","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"price","type":"uint256"}],"name":"setMintPrice","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"enum SalePhase","name":"phase","type":"uint8"}],"name":"setSalePhase","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_serverAddress","type":"address"}],"name":"setServerAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"newTokenBaseURI","type":"string"}],"name":"setTokenBaseURI","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_withdrawalAddress","type":"address"}],"name":"setWithdrawalAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"tokensOfOwner","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"start","type":"uint256"},{"internalType":"uint256","name":"stop","type":"uint256"}],"name":"tokensOfOwnerIn","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"usedHashes","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"withdrawETH","outputs":[],"stateMutability":"nonpayable","type":"function"}]