SpinOrSpinToken

0x776853604648864acc45fd9658a4afefae6d7c77

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
6,163 bytes
ABI entries
86
32 read · 15 write
License
mit

Contract information

Address
0x776853604648864acc45fd9658a4afefae6d7c77
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0x332c67c98f…f9b3567a6a

Token

Name
Spin or Spin
Symbol
SOS
Decimals
18
Holders
91

Read contract (32)

LIQUIDITY_RESERVE_SUPPLY()uint256
MAX_BULK_SPINS()uint256
ONE_TOKEN()uint256
PUBLIC_SPIN_SUPPLY_CAP()uint256
REWARD_BASE()uint256
REWARD_BIG_WIN()uint256
REWARD_DOUBLE_SEVEN()uint256
REWARD_JACKPOT()uint256
REWARD_TRIPLE()uint256
SPIN_QUOTE_TYPEHASH()bytes32
allowance(address, address)uint256
balanceOf(address)uint256
cap()uint256
decimals()uint8
eip712Domain()bytes1, string, string, uint256, address, bytes32, uint256[]
liquidityReserveSupply()uint256
mintOpen()bool
mintPrice()uint256
name()string
owner()address
publicSpinMinted()uint256
publicSpinSupplyCap()uint256
randomSpinNonce()uint256
remainingPublicSpinSupply()uint256
remainingSupply()uint256
reserveMinted()bool
reserveTreasury()address
rewardForTier(uint8)uint256
symbol()string
totalSupply()uint256
trustedSigner()address
usedNonces(bytes32)bool

Events (11)

ApprovalEIP712DomainChangedLiquidityReserveMintedMintOpenChangedMintPriceChangedOwnershipTransferredRandomSpinBatchMintedSpinMintedTransferTreasuryWithdrawalTrustedSignerChanged

ABI

[
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "tokenName",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "tokenSymbol",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "maxSupply",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "initialMintPrice",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "initialSigner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "initialOwner",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "BadSignature",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BadTier",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BulkArrayLengthMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DeadlineExpired",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "increasedSupply",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "cap",
        "type": "uint256"
      }
    ],
    "name": "ERC20ExceededCap",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "allowance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      }
    ],
    "name": "ERC20InsufficientAllowance",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      }
    ],
    "name": "ERC20InsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "approver",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidApprover",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "cap",
        "type": "uint256"
      }
    ],
    "name": "ERC20InvalidCap",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidReceiver",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidSender",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidSpender",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidBulkSpinCount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidShortString",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidWithdrawAmount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MintClosed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NonceAlreadyUsed",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PublicSpinSupplyExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReserveAlreadyMinted",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SignedSpinDeprecated",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "str",
        "type": "string"
      }
    ],
    "name": "StringTooLong",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "WithdrawFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "WrongMintPrice",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "Approval",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [],
    "name": "EIP712DomainChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "treasury",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "totalSupplyAfter",
        "type": "uint256"
      }
    ],
    "name": "LiquidityReserveMinted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bool",
        "name": "open",
        "type": "bool"
      }
    ],
    "name": "MintOpenChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "oldPrice",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newPrice",
        "type": "uint256"
      }
    ],
    "name": "MintPriceChanged",
    "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": "minter",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "spinCount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "totalRewardAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "paidAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "totalSupplyAfter",
        "type": "uint256"
      }
    ],
    "name": "RandomSpinBatchMinted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "minter",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint8",
        "name": "tier",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "rewardAmount",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "nonce",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "paidAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "totalSupplyAfter",
        "type": "uint256"
      }
    ],
    "name": "SpinMinted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "Transfer",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "TreasuryWithdrawal",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldSigner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newSigner",
        "type": "address"
      }
    ],
    "name": "TrustedSignerChanged",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "LIQUIDITY_RESERVE_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_BULK_SPINS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ONE_TOKEN",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PUBLIC_SPIN_SUPPLY_CAP",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "REWARD_BASE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "REWARD_BIG_WIN",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "REWARD_DOUBLE_SEVEN",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "REWARD_JACKPOT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "REWARD_TRIPLE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SPIN_QUOTE_TYPEHASH",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      }
    ],
    "name": "allowance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "approve",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8[]",
        "name": "",
        "type": "uint8[]"
      },
      {
        "internalType": "bytes32[]",
        "name": "",
        "type": "bytes32[]"
      },
      {
        "internalType": "uint256[]",
        "name": "",
        "type": "uint256[]"
      },
      {
        "internalType": "bytes[]",
        "name": "",
        "type": "bytes[]"
      }
    ],
    "name": "bulkSpinMint",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "cap",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "decimals",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "eip712Domain",
    "outputs": [
      {
        "internalType": "bytes1",
        "name": "fields",
        "type": "bytes1"
      },
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "version",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "chainId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "verifyingContract",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      },
      {
        "internalType": "uint256[]",
        "name": "extensions",
        "type": "uint256[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "liquidityReserveSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "mintOpen",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "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": [],
    "name": "publicSpinMinted",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "publicSpinSupplyCap",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "randomSpinNonce",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "remainingPublicSpinSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "remainingSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "treasury",
        "type": "address"
      }
    ],
    "name": "reserveMint",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "reserveMinted",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "reserveTreasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "tier",
        "type": "uint8"
      }
    ],
    "name": "rewardForTier",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bool",
        "name": "open",
        "type": "bool"
      }
    ],
    "name": "setMintOpen",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "newPrice",
        "type": "uint256"
      }
    ],
    "name": "setMintPrice",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newSigner",
        "type": "address"
      }
    ],
    "name": "setTrustedSigner",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      },
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "name": "spinMint",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "count",
        "type": "uint8"
      }
    ],
    "name": "spinMintRandom",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "symbol",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "transfer",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "transferFrom",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "trustedSigner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "name": "usedNonces",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "withdraw",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address payable",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "withdrawAmountTo",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address payable",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "withdrawTo",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Capped} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract SpinOrSpinToken is ERC20, ERC20Capped, Ownable, ReentrancyGuard, EIP712 {
    using ECDSA for bytes32;

    uint256 public constant ONE_TOKEN = 1 ether;
    uint256 public constant REWARD_BASE = 1_000 ether;
    uint256 public constant REWARD_DOUBLE_SEVEN = 3_000 ether;
    uint256 public constant REWARD_TRIPLE = 5_000 ether;
    uint256 public constant REWARD_JACKPOT = 10_000 ether;
    uint256 public constant REWARD_BIG_WIN = 20_000 ether;

    uint256 public constant LIQUIDITY_RESERVE_SUPPLY = 20_000_000 ether;
    uint256 public constant PUBLIC_SPIN_SUPPLY_CAP = 80_000_000 ether;
    uint256 public constant MAX_BULK_SPINS = 20;
    uint256 private constant ROLL_DENOMINATOR = 1_000_000;

    bytes32 public constant SPIN_QUOTE_TYPEHASH = keccak256(
        "SpinQuote(address minter,uint8 tier,bytes32 nonce,uint256 deadline)"
    );

    uint256 public mintPrice;
    bool public mintOpen;
    address public trustedSigner;
    bool public reserveMinted;
    address public reserveTreasury;
    uint256 public publicSpinMinted;
    uint256 public randomSpinNonce;

    mapping(bytes32 => bool) public usedNonces;

    event MintOpenChanged(bool indexed open);
    event MintPriceChanged(uint256 oldPrice, uint256 newPrice);
    event TrustedSignerChanged(address indexed oldSigner, address indexed newSigner);
    event LiquidityReserveMinted(address indexed treasury, uint256 amount, uint256 totalSupplyAfter);
    event SpinMinted(
        address indexed minter,
        uint8 indexed tier,
        uint256 rewardAmount,
        bytes32 indexed nonce,
        uint256 paidAmount,
        uint256 totalSupplyAfter
    );
    event RandomSpinBatchMinted(
        address indexed minter,
        uint256 spinCount,
        uint256 totalRewardAmount,
        uint256 paidAmount,
        uint256 totalSupplyAfter
    );
    event TreasuryWithdrawal(address indexed to, uint256 amount);

    error MintClosed();
    error WrongMintPrice();
    error BadTier();
    error DeadlineExpired();
    error NonceAlreadyUsed();
    error BadSignature();
    error ZeroAddress();
    error WithdrawFailed();
    error InvalidWithdrawAmount();
    error ReserveAlreadyMinted();
    error PublicSpinSupplyExceeded();
    error InvalidBulkSpinCount();
    error BulkArrayLengthMismatch();
    error SignedSpinDeprecated();

    constructor(
        string memory tokenName,
        string memory tokenSymbol,
        uint256 maxSupply,
        uint256 initialMintPrice,
        address initialSigner,
        address initialOwner
    )
        ERC20(tokenName, tokenSymbol)
        ERC20Capped(maxSupply)
        Ownable(initialOwner)
        EIP712("Spin or Spin", "1")
    {
        if (initialOwner == address(0) || initialSigner == address(0)) revert ZeroAddress();
        mintPrice = initialMintPrice;
        trustedSigner = initialSigner;
    }

    /// @notice Legacy signed spin is intentionally disabled in v14.
    /// @dev Signed quotes reveal the reward before the wallet tx is approved. Use spinMintRandom instead.
    function spinMint(uint8, bytes32, uint256, bytes calldata)
        external
        payable
        nonReentrant
    {
        revert SignedSpinDeprecated();
    }

    /// @notice Legacy signed bulk spin is intentionally disabled in v14.
    /// @dev Signed quotes reveal the reward before the wallet tx is approved. Use spinMintRandom instead.
    function bulkSpinMint(
        uint8[] calldata,
        bytes32[] calldata,
        uint256[] calldata,
        bytes[] calldata
    )
        external
        payable
        nonReentrant
    {
        revert SignedSpinDeprecated();
    }

    /// @notice One transaction random spin mint. The result is generated inside the transaction,
    /// so the website cannot show the reward before the user signs/sends the tx.
    function spinMintRandom(uint8 count) external payable nonReentrant {
        if (!mintOpen) revert MintClosed();
        if (count == 0 || count > MAX_BULK_SPINS) revert InvalidBulkSpinCount();
        if (msg.value != mintPrice * uint256(count)) revert WrongMintPrice();

        uint256 totalReward;
        for (uint256 i = 0; i < count; i++) {
            uint256 localNonce = randomSpinNonce++;
            bytes32 seed = keccak256(
                abi.encodePacked(
                    block.prevrandao,
                    blockhash(block.number - 1),
                    block.timestamp,
                    msg.sender,
                    address(this),
                    totalSupply(),
                    localNonce,
                    i
                )
            );
            uint8 tier = _tierFromRoll(uint256(seed) % ROLL_DENOMINATOR);
            uint256 reward = rewardForTier(tier);
            if (publicSpinMinted + reward > PUBLIC_SPIN_SUPPLY_CAP) revert PublicSpinSupplyExceeded();

            bytes32 eventNonce = keccak256(abi.encodePacked("SOS_RANDOM_SPIN", msg.sender, localNonce, i, seed));
            publicSpinMinted += reward;
            totalReward += reward;
            _mint(msg.sender, reward);

            emit SpinMinted(msg.sender, tier, reward, eventNonce, mintPrice, totalSupply());
        }

        emit RandomSpinBatchMinted(msg.sender, count, totalReward, msg.value, totalSupply());
    }

    function _consumeSpinQuote(
        address minter,
        uint8 tier,
        bytes32 nonce,
        uint256 deadline,
        bytes calldata signature,
        uint256 paidAmount
    )
        internal
    {
        if (!mintOpen) revert MintClosed();
        if (block.timestamp > deadline) revert DeadlineExpired();
        if (usedNonces[nonce]) revert NonceAlreadyUsed();

        uint256 reward = rewardForTier(tier);
        if (publicSpinMinted + reward > PUBLIC_SPIN_SUPPLY_CAP) revert PublicSpinSupplyExceeded();

        bytes32 structHash = keccak256(abi.encode(SPIN_QUOTE_TYPEHASH, minter, tier, nonce, deadline));
        bytes32 digest = _hashTypedDataV4(structHash);
        address recovered = ECDSA.recover(digest, signature);
        if (recovered != trustedSigner) revert BadSignature();

        usedNonces[nonce] = true;
        publicSpinMinted += reward;
        _mint(minter, reward);

        emit SpinMinted(minter, tier, reward, nonce, paidAmount, totalSupply());
    }

    function reserveMint(address treasury) external onlyOwner nonReentrant {
        if (treasury == address(0)) revert ZeroAddress();
        if (reserveMinted) revert ReserveAlreadyMinted();
        reserveMinted = true;
        reserveTreasury = treasury;
        _mint(treasury, LIQUIDITY_RESERVE_SUPPLY);
        emit LiquidityReserveMinted(treasury, LIQUIDITY_RESERVE_SUPPLY, totalSupply());
    }

    function rewardForTier(uint8 tier) public pure returns (uint256) {
        if (tier == 0) return REWARD_BASE;
        if (tier == 1) return REWARD_DOUBLE_SEVEN;
        if (tier == 2) return REWARD_TRIPLE;
        if (tier == 3) return REWARD_JACKPOT;
        if (tier == 4) return REWARD_BIG_WIN;
        revert BadTier();
    }

    function _tierFromRoll(uint256 roll) internal pure returns (uint8) {
        if (roll < 900_000) return 0;
        if (roll < 960_000) return 1;
        if (roll < 990_000) return 2;
        if (roll < 999_000) return 3;
        return 4;
    }

    function remainingSupply() external view returns (uint256) {
        return cap() - totalSupply();
    }

    function remainingPublicSpinSupply() external view returns (uint256) {
        return PUBLIC_SPIN_SUPPLY_CAP - publicSpinMinted;
    }

    function liquidityReserveSupply() external pure returns (uint256) {
        return LIQUIDITY_RESERVE_SUPPLY;
    }

    function publicSpinSupplyCap() external pure returns (uint256) {
        return PUBLIC_SPIN_SUPPLY_CAP;
    }

    function setMintOpen(bool open) external onlyOwner {
        mintOpen = open;
        emit MintOpenChanged(open);
    }

    function setMintPrice(uint256 newPrice) external onlyOwner {
        uint256 old = mintPrice;
        mintPrice = newPrice;
        emit MintPriceChanged(old, newPrice);
    }

    function setTrustedSigner(address newSigner) external onlyOwner {
        if (newSigner == address(0)) revert ZeroAddress();
        address old = trustedSigner;
        trustedSigner = newSigner;
        emit TrustedSignerChanged(old, newSigner);
    }

    function withdraw() external onlyOwner nonReentrant {
        _withdrawAmountTo(owner(), address(this).balance);
    }

    function withdrawTo(address payable to) external onlyOwner nonReentrant {
        if (to == address(0)) revert ZeroAddress();
        _withdrawAmountTo(to, address(this).balance);
    }

    function withdrawAmountTo(address payable to, uint256 amount) external onlyOwner nonReentrant {
        if (to == address(0)) revert ZeroAddress();
        if (amount == 0 || amount > address(this).balance) revert InvalidWithdrawAmount();
        _withdrawAmountTo(to, amount);
    }

    function _withdrawAmountTo(address to, uint256 amount) internal {
        (bool ok,) = payable(to).call{value: amount}("");
        if (!ok) revert WithdrawFailed();
        emit TreasuryWithdrawal(to, amount);
    }

    function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Capped) {
        super._update(from, to, value);
    }
}
Chain explorer6010msChain node81ms