GridMining

0x46d5459f439e64b8cc2d02e89b137608ea5711ce

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
15,115 bytes
ABI entries
107
45 read · 18 write
License
none

Contract information

Address
0x46d5459f439e64b8cc2d02e89b137608ea5711ce
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0xe4e1b34a5b…51029b54fd

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (45)

ADMIN_FEE_BPS()uint256
BPS_DENOMINATOR()uint256
GRID_SIZE()uint256
HARVESTING_FEE_BPS()uint256
MAX_PEAPOT_ACCUMULATION()uint256
MAX_SUPPLY()uint256
MIN_DEPLOY()uint256
MIN_PEAPOT_ACCUMULATION()uint256
ONE_PEA()uint256
PEAPOT_CHANCE()uint256
ROUND_DURATION()uint256
VAULT_FEE_BPS()uint256
accHarvestingPerUnclaimed()uint256
autoMiner()address
currentRoundId()uint64
deriveWords(bytes32)uint256[3]
feeCollector()address
gameStarted()bool
getCurrentRoundInfo()uint64, uint256, uint256, uint256, uint256, bool
getMinerInfo(uint64, address)uint256, uint256, bool
getPendingETH(address)uint256
getPendingPEA(address)uint256, uint256, uint256
getRound(uint64)uint256, uint256, uint256, uint256, uint8, address, uint256, uint256, bool
getRoundDeployed(uint64)uint256[25]
getTotalPendingRewards(address)uint256, uint256, uint256, uint64
maxMinersForSingleWinner()uint256
miners(uint64, address)uint256, uint256, bool
owner()address
pea()address
peapotAccumulation()uint256
peapotPool()uint256
pendingOwner()address
provider()address
quiver()address
renounceOwnership()
requestToRound(bytes32)uint64
roundRequests(uint64)address, uint64, bool
rounds(uint64)uint256, uint256, uint256, uint256, uint256, uint8, address, uint256, uint256, uint256, bool, uint256
totalUnclaimed()uint256
treasury()address
userHarvestedPEA(address)uint256
userHarvestingDebt(address)uint256
userLastRound(address)uint64
userUnclaimedETH(address)uint256
userUnclaimedPEA(address)uint256

Events (15)

AutoMinerUpdatedCheckpointedClaimedETHClaimedPEACoordinatorUpdatedDeployedDeployedForEmergencyVRFRequestedGameStartedOwnershipTransferStartedOwnershipTransferredPeapotAccumulationUpdatedProviderUpdatedResetRequestedRoundSettled

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_coordinator",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_provider",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_pea",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_treasury",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_feeCollector",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AlreadyCheckpointed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AlreadyDeployedThisRound",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "caller",
        "type": "address"
      }
    ],
    "name": "CallerNotCoordinator",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EmergencyTooEarly",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "GameAlreadyStarted",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "GameNotStarted",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientDeployAmount",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "provided",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "required",
        "type": "uint256"
      }
    ],
    "name": "InsufficientFeeAttached",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidBlockId",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidPeapotAccumulation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidVRFRequest",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MaxSupplyReached",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MinimumThresholdTooLow",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoBlocksSelected",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotAutoMiner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NothingToClaim",
    "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": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RenounceDisabled",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RoundAlreadySettled",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RoundNotActive",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RoundNotEnded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RoundNotSettled",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "VRFAlreadyRequested",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldAutoMiner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newAutoMiner",
        "type": "address"
      }
    ],
    "name": "AutoMinerUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint64",
        "name": "roundId",
        "type": "uint64"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethReward",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "peaReward",
        "type": "uint256"
      }
    ],
    "name": "Checkpointed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "ClaimedETH",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "minedPea",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "harvestedPea",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "net",
        "type": "uint256"
      }
    ],
    "name": "ClaimedPEA",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldCoordinator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newCoordinator",
        "type": "address"
      }
    ],
    "name": "CoordinatorUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint64",
        "name": "roundId",
        "type": "uint64"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountPerBlock",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "blockMask",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "totalAmount",
        "type": "uint256"
      }
    ],
    "name": "Deployed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint64",
        "name": "roundId",
        "type": "uint64"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "executor",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountPerBlock",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "blockMask",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "totalAmount",
        "type": "uint256"
      }
    ],
    "name": "DeployedFor",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint64",
        "name": "roundId",
        "type": "uint64"
      },
      {
        "indexed": false,
        "internalType": "uint64",
        "name": "oldSeq",
        "type": "uint64"
      },
      {
        "indexed": false,
        "internalType": "uint64",
        "name": "newSeq",
        "type": "uint64"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "provider",
        "type": "address"
      }
    ],
    "name": "EmergencyVRFRequested",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint64",
        "name": "roundId",
        "type": "uint64"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "startTime",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "endTime",
        "type": "uint256"
      }
    ],
    "name": "GameStarted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "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": false,
        "internalType": "uint256",
        "name": "oldValue",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newValue",
        "type": "uint256"
      }
    ],
    "name": "PeapotAccumulationUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldProvider",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newProvider",
        "type": "address"
      }
    ],
    "name": "ProviderUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint64",
        "name": "roundId",
        "type": "uint64"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "provider",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint64",
        "name": "sequenceNumber",
        "type": "uint64"
      },
      {
        "indexed": false,
        "internalType": "bytes32",
        "name": "userRandom",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint128",
        "name": "feePaid",
        "type": "uint128"
      }
    ],
    "name": "ResetRequested",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint64",
        "name": "roundId",
        "type": "uint64"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "winningBlock",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "topMiner",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "totalWinnings",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "topMinerReward",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "peapotAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "isSplit",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "topMinerSeed",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "winnersDeployed",
        "type": "uint256"
      }
    ],
    "name": "RoundSettled",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "ADMIN_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "BPS_DENOMINATOR",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "GRID_SIZE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "HARVESTING_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_PEAPOT_ACCUMULATION",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_DEPLOY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_PEAPOT_ACCUMULATION",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ONE_PEA",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PEAPOT_CHANCE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ROUND_DURATION",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "VAULT_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "accHarvestingPerUnclaimed",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "autoMiner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint64",
        "name": "roundId",
        "type": "uint64"
      }
    ],
    "name": "checkpoint",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claimETH",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claimPEA",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "currentRoundId",
    "outputs": [
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8[]",
        "name": "blockIds",
        "type": "uint8[]"
      }
    ],
    "name": "deploy",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint8[]",
        "name": "blockIds",
        "type": "uint8[]"
      }
    ],
    "name": "deployFor",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "randomNumber",
        "type": "bytes32"
      }
    ],
    "name": "deriveWords",
    "outputs": [
      {
        "internalType": "uint256[3]",
        "name": "words",
        "type": "uint256[3]"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "emergencyResetVRF",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeCollector",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "gameStarted",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getCurrentRoundInfo",
    "outputs": [
      {
        "internalType": "uint64",
        "name": "roundId",
        "type": "uint64"
      },
      {
        "internalType": "uint256",
        "name": "startTime",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "endTime",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "totalDeployed",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "timeRemaining",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "isActive",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint64",
        "name": "roundId",
        "type": "uint64"
      },
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getMinerInfo",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "deployedMask",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amountPerBlock",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "checkpointed",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getPendingETH",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getPendingPEA",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "gross",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "net",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint64",
        "name": "roundId",
        "type": "uint64"
      }
    ],
    "name": "getRound",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "startTime",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "endTime",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "totalDeployed",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "totalWinnings",
        "type": "uint256"
      },
      {
        "internalType": "uint8",
        "name": "winningBlock",
        "type": "uint8"
      },
      {
        "internalType": "address",
        "name": "topMiner",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "topMinerReward",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "peapotAmount",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "settled",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint64",
        "name": "roundId",
        "type": "uint64"
      }
    ],
    "name": "getRoundDeployed",
    "outputs": [
      {
        "internalType": "uint256[25]",
        "name": "",
        "type": "uint256[25]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getTotalPendingRewards",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "pendingETH",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "pendingUnharvestedPEA",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "pendingHarvestedPEA",
        "type": "uint256"
      },
      {
        "internalType": "uint64",
        "name": "uncheckpointedRound",
        "type": "uint64"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "maxMinersForSingleWinner",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "miners",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "deployedMask",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amountPerBlock",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "checkpointed",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pea",
    "outputs": [
      {
        "internalType": "contract IPea",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "peapotAccumulation",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "peapotPool",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "provider",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "quiver",
    "outputs": [
      {
        "internalType": "contract IQuiverCoordinator",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint64",
        "name": "sequenceNumber",
        "type": "uint64"
      },
      {
        "internalType": "address",
        "name": "provider_",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "randomNumber",
        "type": "bytes32"
      }
    ],
    "name": "quiverCallback",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "name": "requestToRound",
    "outputs": [
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "reset",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      }
    ],
    "name": "roundRequests",
    "outputs": [
      {
        "internalType": "address",
        "name": "provider",
        "type": "address"
      },
      {
        "internalType": "uint64",
        "name": "seq",
        "type": "uint64"
      },
      {
        "internalType": "bool",
        "name": "pending",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      }
    ],
    "name": "rounds",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "startTime",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "endTime",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "totalDeployed",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "totalWinnings",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "winnersDeployed",
        "type": "uint256"
      },
      {
        "internalType": "uint8",
        "name": "winningBlock",
        "type": "uint8"
      },
      {
        "internalType": "address",
        "name": "topMiner",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "topMinerReward",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "peapotAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "topMinerSeed",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "settled",
        "type": "bool"
      },
      {
        "internalType": "uint256",
        "name": "minerCount",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_autoMiner",
        "type": "address"
      }
    ],
    "name": "setAutoMiner",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_coordinator",
        "type": "address"
      }
    ],
    "name": "setCoordinator",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_feeCollector",
        "type": "address"
      }
    ],
    "name": "setFeeCollector",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "_max",
        "type": "uint256"
      }
    ],
    "name": "setMaxMinersForSingleWinner",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "_accumulation",
        "type": "uint256"
      }
    ],
    "name": "setPeapotAccumulation",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_provider",
        "type": "address"
      }
    ],
    "name": "setProvider",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_treasury",
        "type": "address"
      }
    ],
    "name": "setTreasury",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "startFirstRound",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalUnclaimed",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "contract ITreasury",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "userHarvestedPEA",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "userHarvestingDebt",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "userLastRound",
    "outputs": [
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "userUnclaimedETH",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "userUnclaimedPEA",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

import {IQuiverCoordinator} from "./quiver/interfaces/IQuiverCoordinator.sol";
import {IQuiverConsumer} from "./quiver/interfaces/IQuiverConsumer.sol";
import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IPea {
    function mint(address to, uint256 amount) external;
    function burn(uint256 amount) external;
    function totalMinted() external view returns (uint256);
}

interface ITreasury {
    function receiveVault() external payable;
}


// Two-step transfer, no renounce. Ownership here is the randomness liveness
contract GridMining is IQuiverConsumer, Ownable2Step, ReentrancyGuard {
    // ============ Constants ============

    uint256 public constant ROUND_DURATION = 60 seconds;
    uint256 public constant MIN_DEPLOY = 0.0000025 ether;
    uint256 public constant GRID_SIZE = 25;

    uint256 public constant ADMIN_FEE_BPS = 100;      // 1%
    uint256 public constant VAULT_FEE_BPS = 1000;     // 10%
    uint256 public constant HARVESTING_FEE_BPS = 1000;  // 10%
    uint256 public constant BPS_DENOMINATOR = 10000;

    uint256 public constant ONE_PEA = 1e18;
    uint256 public constant MIN_PEAPOT_ACCUMULATION = ONE_PEA / 10;
    uint256 public constant MAX_PEAPOT_ACCUMULATION = ONE_PEA;
    uint256 public peapotAccumulation = ONE_PEA / 10;
    uint256 public constant PEAPOT_CHANCE = 333;
    uint256 public constant MAX_SUPPLY = 3_000_000 * ONE_PEA;

    // ============ Randomness Config (Pyth-inherited commit-reveal) ============

    /// @dev Both owner-settable. Ops rule:
    ///      rotating the PROVIDER mid-pending-request is safe (old request stays
    ///      mapped); rotating the COORDINATOR mid-pending requires timing — the callback gate
    ///      checks the current coordinator, so settle or emergency-reset first.
    IQuiverCoordinator public quiver;
    address public provider;

    // ============ External Contracts ============

    IPea public pea;
    ITreasury public treasury;
    address public feeCollector;
    address public autoMiner;

    // ============ Board State ============

    uint64 public currentRoundId;
    bool public gameStarted;
    uint256 public maxMinersForSingleWinner = 2000;

    // ============ Round State ============

    struct Round {
        uint256 startTime;
        uint256 endTime;
        uint256[25] deployed;           // ETH per block
        uint256 totalDeployed;
        uint256 totalWinnings;          // After fees, for winners to claim
        uint256 winnersDeployed;        // Amount on winning block
        uint8 winningBlock;
        address topMiner;               // Single winner address, or address(0) if split
        uint256 topMinerReward;         // PEA reward amount
        uint256 peapotAmount;          // Peapot payout if triggered
        uint256 topMinerSeed;
        bool settled;
        uint256 minerCount;             // Number of unique deployers this round
    }

    /// @dev Quiver request state, kept out of the Round struct: the auto-generated
    ///      rounds() getter is prefix-decoded by AutoMiner and already near the
    ///      stack-depth limit. A separate single-slot record avoids both hazards.
    struct RequestState {
        address provider;               // provider serving the active request
        uint64 seq;                     // Quiver sequence number (per-provider, starts at 1)
        bool pending;
    }

    struct FeeCalc {
        uint256 adminFee;
        uint256 vaultAmount;
        uint256 totalWinnings;
    }

    mapping(uint64 => Round) public rounds;
    /// @dev roundId => active randomness request for that round.
    mapping(uint64 => RequestState) public roundRequests;
    /// @dev keccak256(provider, seq) => roundId. Quiver sequence numbers are PER-PROVIDER
    mapping(bytes32 => uint64) public requestToRound;

    // ============ Miner State ============

    struct Miner {
        uint256 deployedMask;           // Bitmask of blocks deployed to
        uint256 amountPerBlock;         // ETH per block
        bool checkpointed;
    }

    // roundId => user => Miner
    mapping(uint64 => mapping(address => Miner)) public miners;

    // roundId => deployIndex => deployer address
    mapping(uint64 => mapping(uint256 => address)) internal minerByIndex;

    // ============ Global Rewards State ============

    uint256 public peapotPool;

    // Harvesting fee redistribution
    uint256 public totalUnclaimed;                              
    uint256 public accHarvestingPerUnclaimed;                     
    mapping(address => uint256) public userHarvestingDebt;
    mapping(address => uint256) public userUnclaimedPEA;
    mapping(address => uint256) public userUnclaimedETH;
    mapping(address => uint256) public userHarvestedPEA;
    mapping(address => uint64) public userLastRound;

    // ============ Events ============

    event GameStarted(uint64 indexed roundId, uint256 startTime, uint256 endTime);
    event Deployed(uint64 indexed roundId, address indexed user, uint256 amountPerBlock, uint256 blockMask, uint256 totalAmount);
    event ResetRequested(
        uint64 indexed roundId,
        address indexed provider,
        uint64 sequenceNumber,
        bytes32 userRandom,
        uint128 feePaid
    );
    event RoundSettled(
        uint64 indexed roundId,
        uint8 winningBlock,
        address topMiner,
        uint256 totalWinnings,
        uint256 topMinerReward,
        uint256 peapotAmount,
        bool isSplit,
        uint256 topMinerSeed,
        uint256 winnersDeployed
    );
    event DeployedFor(
        uint64 indexed roundId,
        address indexed user,
        address indexed executor,
        uint256 amountPerBlock,
        uint256 blockMask,
        uint256 totalAmount
    );
    event AutoMinerUpdated(address indexed oldAutoMiner, address indexed newAutoMiner);
    event Checkpointed(uint64 indexed roundId, address indexed user, uint256 ethReward, uint256 peaReward);
    event ClaimedETH(address indexed user, uint256 amount);
    event ClaimedPEA(address indexed user, uint256 minedPea, uint256 harvestedPea, uint256 fee, uint256 net);
    event EmergencyVRFRequested(uint64 indexed roundId, uint64 oldSeq, uint64 newSeq, address indexed provider);
    event PeapotAccumulationUpdated(uint256 oldValue, uint256 newValue);
    event CoordinatorUpdated(address indexed oldCoordinator, address indexed newCoordinator);
    event ProviderUpdated(address indexed oldProvider, address indexed newProvider);

    // ============ Errors ============

    error GameNotStarted();
    error GameAlreadyStarted();
    error RoundNotActive();
    error RoundNotEnded();
    error RoundAlreadySettled();
    error RoundNotSettled();
    error AlreadyCheckpointed();
    error InvalidBlockId();
    error InsufficientDeployAmount();
    error NoBlocksSelected();
    error AlreadyDeployedThisRound();
    error NothingToClaim();
    error TransferFailed();
    error InvalidVRFRequest();
    error MaxSupplyReached();
    error VRFAlreadyRequested();
    error NotAutoMiner();
    error MinimumThresholdTooLow();
    error EmergencyTooEarly();
    error InvalidPeapotAccumulation();
    error ZeroAddress();
    error CallerNotCoordinator(address caller);
    error InsufficientFeeAttached(uint256 provided, uint256 required);
    error RenounceDisabled();

    // ============ Constructor ============

    constructor(
        address _coordinator,
        address _provider,
        address _pea,
        address _treasury,
        address _feeCollector
    ) Ownable(msg.sender) {
        if (_coordinator == address(0)) revert ZeroAddress();
        if (_provider == address(0)) revert ZeroAddress();
        if (_treasury == address(0)) revert ZeroAddress();
        if (_feeCollector == address(0)) revert ZeroAddress();
        if (_pea == address(0)) revert ZeroAddress();

        quiver = IQuiverCoordinator(_coordinator);
        provider = _provider;
        pea = IPea(_pea);
        treasury = ITreasury(_treasury);
        feeCollector = _feeCollector;
    }

    // ============ Admin Functions ============

    /// @notice Replace the Quiver coordinator.
    /// @dev Do not rotate while a request is pending. The callback gate checks the
    ///      current coordinator, so the old one's delivery would revert.
    function setCoordinator(address _coordinator) external onlyOwner {
        if (_coordinator == address(0)) revert ZeroAddress();
        address old = address(quiver);
        quiver = IQuiverCoordinator(_coordinator);
        emit CoordinatorUpdated(old, _coordinator);
    }

    /// @notice Replace the default randomness provider.
    /// @dev Safe mid-pending: the in-flight request stays keyed to its own provider.
    function setProvider(address _provider) external onlyOwner {
        if (_provider == address(0)) revert ZeroAddress();
        address old = provider;
        provider = _provider;
        emit ProviderUpdated(old, _provider);
    }

    /// @notice Disabled: ownership is the randomness liveness failover
    function renounceOwnership() public view override onlyOwner {
        revert RenounceDisabled();
    }

    function setFeeCollector(address _feeCollector) external onlyOwner {
        if (_feeCollector == address(0)) revert ZeroAddress();
        feeCollector = _feeCollector;
    }

    function setTreasury(address _treasury) external onlyOwner {
        if (_treasury == address(0)) revert ZeroAddress();
        treasury = ITreasury(_treasury);
    }

    function setAutoMiner(address _autoMiner) external onlyOwner {
        address old = autoMiner;
        autoMiner = _autoMiner;
        emit AutoMinerUpdated(old, _autoMiner);
    }

    function setPeapotAccumulation(uint256 _accumulation) external onlyOwner {
        if (_accumulation < MIN_PEAPOT_ACCUMULATION || _accumulation > MAX_PEAPOT_ACCUMULATION) {
            revert InvalidPeapotAccumulation();
        }
        uint256 oldValue = peapotAccumulation;
        peapotAccumulation = _accumulation;
        emit PeapotAccumulationUpdated(oldValue, _accumulation);
    }

    function setMaxMinersForSingleWinner(uint256 _max) external onlyOwner {
        if (_max < 500) revert MinimumThresholdTooLow();
        maxMinersForSingleWinner = _max;
    }

    function startFirstRound() external onlyOwner {
        // Coordinator + provider are constructor-guaranteed non-zero
        if (gameStarted) revert GameAlreadyStarted();

        gameStarted = true;
        currentRoundId = 1;

        Round storage round = rounds[1];
        round.startTime = block.timestamp;
        round.endTime = block.timestamp + ROUND_DURATION;

        emit GameStarted(1, round.startTime, round.endTime);
    }

    // ============ Core Game Functions ============

    /**
     * @notice Deploy ETH to selected blocks in the current round
     * @param blockIds Array of block IDs (0-24) to deploy to
     * @dev Deploys msg.value / blockIds.length to each selected block
     */
    function deploy(uint8[] calldata blockIds) external payable nonReentrant {
        if (!gameStarted) revert GameNotStarted();

        // Auto-checkpoint previous round
        _autoCheckpointPrevious(msg.sender);

        Round storage round = rounds[currentRoundId];

        // Check round is active
        if (block.timestamp >= round.endTime) revert RoundNotActive();

        uint256 numBlocks = blockIds.length;
        if (numBlocks == 0) revert NoBlocksSelected();

        uint256 amountPerBlock = msg.value / numBlocks;
        if (amountPerBlock < MIN_DEPLOY) revert InsufficientDeployAmount();

        Miner storage miner = miners[currentRoundId][msg.sender];

        // One deploy per round
        if (miner.deployedMask != 0) revert AlreadyDeployedThisRound();

        // Assign deploy index for lazy top miner resolution
        minerByIndex[currentRoundId][round.minerCount] = msg.sender;
        round.minerCount++;

        uint256 blockMask;
        uint256 totalAmount;

        for (uint256 i; i < numBlocks; ) {
            uint8 blockId = blockIds[i];
            if (blockId >= GRID_SIZE) revert InvalidBlockId();

            uint256 bit = 1 << blockId;
            if ((blockMask & bit) != 0) revert InvalidBlockId();
            blockMask |= bit;

            uint256 currentDeployed = round.deployed[blockId];
            round.deployed[blockId] = currentDeployed + amountPerBlock;

            unchecked {
                totalAmount += amountPerBlock;
                ++i;
            }
        }

        // Write miner state
        miner.deployedMask = blockMask;
        miner.amountPerBlock = amountPerBlock;

        round.totalDeployed += totalAmount;

        // Track user's last played round
        userLastRound[msg.sender] = currentRoundId;

        emit Deployed(currentRoundId, msg.sender, amountPerBlock, blockMask, totalAmount);
    }

    /**
    * @notice Deploy ETH to selected blocks on behalf of a user (AutoMiner only)
    * @param user The address to credit deposits to
    * @param blockIds Array of block IDs (0-24) to deploy to
    * @dev Deploys msg.value / blockIds.length to each selected block
    */
    function deployFor(address user, uint8[] calldata blockIds) external payable nonReentrant {
        if (msg.sender != autoMiner) revert NotAutoMiner();
        if (user == address(0)) revert ZeroAddress();
        if (!gameStarted) revert GameNotStarted();

        // Auto-checkpoint previous round for the USER
        _autoCheckpointPrevious(user);

        Round storage round = rounds[currentRoundId];

        // Check round is active
        if (block.timestamp >= round.endTime) revert RoundNotActive();

        uint256 numBlocks = blockIds.length;
        if (numBlocks == 0) revert NoBlocksSelected();

        uint256 amountPerBlock = msg.value / numBlocks;
        if (amountPerBlock < MIN_DEPLOY) revert InsufficientDeployAmount();

        // Use USER's miner state
        Miner storage miner = miners[currentRoundId][user];

        // One deploy per round
        if (miner.deployedMask != 0) revert AlreadyDeployedThisRound();

        // Assign deploy index for lazy top miner resolution
        minerByIndex[currentRoundId][round.minerCount] = user;
        round.minerCount++;

        uint256 blockMask;
        uint256 totalAmount;

        for (uint256 i; i < numBlocks; ) {
            uint8 blockId = blockIds[i];
            if (blockId >= GRID_SIZE) revert InvalidBlockId();

            uint256 bit = 1 << blockId;
            if ((blockMask & bit) != 0) revert InvalidBlockId();
            blockMask |= bit;

            uint256 currentDeployed = round.deployed[blockId];
            round.deployed[blockId] = currentDeployed + amountPerBlock;

            unchecked {
                totalAmount += amountPerBlock;
                ++i;
            }
        }

        // Write miner state
        miner.deployedMask = blockMask;
        miner.amountPerBlock = amountPerBlock;

        round.totalDeployed += totalAmount;

        // Track USER's last played round
        userLastRound[user] = currentRoundId;

        emit DeployedFor(currentRoundId, user, msg.sender, amountPerBlock, blockMask, totalAmount);
    }

    /**
     * @notice End the current round and request settlement randomness from Quiver
     * @dev Anyone can call this after round ends. Payable: attach getFee(provider)
     *      as msg.value. Exactly the fee is forwarded and any excess is refunded to the caller.
     */
    function reset() external payable nonReentrant {
        if (!gameStarted) revert GameNotStarted();

        Round storage round = rounds[currentRoundId];

        if (block.timestamp < round.endTime) revert RoundNotEnded();
        if (round.settled) revert RoundAlreadySettled();
        if (roundRequests[currentRoundId].pending) revert VRFAlreadyRequested();

        // Handle empty round (no one deployed) — synchronous, no randomness needed.
        // Refund any attached fee so it can't strand in the contract.
        if (round.totalDeployed == 0) {
            round.settled = true;
            _startNextRound();
            if (msg.value > 0) _safeTransferETH(msg.sender, msg.value);
            return;
        }

        _requestSettlement(currentRoundId);
    }

    /// @dev Request settlement randomness from the current provider. The fee rides in
    ///      as msg.value: forward EXACTLY the fee (the coordinator refunds overpayment
    ///      to this contract, not the caller) and refund the excess here.
    function _requestSettlement(uint64 roundId) internal {
        bytes32 userRandom = keccak256(
            abi.encode(
                address(this), roundId, roundRequests[roundId].seq, block.timestamp, blockhash(block.number - 1), msg.sender, gasleft()
            )
        );

        address provider_ = provider;
        uint128 fee = quiver.getFee(provider_);
        if (msg.value < fee) revert InsufficientFeeAttached(msg.value, fee);

        uint64 seq = quiver.requestWithCallback{value: fee}(provider_, userRandom);

        roundRequests[roundId] = RequestState({provider: provider_, seq: seq, pending: true});
        requestToRound[_requestKey(provider_, seq)] = roundId;

        emit ResetRequested(roundId, provider_, seq, userRandom, fee);

        uint256 excess = msg.value - fee;
        if (excess > 0) _safeTransferETH(msg.sender, excess);
    }

    /// @notice Quiver push-flow delivery point.
    /// @dev Only the coordinator may call. Never reverts on stale/unknown sequences.
    ///      The emergency re-request path depends on silent drops.
    function quiverCallback(uint64 sequenceNumber, address provider_, bytes32 randomNumber) external override {
        if (msg.sender != address(quiver)) revert CallerNotCoordinator(msg.sender);
        _fulfillRandomness(sequenceNumber, provider_, randomNumber);
    }

    function _fulfillRandomness(uint64 sequenceNumber, address provider_, bytes32 randomNumber) internal {
        uint64 roundId = requestToRound[_requestKey(provider_, sequenceNumber)];
        if (roundId == 0) return; // stale/unknown request = silent drop

        Round storage round = rounds[roundId];
        if (round.settled) return; // replay guard

        // Expand single bytes32 into the 3 words to deliver
        uint256[3] memory randomWords = deriveWords(randomNumber);

        uint8 winningBlock = uint8(randomWords[0] % GRID_SIZE);
        round.winningBlock = winningBlock;
        round.winnersDeployed = round.deployed[winningBlock];

        if (round.winnersDeployed == 0) {
            roundRequests[roundId].pending = false;
            _settleNoWinners(roundId, round, winningBlock);
            return;
        }

        FeeCalc memory fees = _calculateSettlementFees(round.totalDeployed, round.winnersDeployed);
        round.totalWinnings = fees.totalWinnings;

        bool isSplit = _processMinting(round, randomWords);

        round.settled = true;
        roundRequests[roundId].pending = false;

        _safeTransferETH(feeCollector, fees.adminFee);
        treasury.receiveVault{value: fees.vaultAmount}();

        _startNextRound();

        emit RoundSettled(roundId, winningBlock, round.topMiner, round.totalWinnings, round.topMinerReward, round.peapotAmount, isSplit, round.topMinerSeed, round.winnersDeployed);
    }

    function deriveWords(bytes32 randomNumber) public pure returns (uint256[3] memory words) {
        for (uint256 i = 0; i < 3; i++) {
            words[i] = uint256(keccak256(abi.encode(randomNumber, i)));
        }
    }

    /// @dev Deterministic key for a (provider, sequenceNumber) request.
    function _requestKey(address provider_, uint64 sequenceNumber) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked(provider_, sequenceNumber));
    }

    function _settleNoWinners(uint64 roundId, Round storage round, uint8 winningBlock) internal {
        uint256 adminFee = (round.totalDeployed * ADMIN_FEE_BPS) / BPS_DENOMINATOR;
        uint256 vaultAmount = round.totalDeployed - adminFee;

        round.settled = true;

        _safeTransferETH(feeCollector, adminFee);
        treasury.receiveVault{value: vaultAmount}();

        _startNextRound();

        emit RoundSettled(roundId, winningBlock, address(0), 0, 0, 0, false, 0, 0);
    }

    function _calculateSettlementFees(uint256 totalDeployed, uint256 winnersDeployed) internal pure returns (FeeCalc memory) {
        uint256 losersPool = totalDeployed - winnersDeployed;
        uint256 adminFee = (totalDeployed * ADMIN_FEE_BPS) / BPS_DENOMINATOR;
        uint256 losersAdminShare = (losersPool * ADMIN_FEE_BPS) / BPS_DENOMINATOR;
        uint256 losersAfterAdmin = losersPool - losersAdminShare;
        uint256 vaultAmount = (losersAfterAdmin * VAULT_FEE_BPS) / BPS_DENOMINATOR;
        uint256 totalWinnings = losersAfterAdmin - vaultAmount;

        return FeeCalc(adminFee, vaultAmount, totalWinnings);
    }

    function _processMinting(Round storage round, uint256[3] memory randomWords) internal returns (bool isSplit) {
        isSplit = (randomWords[1] % 2 == 0);

        // Cap combined mint to remaining supply. Avoid revert near MAX_SUPPLY
        uint256 remaining = MAX_SUPPLY - pea.totalMinted();
        uint256 mintAmount = ONE_PEA > remaining ? remaining : ONE_PEA;
        remaining -= mintAmount;
        uint256 peapotMint = peapotAccumulation > remaining ? remaining : peapotAccumulation;

        round.topMinerSeed = randomWords[1];
        round.topMinerReward = mintAmount;
        round.topMiner = isSplit ? address(0) : address(1);

        if (randomWords[2] % PEAPOT_CHANCE == 0 && peapotPool > 0) {
            round.peapotAmount = peapotPool;
            peapotPool = 0;
        }

        peapotPool += peapotMint;

        uint256 totalMint = mintAmount + peapotMint;
        if (totalMint > 0) {
            pea.mint(address(this), totalMint);
        }
    }

    function _checkpoint(uint64 roundId, address user) internal {
        Round storage round = rounds[roundId];
        Miner storage miner = miners[roundId][user];

        // Skip if already checkpointed or round not settled
        if (miner.checkpointed || !round.settled) return;

        uint8 winningBlock = round.winningBlock;

        // Bitmask check
        uint256 userDeployed = (miner.deployedMask & (1 << winningBlock)) != 0
            ? miner.amountPerBlock
            : 0;

        // If user didn't deploy to winning block, mark as checkpointed and return
        if (userDeployed == 0) {
            miner.checkpointed = true;
            emit Checkpointed(roundId, user, 0, 0);
            return;
        }

        uint256 winnersDeployed = round.winnersDeployed;

        // Calculate ETH rewards: proportional share of actual retained ETH
        uint256 ethReward;
        unchecked {
            uint256 claimablePool = _getClaimablePool(round.totalDeployed, winnersDeployed);
            ethReward = (claimablePool * userDeployed) / winnersDeployed;
        }

        // Calculate PEA rewards
        uint256 peaReward;

        if (round.topMiner == address(0)) {
            peaReward = (round.topMinerReward * userDeployed) / winnersDeployed;
        } else if (round.topMiner == address(1)) {
            if (round.minerCount > maxMinersForSingleWinner) {
                // Too many miners: force proportional split
                round.topMiner = address(0);
                peaReward = (round.topMinerReward * userDeployed) / winnersDeployed;
            } else {
                // Safe to iterate: resolve single winner
                _resolveTopMiner(roundId, round);
                if (round.topMiner == user) {
                    peaReward = round.topMinerReward;
                }
            }
        } else if (round.topMiner == user) {
            peaReward = round.topMinerReward;
        }

        if (round.peapotAmount != 0) {
            peaReward += (round.peapotAmount * userDeployed) / winnersDeployed;
        }

        miner.checkpointed = true;

        // Snapshot pending harvested rewards before modifying unclaimed or debt
        // This converts the accumulator delta into userHarvestedPEA before debt is overwritten
        _updateHarvestingRewards(user);

        // Conditional unclaimed updates
        if (ethReward != 0) {
            userUnclaimedETH[user] += ethReward;
        }
        if (peaReward != 0) {
            userUnclaimedPEA[user] += peaReward;
            totalUnclaimed += peaReward;
        }

        // Update user's harvesting debt
        userHarvestingDebt[user] = accHarvestingPerUnclaimed;

        emit Checkpointed(roundId, user, ethReward, peaReward);
    }

    /**
     * @notice Checkpoint rewards for a specific round
     * @param roundId The round to checkpoint
     */
    function checkpoint(uint64 roundId) external nonReentrant {
        _checkpoint(roundId, msg.sender);
    }

    function _autoCheckpointPrevious(address user) internal {
        uint64 lastRound = userLastRound[user];
        if (lastRound == 0) return;
        if (lastRound >= currentRoundId) return;

        _checkpoint(lastRound, user);
    }

    /**
     * @notice Claim accumulated ETH rewards
     */
    function claimETH() external nonReentrant {
        // Auto-checkpoint previous round
        _autoCheckpointPrevious(msg.sender);

        uint256 amount = userUnclaimedETH[msg.sender];
        if (amount == 0) revert NothingToClaim();

        // Safety net: cap at contract balance to prevent revert from rounding dust
        uint256 balance = address(this).balance;
        if (amount > balance) {
            amount = balance;
        }

        userUnclaimedETH[msg.sender] = 0;

        _safeTransferETH(msg.sender, amount);

        emit ClaimedETH(msg.sender, amount);
    }

    /**
     * @notice Claim accumulated PEA rewards with harvesting fee
     */
    function claimPEA() external nonReentrant {
        // Auto-checkpoint previous round
        _autoCheckpointPrevious(msg.sender);

        // First, update user's harvesting bonus
        _updateHarvestingRewards(msg.sender);

        uint256 minedPEA = userUnclaimedPEA[msg.sender];
        uint256 harvestedPEA = userHarvestedPEA[msg.sender];
        uint256 gross = minedPEA + harvestedPEA;

        if (gross == 0) revert NothingToClaim();

        // Calculate harvesting fee (only on mined, not on harvested bonus)
        uint256 fee = 0;
        if (minedPEA > 0 && totalUnclaimed > 0) {
            fee = (minedPEA * HARVESTING_FEE_BPS) / BPS_DENOMINATOR;

            // Distribute fee to remaining unclaimed holders
            // Subtract user's unclaimed first since they're claiming
            uint256 remainingUnclaimed = totalUnclaimed - minedPEA;
            if (remainingUnclaimed > 0) {
                accHarvestingPerUnclaimed += (fee * 1e18) / remainingUnclaimed;
            } else {
                // No other unclaimed holders: burn the fee instead of losing it
                pea.burn(fee);
            }
        }

        uint256 net = gross - fee;

        // Safety net: cap at contract PEA balance to prevent revert from rounding dust
        uint256 contractPeaBalance = IERC20(address(pea)).balanceOf(address(this));
        if (net > contractPeaBalance) {
            net = contractPeaBalance;
        }

        // Update state
        totalUnclaimed -= minedPEA;
        userUnclaimedPEA[msg.sender] = 0;
        userHarvestedPEA[msg.sender] = 0;
        userHarvestingDebt[msg.sender] = accHarvestingPerUnclaimed;

        // Transfer PEA
        require(IERC20(address(pea)).transfer(msg.sender, net), "Transfer failed");

        emit ClaimedPEA(msg.sender, minedPEA, harvestedPEA, fee, net);
    }

    /**
     * @notice Re-request VRF if the original request wasn't fulfilled within 1 hour
     * @dev Anyone can call this after the timeout period
     */
    function emergencyResetVRF() external payable nonReentrant {
        if (!gameStarted) revert GameNotStarted();

        Round storage round = rounds[currentRoundId];
        RequestState storage req = roundRequests[currentRoundId];

        // Must have a pending randomness request
        if (!req.pending) revert InvalidVRFRequest();

        // Must not be already settled
        if (round.settled) revert RoundAlreadySettled();

        // Must be at least 1 hour since round ended
        if (block.timestamp < round.endTime + 1 hours) revert EmergencyTooEarly();

        // Unmap the old request: if its reveal ever lands, _fulfillRandomness
        // resolves roundId 0 and drops it silently. Note the old provider is used
        // for the key, the owner may have rotated `provider` since the request.
        uint64 oldSeq = req.seq;
        delete requestToRound[_requestKey(req.provider, oldSeq)];

        // Re-request from the CURRENT provider (owner rotates via setProvider if
        // the original is dead/exhausted.
        _requestSettlement(currentRoundId);

        emit EmergencyVRFRequested(currentRoundId, oldSeq, req.seq, req.provider);
    }

    // ============ Internal Functions ============

    function _startNextRound() internal {
        currentRoundId += 1;

        Round storage nextRound = rounds[currentRoundId];
        nextRound.startTime = block.timestamp;
        nextRound.endTime = block.timestamp + ROUND_DURATION;

        emit GameStarted(currentRoundId, nextRound.startTime, nextRound.endTime);
    }


    function _getTopMinerSample(uint64 roundId) internal view returns (uint256) {
        Round storage round = rounds[roundId];

        return round.topMinerSeed % round.winnersDeployed;
    }

    /**
     * @notice Resolve top miner by iterating deployers in index order
     * @dev Called once per single-winner round on first checkpoint. Builds cumulative
     *      ranges on the fly for the winning block and finds whose range contains the sample.
     */
    function _resolveTopMiner(uint64 roundId, Round storage round) internal {
        uint8 winningBlock = round.winningBlock;
        uint256 sample = round.topMinerSeed % round.winnersDeployed;
        uint256 cumulative;
        uint256 count = round.minerCount;

        for (uint256 i; i < count; ) {
            address addr = minerByIndex[roundId][i];
            Miner storage m = miners[roundId][addr];

            if ((m.deployedMask & (1 << winningBlock)) != 0) {
                uint256 amt = m.amountPerBlock;
                if (sample >= cumulative && sample < cumulative + amt) {
                    round.topMiner = addr;
                    return;
                }
                cumulative += amt;
            }

            unchecked { ++i; }
        }
    }

    /// @dev View-only version of _resolveTopMiner for getTotalPendingRewards
    function _viewResolveTopMiner(uint64 roundId, Round storage round) internal view returns (address) {
        uint8 winningBlock = round.winningBlock;
        uint256 sample = round.topMinerSeed % round.winnersDeployed;
        uint256 cumulative;
        uint256 count = round.minerCount;

        for (uint256 i; i < count; ) {
            address addr = minerByIndex[roundId][i];
            Miner storage m = miners[roundId][addr];

            if ((m.deployedMask & (1 << winningBlock)) != 0) {
                uint256 amt = m.amountPerBlock;
                if (sample >= cumulative && sample < cumulative + amt) {
                    return addr;
                }
                cumulative += amt;
            }

            unchecked { ++i; }
        }
        return address(0);
    }

    function _getClaimablePool(uint256 totalDeployed, uint256 winnersDeployed) internal pure returns (uint256) {
        uint256 adminFee = (totalDeployed * ADMIN_FEE_BPS) / BPS_DENOMINATOR;
        uint256 losersPool = totalDeployed - winnersDeployed;
        uint256 losersAdminShare = (losersPool * ADMIN_FEE_BPS) / BPS_DENOMINATOR;
        uint256 vaultAmount = ((losersPool - losersAdminShare) * VAULT_FEE_BPS) / BPS_DENOMINATOR;
        return totalDeployed - adminFee - vaultAmount;
    }

    function _updateHarvestingRewards(address user) internal {
        uint256 unclaimed = userUnclaimedPEA[user];
        if (unclaimed == 0) return;

        uint256 accumulatedPerToken = accHarvestingPerUnclaimed - userHarvestingDebt[user];
        uint256 pending = (unclaimed * accumulatedPerToken) / 1e18;

        userHarvestedPEA[user] += pending;
        userHarvestingDebt[user] = accHarvestingPerUnclaimed;
    }

    function _safeTransferETH(address to, uint256 amount) internal {
        (bool success, ) = to.call{value: amount}("");
        if (!success) revert TransferFailed();
    }

    // ============ View Functions ============

    function getRound(uint64 roundId) external view returns (
        uint256 startTime,
        uint256 endTime,
        uint256 totalDeployed,
        uint256 totalWinnings,
        uint8 winningBlock,
        address topMiner,
        uint256 topMinerReward,
        uint256 peapotAmount,
        bool settled
    ) {
        Round storage round = rounds[roundId];
        return (
            round.startTime,
            round.endTime,
            round.totalDeployed,
            round.totalWinnings,
            round.winningBlock,
            round.topMiner,
            round.topMinerReward,
            round.peapotAmount,
            round.settled
        );
    }

    function getRoundDeployed(uint64 roundId) external view returns (uint256[25] memory) {
        return rounds[roundId].deployed;
    }

    function getMinerInfo(uint64 roundId, address user) external view returns (
        uint256 deployedMask,
        uint256 amountPerBlock,
        bool checkpointed
    ) {
        Miner storage miner = miners[roundId][user];
        return (miner.deployedMask, miner.amountPerBlock, miner.checkpointed);
    }

    function getPendingETH(address user) external view returns (uint256) {
        return userUnclaimedETH[user];
    }

    function getPendingPEA(address user) external view returns (uint256 gross, uint256 fee, uint256 net) {
        uint256 minedPEA = userUnclaimedPEA[user];

        // Calculate pending harvesting bonus
        uint256 accumulatedPerToken = accHarvestingPerUnclaimed - userHarvestingDebt[user];
        uint256 pendingHarvested = (minedPEA * accumulatedPerToken) / 1e18;
        uint256 harvestedPEA = userHarvestedPEA[user] + pendingHarvested;

        gross = minedPEA + harvestedPEA;

        // Fee only on mined
        fee = (minedPEA * HARVESTING_FEE_BPS) / BPS_DENOMINATOR;
        net = gross - fee;
    }

    function getTotalPendingRewards(address user) external view returns (
        uint256 pendingETH,
        uint256 pendingUnharvestedPEA,
        uint256 pendingHarvestedPEA,
        uint64 uncheckpointedRound
    ) {
        // 1. Already checkpointed rewards
        pendingETH = userUnclaimedETH[user];
        pendingUnharvestedPEA = userUnclaimedPEA[user];

        // 2. Harvesting bonus (stored + pending)
        pendingHarvestedPEA = userHarvestedPEA[user]
            + (userUnclaimedPEA[user] * (accHarvestingPerUnclaimed - userHarvestingDebt[user])) / 1e18;

        // 3. Check for uncheckpointed round
        uint64 lastRound = userLastRound[user];
        if (lastRound == 0) return (pendingETH, pendingUnharvestedPEA, pendingHarvestedPEA, 0);

        Round storage round = rounds[lastRound];
        Miner storage miner = miners[lastRound][user];

        if (miner.checkpointed || !round.settled) {
            return (pendingETH, pendingUnharvestedPEA, pendingHarvestedPEA, 0);
        }

        // 4. Calculate uncheckpointed rewards
        {
            uint8 winningBlock = round.winningBlock;
            uint256 userDeployed = (miner.deployedMask & (1 << winningBlock)) != 0
                ? miner.amountPerBlock
                : 0;

            if (userDeployed == 0) {
                return (pendingETH, pendingUnharvestedPEA, pendingHarvestedPEA, 0);
            }

            uncheckpointedRound = lastRound;
            uint256 winnersDeployed = round.winnersDeployed;

            // ETH: proportional share of actual retained ETH
            pendingETH += (_getClaimablePool(round.totalDeployed, winnersDeployed) * userDeployed) / winnersDeployed;

            // PEA (uncheckpointed goes to unharvested)
            uint256 peaReward;

            if (round.topMiner == address(0)) {
                peaReward = (round.topMinerReward * userDeployed) / winnersDeployed;
            } else if (round.topMiner == address(1)) {
                if (round.minerCount > maxMinersForSingleWinner) {
                    // Over cap: would be forced to split on checkpoint
                    peaReward = (round.topMinerReward * userDeployed) / winnersDeployed;
                } else {
                    // Unresolved single winner: iterate deployers to check
                    if (_viewResolveTopMiner(lastRound, round) == user) {
                        peaReward = round.topMinerReward;
                    }
                }
            } else if (round.topMiner == user) {
                peaReward = round.topMinerReward;
            }

            if (round.peapotAmount != 0) {
                peaReward += (round.peapotAmount * userDeployed) / winnersDeployed;
            }

            pendingUnharvestedPEA += peaReward;
        }
    }

    function getCurrentRoundInfo() external view returns (
        uint64 roundId,
        uint256 startTime,
        uint256 endTime,
        uint256 totalDeployed,
        uint256 timeRemaining,
        bool isActive
    ) {
        Round storage round = rounds[currentRoundId];

        roundId = currentRoundId;
        startTime = round.startTime;
        endTime = round.endTime;
        totalDeployed = round.totalDeployed;
        timeRemaining = block.timestamp >= round.endTime ? 0 : round.endTime - block.timestamp;
        isActive = gameStarted && block.timestamp < round.endTime && !round.settled;
    }

    // ============ Receive ============

    receive() external payable {}
}
Chain explorer6381msChain node77ms