AutoMiner

0x88d3eb3b38dfb9a62b435809144c771e9cab64a1

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
13,347 bytes
ABI entries
84
27 read · 15 write
License
none

Contract information

Address
0x88d3eb3b38dfb9a62b435809144c771e9cab64a1
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0x67c5b75486…254d478b7a

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (27)

BPS_DENOMINATOR()uint256
GRID_SIZE()uint8
MAX_FEE_BPS()uint256
MAX_FLAT_FEE()uint256
SELECT_STRATEGY_ID()uint8
accumulatedFees()uint256
activeUserIndex(address)uint256
activeUsers(uint256)address
canExecute(address)bool, string
configs(address)uint8, uint8, bool, uint16, uint32, uint128, uint64, uint64, uint128, uint32, uint96
executor()address
executorFeeBps()uint256
executorFlatFee()uint256
getActiveUserCount()uint256
getActiveUsers(uint256, uint256)address[]
getConfigProgress(address)bool, uint256, uint256, uint256, uint256
getRoundsRemaining(address)uint256
getStrategy(uint8)uint8, bool, bool
getUserState(address)tuple, uint64, uint256, uint256, uint256
gridMining()address
lastRoundPlayed(address)uint64
minDeploy()uint256
minGasForBatch()uint256
minGasReserve()uint256
owner()address
strategies(uint8)uint8, bool, bool
strategyCount()uint8

Events (14)

BatchExecutedBatchStoppedConfigDeactivatedConfigUpdatedExecutedForExecutorFeeBpsUpdatedExecutorFlatFeeUpdatedExecutorUpdatedFeesCollectedGridMiningUpdatedOwnershipTransferredStoppedStrategyAddedStrategyUpdated

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_gridMining",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_executor",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "_executorFeeBps",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "_executorFlatFee",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AlreadyPlayedThisRound",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ConfigAlreadyActive",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ConfigNotActive",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DuplicateBlock",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeTooHigh",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "GameNotStarted",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientDeposit",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientGas",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidBlockCount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidBlockId",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidBlockMask",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidDeposit",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidFixedBlocks",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidGasLimits",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidNumBlocks",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidNumRounds",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidStrategy",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoFeesToCollect",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotExecutor",
    "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": "RoundLimitReached",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RoundNotActive",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "StrategyNotFound",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "total",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "successful",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "failed",
        "type": "uint256"
      }
    ],
    "name": "BatchExecuted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "stoppedAtIndex",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "total",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "successful",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "failed",
        "type": "uint256"
      }
    ],
    "name": "BatchStopped",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "roundsCompleted",
        "type": "uint256"
      }
    ],
    "name": "ConfigDeactivated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "strategyId",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "numBlocks",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountPerBlock",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "numRounds",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "depositAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "active",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "uint32",
        "name": "selectedBlockMask",
        "type": "uint32"
      }
    ],
    "name": "ConfigUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint64",
        "name": "roundId",
        "type": "uint64"
      },
      {
        "indexed": false,
        "internalType": "uint8[]",
        "name": "blocks",
        "type": "uint8[]"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "totalDeployed",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "roundsExecuted",
        "type": "uint256"
      }
    ],
    "name": "ExecutedFor",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "oldFeeBps",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newFeeBps",
        "type": "uint256"
      }
    ],
    "name": "ExecutorFeeBpsUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "oldFee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newFee",
        "type": "uint256"
      }
    ],
    "name": "ExecutorFlatFeeUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldExecutor",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newExecutor",
        "type": "address"
      }
    ],
    "name": "ExecutorUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "executor",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "FeesCollected",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldGridMining",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newGridMining",
        "type": "address"
      }
    ],
    "name": "GridMiningUpdated",
    "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": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "refundAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "roundsCompleted",
        "type": "uint256"
      }
    ],
    "name": "Stopped",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint8",
        "name": "strategyId",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "fixedBlocks",
        "type": "uint8"
      }
    ],
    "name": "StrategyAdded",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint8",
        "name": "strategyId",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "active",
        "type": "bool"
      }
    ],
    "name": "StrategyUpdated",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BPS_DENOMINATOR",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "GRID_SIZE",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_FLAT_FEE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SELECT_STRATEGY_ID",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "accumulatedFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "activeUserIndex",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "activeUsers",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "fixedBlocks",
        "type": "uint8"
      }
    ],
    "name": "addStrategy",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "strategyId",
        "type": "uint8"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "canExecute",
    "outputs": [
      {
        "internalType": "bool",
        "name": "executable",
        "type": "bool"
      },
      {
        "internalType": "string",
        "name": "reason",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "collectFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "configs",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "strategyId",
        "type": "uint8"
      },
      {
        "internalType": "uint8",
        "name": "numBlocks",
        "type": "uint8"
      },
      {
        "internalType": "bool",
        "name": "active",
        "type": "bool"
      },
      {
        "internalType": "uint16",
        "name": "executorFeeBps",
        "type": "uint16"
      },
      {
        "internalType": "uint32",
        "name": "selectedBlockMask",
        "type": "uint32"
      },
      {
        "internalType": "uint128",
        "name": "amountPerBlock",
        "type": "uint128"
      },
      {
        "internalType": "uint64",
        "name": "numRounds",
        "type": "uint64"
      },
      {
        "internalType": "uint64",
        "name": "roundsExecuted",
        "type": "uint64"
      },
      {
        "internalType": "uint128",
        "name": "depositAmount",
        "type": "uint128"
      },
      {
        "internalType": "uint32",
        "name": "depositTimestamp",
        "type": "uint32"
      },
      {
        "internalType": "uint96",
        "name": "executorFlatFee",
        "type": "uint96"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address[]",
        "name": "users",
        "type": "address[]"
      },
      {
        "internalType": "uint8[][]",
        "name": "blocks",
        "type": "uint8[][]"
      }
    ],
    "name": "executeBatch",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "successCount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "failCount",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint8[]",
        "name": "blocks",
        "type": "uint8[]"
      }
    ],
    "name": "executeFor",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint8[]",
        "name": "blocks",
        "type": "uint8[]"
      }
    ],
    "name": "executeForInternal",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "executor",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "executorFeeBps",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "executorFlatFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getActiveUserCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "offset",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "limit",
        "type": "uint256"
      }
    ],
    "name": "getActiveUsers",
    "outputs": [
      {
        "internalType": "address[]",
        "name": "users",
        "type": "address[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getConfigProgress",
    "outputs": [
      {
        "internalType": "bool",
        "name": "active",
        "type": "bool"
      },
      {
        "internalType": "uint256",
        "name": "numRounds",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "roundsExecuted",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "roundsRemaining",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "percentComplete",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getRoundsRemaining",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "strategyId",
        "type": "uint8"
      }
    ],
    "name": "getStrategy",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "fixedBlocks",
        "type": "uint8"
      },
      {
        "internalType": "bool",
        "name": "exists",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "active",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getUserState",
    "outputs": [
      {
        "components": [
          {
            "internalType": "uint8",
            "name": "strategyId",
            "type": "uint8"
          },
          {
            "internalType": "uint8",
            "name": "numBlocks",
            "type": "uint8"
          },
          {
            "internalType": "bool",
            "name": "active",
            "type": "bool"
          },
          {
            "internalType": "uint16",
            "name": "executorFeeBps",
            "type": "uint16"
          },
          {
            "internalType": "uint32",
            "name": "selectedBlockMask",
            "type": "uint32"
          },
          {
            "internalType": "uint128",
            "name": "amountPerBlock",
            "type": "uint128"
          },
          {
            "internalType": "uint64",
            "name": "numRounds",
            "type": "uint64"
          },
          {
            "internalType": "uint64",
            "name": "roundsExecuted",
            "type": "uint64"
          },
          {
            "internalType": "uint128",
            "name": "depositAmount",
            "type": "uint128"
          },
          {
            "internalType": "uint32",
            "name": "depositTimestamp",
            "type": "uint32"
          },
          {
            "internalType": "uint96",
            "name": "executorFlatFee",
            "type": "uint96"
          }
        ],
        "internalType": "struct AutoMiner.AutoConfig",
        "name": "config",
        "type": "tuple"
      },
      {
        "internalType": "uint64",
        "name": "lastRound",
        "type": "uint64"
      },
      {
        "internalType": "uint256",
        "name": "costPerRound",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "roundsRemaining",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "totalRefundable",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "gridMining",
    "outputs": [
      {
        "internalType": "contract IGridMining",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "lastRoundPlayed",
    "outputs": [
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "minDeploy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "minGasForBatch",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "minGasReserve",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "strategyId",
        "type": "uint8"
      },
      {
        "internalType": "uint256",
        "name": "numRounds",
        "type": "uint256"
      },
      {
        "internalType": "uint8",
        "name": "numBlocks",
        "type": "uint8"
      },
      {
        "internalType": "uint32",
        "name": "blockMask",
        "type": "uint32"
      }
    ],
    "name": "setConfig",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_executor",
        "type": "address"
      }
    ],
    "name": "setExecutor",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "_feeBps",
        "type": "uint256"
      }
    ],
    "name": "setExecutorFeeBps",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "_flatFee",
        "type": "uint256"
      }
    ],
    "name": "setExecutorFlatFee",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "_minForBatch",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "_minReserve",
        "type": "uint256"
      }
    ],
    "name": "setGasLimits",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_gridMining",
        "type": "address"
      }
    ],
    "name": "setGridMining",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "strategyId",
        "type": "uint8"
      },
      {
        "internalType": "bool",
        "name": "active",
        "type": "bool"
      }
    ],
    "name": "setStrategyActive",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "stop",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "name": "strategies",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "fixedBlocks",
        "type": "uint8"
      },
      {
        "internalType": "bool",
        "name": "exists",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "active",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "strategyCount",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

interface IGridMining {
    function MIN_DEPLOY() external pure returns (uint256);
    function deployFor(address user, uint8[] calldata blockIds) external payable;
    function currentRoundId() external view returns (uint64);
    function gameStarted() external view returns (bool);
    function rounds(uint64 roundId) external view returns (
        uint256 startTime,
        uint256 endTime,
        uint256 totalDeployed,
        uint256 totalWinnings,
        uint256 winnersDeployed,
        uint8 winningBlock,
        address topMiner,
        uint256 topMinerReward,
        uint256 peapotAmount,
        uint256 topMinerSeed,
        bool settled
    );
}

contract AutoMiner is Ownable, ReentrancyGuard {
    // ============ Constants ============

    uint256 public constant BPS_DENOMINATOR = 10000;
    uint256 public constant MAX_FEE_BPS = 1000;
    uint256 public constant MAX_FLAT_FEE = 0.005 ether;
    uint8 public constant GRID_SIZE = 25;
    uint8 public constant SELECT_STRATEGY_ID = 2;

    // ============ Types ============

    struct StrategyDef {
        uint8 fixedBlocks;  // 0 = user specifies numBlocks, 1-25 = enforced count
        bool exists;
        bool active;
    }

    // ============ State ============

    IGridMining public gridMining;
    address public executor;
    uint256 public executorFeeBps;
    uint256 public executorFlatFee;
    uint256 public minDeploy;           // Cached from GridMining.MIN_DEPLOY()

    mapping(uint8 => StrategyDef) public strategies;
    uint8 public strategyCount;

    struct AutoConfig {
        // Slot 0 (9 bytes used, 23 free)
        uint8 strategyId;              // uint8 - references strategies mapping
        uint8 numBlocks;               // uint8 - resolved block count
        bool active;                   // bool
        uint16 executorFeeBps;         // uint16 - frozen at config time
        uint32 selectedBlockMask;      // uint32 - bitmask for Select strategy (0 = not Select)

        // Slot 1
        uint128 amountPerBlock;        // uint128 (calculated, not user-provided)
        uint64 numRounds;              // uint64 (user-specified execution limit)
        uint64 roundsExecuted;         // uint64 (successful executions count)

        // Slot 2
        uint128 depositAmount;         // uint128 (original deposit for refunds)
        uint32 depositTimestamp;       // uint32 (config creation time)
        uint96 executorFlatFee;        // uint96 (frozen flat fee at config time)
    }

    mapping(address => AutoConfig) public configs;
    mapping(address => uint64) public lastRoundPlayed;

    // Track active users
    address[] public activeUsers;
    mapping(address => uint256) public activeUserIndex;  // index + 1 (0 means not in array)

    // Accumulated fees for executor
    uint256 public accumulatedFees;

    // Gas limits for batch execution
    uint256 public minGasForBatch = 2_000_000;  // Minimum gas to start batch
    uint256 public minGasReserve = 100_000;      // Gas threshold for exit

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

    event ConfigUpdated(
        address indexed user,
        uint8 strategyId,
        uint8 numBlocks,
        uint256 amountPerBlock,
        uint256 numRounds,
        uint256 depositAmount,
        bool active,
        uint32 selectedBlockMask
    );
    event ExecutedFor(
        address indexed user,
        uint64 indexed roundId,
        uint8[] blocks,
        uint256 totalDeployed,
        uint256 fee,
        uint256 roundsExecuted
    );
    event ConfigDeactivated(address indexed user, uint256 roundsCompleted);
    event Stopped(address indexed user, uint256 refundAmount, uint256 roundsCompleted);
    event BatchExecuted(uint256 total, uint256 successful, uint256 failed);
    event BatchStopped(uint256 indexed stoppedAtIndex, uint256 total, uint256 successful, uint256 failed);
    event ExecutorUpdated(address indexed oldExecutor, address indexed newExecutor);
    event GridMiningUpdated(address indexed oldGridMining, address indexed newGridMining);
    event ExecutorFeeBpsUpdated(uint256 oldFeeBps, uint256 newFeeBps);
    event ExecutorFlatFeeUpdated(uint256 oldFee, uint256 newFee);
    event FeesCollected(address indexed executor, uint256 amount);
    event StrategyAdded(uint8 indexed strategyId, uint8 fixedBlocks);
    event StrategyUpdated(uint8 indexed strategyId, bool active);

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

    error ZeroAddress();
    error NotExecutor();
    error InvalidNumBlocks();
    error ConfigNotActive();
    error AlreadyPlayedThisRound();
    error GameNotStarted();
    error RoundNotActive();
    error FeeTooHigh();
    error NoFeesToCollect();
    error InvalidBlockCount();
    error DuplicateBlock();
    error InvalidBlockId();
    error ConfigAlreadyActive();
    error InvalidDeposit();
    error InvalidNumRounds();
    error InsufficientDeposit();
    error RoundLimitReached();
    error InvalidStrategy();
    error InvalidFixedBlocks();
    error StrategyNotFound();
    error InsufficientGas();
    error InvalidGasLimits();
    error InvalidBlockMask();

    // ============ Modifiers ============

    modifier onlyExecutor() {
        if (msg.sender != executor) revert NotExecutor();
        _;
    }

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

    constructor(
        address _gridMining,
        address _executor,
        uint256 _executorFeeBps,
        uint256 _executorFlatFee
    ) Ownable(msg.sender) {
        if (_gridMining == address(0)) revert ZeroAddress();
        if (_executor == address(0)) revert ZeroAddress();
        if (_executorFeeBps > MAX_FEE_BPS) revert FeeTooHigh();
        if (_executorFlatFee > MAX_FLAT_FEE) revert FeeTooHigh();

        gridMining = IGridMining(_gridMining);
        executor = _executor;
        executorFeeBps = _executorFeeBps;
        executorFlatFee = _executorFlatFee;
        minDeploy = IGridMining(_gridMining).MIN_DEPLOY();

        // Seed default strategies: 0=Random, 1=All, 2=Select
        strategies[0] = StrategyDef({fixedBlocks: 0, exists: true, active: true});
        strategies[1] = StrategyDef({fixedBlocks: GRID_SIZE, exists: true, active: true});
        strategies[2] = StrategyDef({fixedBlocks: 0, exists: true, active: true});
        strategyCount = 3;
    }

    // ============ User Functions ============

    /**
     * @notice Set auto-mining configuration with deposit
     * @param strategyId Strategy ID from the registry
     * @param numRounds Number of rounds to execute
     * @param numBlocks Number of blocks (used for strategy 0; ignored for strategy 1 and 2)
     * @param blockMask Bitmask of selected blocks (only used for Select strategy 2; pass 0 otherwise)
     */
    function setConfig(
        uint8 strategyId,
        uint256 numRounds,
        uint8 numBlocks,
        uint32 blockMask
    ) external payable nonReentrant {
        // Validate: Must not have active config (must stop first)
        if (configs[msg.sender].active) revert ConfigAlreadyActive();
        if (msg.value == 0) revert InvalidDeposit();
        if (numRounds == 0) revert InvalidNumRounds();

        // Resolve blocks + validate strategy
        (uint8 actualNumBlocks, uint32 storedBlockMask) = _resolveBlocks(strategyId, numBlocks, blockMask);

        // Calculate amountPerBlock with hybrid fee
        uint256 amountPerBlock = _calculateAmountPerBlock(actualNumBlocks, numRounds);

        // Validate minimum deposit
        if (amountPerBlock < minDeploy) revert InsufficientDeposit();

        // Validate deposit covers all rounds + fees
        {
            uint256 deployPerRound = amountPerBlock * actualNumBlocks;
            uint256 feePerRound = _calculateFee(deployPerRound, uint16(executorFeeBps), uint96(executorFlatFee));
            if (msg.value < (deployPerRound + feePerRound) * numRounds) revert InsufficientDeposit();
        }

        // Store config
        configs[msg.sender] = AutoConfig({
            strategyId: strategyId,
            numBlocks: actualNumBlocks,
            active: true,
            executorFeeBps: uint16(executorFeeBps),
            selectedBlockMask: storedBlockMask,
            amountPerBlock: uint128(amountPerBlock),
            numRounds: uint64(numRounds),
            roundsExecuted: 0,
            depositAmount: uint128(msg.value),
            depositTimestamp: uint32(block.timestamp),
            executorFlatFee: uint96(executorFlatFee)
        });

        // Add to active users
        _addToActiveUsers(msg.sender);

        emit ConfigUpdated(msg.sender, strategyId, actualNumBlocks, amountPerBlock, numRounds, msg.value, true, storedBlockMask);
    }

    /**
     * @notice Stop auto-mining and refund remaining rounds
     */
    function stop() external nonReentrant {
        AutoConfig storage config = configs[msg.sender];

        // Validate active config
        if (!config.active) revert ConfigNotActive();

        // Calculate remaining rounds
        uint256 remainingRounds = config.numRounds - config.roundsExecuted;

        if (remainingRounds == 0) {
            _deactivateUser(msg.sender);
            emit Stopped(msg.sender, 0, config.roundsExecuted);
            return;
        }

        // Calculate refund: cost per round * remaining rounds
        uint256 deployPerRound = config.amountPerBlock * config.numBlocks;
        uint256 feePerRound = _calculateFee(deployPerRound, config.executorFeeBps, config.executorFlatFee);
        uint256 costPerRound = deployPerRound + feePerRound;

        // Use actual remaining deposit (handles rounding dust)
        uint256 totalSpent = costPerRound * config.roundsExecuted;
        uint256 refundAmount = config.depositAmount - totalSpent;

        // Deactivate before transfer
        _deactivateUser(msg.sender);

        // Transfer refund
        if (refundAmount > 0) {
            (bool success, ) = msg.sender.call{value: refundAmount}("");
            require(success, "Transfer failed");
        }

        emit Stopped(msg.sender, refundAmount, config.roundsExecuted);
    }

    // ============ Executor Functions ============

    /**
     * @notice Execute auto-mining for a single user
     * @param user User to deploy for
     * @param blocks Block IDs to deploy to (ignored for Select strategy, contract uses stored mask)
     */
    function executeFor(
        address user,
        uint8[] calldata blocks
    ) external onlyExecutor nonReentrant {
        _executeFor(user, blocks);
    }

    /**
     * @notice Execute auto-mining for multiple users in batch
     * @param users Array of users to deploy for
     * @param blocks Array of block arrays for each user
     * @dev Uses gas-limited inner calls to prevent OOG from reverting entire batch
     */
    function executeBatch(
        address[] calldata users,
        uint8[][] calldata blocks
    ) external onlyExecutor nonReentrant returns (uint256 successCount, uint256 failCount) {
        if (gasleft() < minGasForBatch) revert InsufficientGas();
        require(users.length == blocks.length, "Length mismatch");

        for (uint256 i = 0; i < users.length; i++) {
            // Exit gracefully when running low on gas
            if (gasleft() < minGasReserve) {
                emit BatchStopped(i, users.length, successCount, failCount);
                return (successCount, failCount);
            }

            // Forward remaining gas minus reserve to inner call
            // If inner call runs out, it reverts and gets caught
            uint256 gasToForward = gasleft() - minGasReserve;

            try this.executeForInternal{gas: gasToForward}(users[i], blocks[i]) {
                successCount++;
            } catch {
                failCount++;
            }
        }

        emit BatchExecuted(users.length, successCount, failCount);
    }

    /**
     * @notice Internal execute function callable only by this contract
     */
    function executeForInternal(
        address user,
        uint8[] calldata blocks
    ) external {
        require(msg.sender == address(this), "Only self");
        _executeFor(user, blocks);
    }

    /**
     * @notice Collect accumulated executor fees
     */
    function collectFees() external onlyExecutor {
        uint256 amount = accumulatedFees;
        if (amount == 0) revert NoFeesToCollect();

        accumulatedFees = 0;

        (bool success, ) = executor.call{value: amount}("");
        require(success, "Transfer failed");

        emit FeesCollected(executor, amount);
    }

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

    /**
     * @notice Resolve block count and mask from strategy + params
     */
    function _resolveBlocks(
        uint8 strategyId,
        uint8 numBlocks,
        uint32 blockMask
    ) internal view returns (uint8 actualNumBlocks, uint32 storedBlockMask) {
        StrategyDef storage strat = strategies[strategyId];
        if (!strat.exists || !strat.active) revert InvalidStrategy();

        if (strategyId == SELECT_STRATEGY_ID) {
            if (blockMask == 0) revert InvalidBlockMask();
            if (blockMask >= (1 << GRID_SIZE)) revert InvalidBlockMask();
            actualNumBlocks = _popcount(blockMask);
            storedBlockMask = blockMask;
        } else if (strat.fixedBlocks > 0) {
            actualNumBlocks = strat.fixedBlocks;
        } else {
            if (numBlocks == 0 || numBlocks > GRID_SIZE) revert InvalidNumBlocks();
            actualNumBlocks = numBlocks;
        }
    }

    /**
     * @notice Calculate amountPerBlock with hybrid fee (% first, fallback to flat)
     */
    function _calculateAmountPerBlock(
        uint8 actualNumBlocks,
        uint256 numRounds
    ) internal view returns (uint256 amountPerBlock) {
        uint256 totalBlocks = uint256(actualNumBlocks) * numRounds;

        // Try percentage
        amountPerBlock = (msg.value * BPS_DENOMINATOR) / (totalBlocks * (BPS_DENOMINATOR + executorFeeBps));

        // Check if flat fee should override
        uint256 pctFee = (amountPerBlock * actualNumBlocks * executorFeeBps) / BPS_DENOMINATOR;
        if (pctFee < executorFlatFee) {
            // Flat fee wins — recalculate
            uint256 totalFlatFees = executorFlatFee * numRounds;
            if (msg.value <= totalFlatFees) revert InsufficientDeposit();
            amountPerBlock = (msg.value - totalFlatFees) / totalBlocks;
        }
    }

    function _executeFor(address user, uint8[] calldata blocks) internal {
        AutoConfig storage config = configs[user];

        // Validations
        if (!config.active) revert ConfigNotActive();
        if (!gridMining.gameStarted()) revert GameNotStarted();

        uint64 currentRound = gridMining.currentRoundId();
        if (lastRoundPlayed[user] >= currentRound) revert AlreadyPlayedThisRound();

        // Check round limit before execution
        if (config.roundsExecuted >= config.numRounds) {
            _deactivateUser(user);
            revert RoundLimitReached();
        }

        // Calculate costs using frozen hybrid fee
        uint256 deployAmount = config.amountPerBlock * config.numBlocks;
        uint256 fee = _calculateFee(deployAmount, config.executorFeeBps, config.executorFlatFee);

        // Check round is still active
        (, uint256 endTime, , , , , , , , , bool settled) = gridMining.rounds(currentRound);
        if (settled || block.timestamp >= endTime) revert RoundNotActive();

        // Accumulate fees
        accumulatedFees += fee;
        lastRoundPlayed[user] = currentRound;

        // Deploy via GridMining — branch on strategy type
        if (config.selectedBlockMask != 0) {
            // Select strategy: contract is authoritative, build blocks from stored mask
            uint8[] memory deployBlocks = _maskToBlocks(config.selectedBlockMask, config.numBlocks);
            gridMining.deployFor{value: deployAmount}(user, deployBlocks);

            config.roundsExecuted++;
            if (config.roundsExecuted >= config.numRounds) {
                _deactivateUser(user);
            }
            emit ExecutedFor(user, currentRound, deployBlocks, deployAmount, fee, config.roundsExecuted);
        } else {
            // Random/All: use executor-provided blocks with validation
            if (blocks.length != config.numBlocks) revert InvalidBlockCount();

            // Validate block IDs: unique and in range
            uint256 usedMask = 0;
            for (uint256 i = 0; i < blocks.length; i++) {
                if (blocks[i] >= GRID_SIZE) revert InvalidBlockId();
                uint256 bit = 1 << blocks[i];
                if ((usedMask & bit) != 0) revert DuplicateBlock();
                usedMask |= bit;
            }

            gridMining.deployFor{value: deployAmount}(user, blocks);

            config.roundsExecuted++;
            if (config.roundsExecuted >= config.numRounds) {
                _deactivateUser(user);
            }
            emit ExecutedFor(user, currentRound, blocks, deployAmount, fee, config.roundsExecuted);
        }
    }

    /**
     * @notice Hybrid fee: max(flatFee, percentageFee)
     */
    function _calculateFee(
        uint256 deployAmount,
        uint16 feeBps,
        uint96 flatFee
    ) internal pure returns (uint256) {
        uint256 pctFee = (deployAmount * feeBps) / BPS_DENOMINATOR;
        return pctFee > flatFee ? pctFee : flatFee;
    }

    /**
     * @notice Count set bits in a bitmask
     */
    function _popcount(uint32 x) internal pure returns (uint8 count) {
        while (x != 0) {
            x &= x - 1;
            count++;
        }
    }

    /**
     * @notice Convert a block bitmask to a uint8[] array
     */
    function _maskToBlocks(uint32 mask, uint8 count) internal pure returns (uint8[] memory blockIds) {
        blockIds = new uint8[](count);
        uint256 idx = 0;
        for (uint8 i = 0; i < GRID_SIZE; i++) {
            if ((mask & (1 << i)) != 0) {
                blockIds[idx++] = i;
            }
        }
    }

    function _deactivateUser(address user) internal {
        configs[user].active = false;
        _removeFromActiveUsers(user);
        emit ConfigDeactivated(user, configs[user].roundsExecuted);
    }

    function _addToActiveUsers(address user) internal {
        if (activeUserIndex[user] == 0) {
            activeUsers.push(user);
            activeUserIndex[user] = activeUsers.length;  // Store index + 1
        }
    }

    function _removeFromActiveUsers(address user) internal {
        uint256 indexPlusOne = activeUserIndex[user];
        if (indexPlusOne == 0) return;

        uint256 index = indexPlusOne - 1;
        uint256 lastIndex = activeUsers.length - 1;

        if (index != lastIndex) {
            // Move last element to deleted position
            address lastUser = activeUsers[lastIndex];
            activeUsers[index] = lastUser;
            activeUserIndex[lastUser] = indexPlusOne;
        }

        activeUsers.pop();
        activeUserIndex[user] = 0;
    }

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

    function setExecutor(address _executor) external onlyOwner {
        if (_executor == address(0)) revert ZeroAddress();

        address old = executor;
        executor = _executor;

        emit ExecutorUpdated(old, _executor);
    }

    function setGridMining(address _gridMining) external onlyOwner {
        if (_gridMining == address(0)) revert ZeroAddress();

        address old = address(gridMining);
        gridMining = IGridMining(_gridMining);
        minDeploy = IGridMining(_gridMining).MIN_DEPLOY();

        emit GridMiningUpdated(old, _gridMining);
    }

    function addStrategy(uint8 fixedBlocks) external onlyOwner returns (uint8 strategyId) {
        if (fixedBlocks > GRID_SIZE) revert InvalidFixedBlocks();

        strategyId = strategyCount;
        strategies[strategyId] = StrategyDef({
            fixedBlocks: fixedBlocks,
            exists: true,
            active: true
        });
        strategyCount++;

        emit StrategyAdded(strategyId, fixedBlocks);
    }

    function setStrategyActive(uint8 strategyId, bool active) external onlyOwner {
        if (!strategies[strategyId].exists) revert StrategyNotFound();
        strategies[strategyId].active = active;
        emit StrategyUpdated(strategyId, active);
    }

    function setExecutorFeeBps(uint256 _feeBps) external onlyOwner {
        if (_feeBps > MAX_FEE_BPS) revert FeeTooHigh();

        uint256 old = executorFeeBps;
        executorFeeBps = _feeBps;

        emit ExecutorFeeBpsUpdated(old, _feeBps);
    }

    function setExecutorFlatFee(uint256 _flatFee) external onlyOwner {
        if (_flatFee > MAX_FLAT_FEE) revert FeeTooHigh();

        uint256 old = executorFlatFee;
        executorFlatFee = _flatFee;

        emit ExecutorFlatFeeUpdated(old, _flatFee);
    }

    function setGasLimits(uint256 _minForBatch, uint256 _minReserve) external onlyOwner {
        if (_minForBatch < 500_000) revert InvalidGasLimits();
        if (_minReserve < 50_000 || _minReserve >= _minForBatch) revert InvalidGasLimits();
        minGasForBatch = _minForBatch;
        minGasReserve = _minReserve;
    }

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

    /**
     * @notice Get user's full auto-mining state
     */
    function getUserState(address user) external view returns (
        AutoConfig memory config,
        uint64 lastRound,
        uint256 costPerRound,
        uint256 roundsRemaining,
        uint256 totalRefundable
    ) {
        config = configs[user];
        lastRound = lastRoundPlayed[user];

        if (config.amountPerBlock > 0 && config.numBlocks > 0) {
            uint256 deployAmount = config.amountPerBlock * config.numBlocks;
            uint256 fee = _calculateFee(deployAmount, config.executorFeeBps, config.executorFlatFee);
            costPerRound = deployAmount + fee;

            if (config.roundsExecuted < config.numRounds) {
                roundsRemaining = config.numRounds - config.roundsExecuted;
                totalRefundable = costPerRound * roundsRemaining;
            }
        }
    }

    /**
     * @notice Get rounds remaining for a user
     */
    function getRoundsRemaining(address user) external view returns (uint256) {
        AutoConfig storage config = configs[user];
        if (config.roundsExecuted >= config.numRounds) return 0;
        return config.numRounds - config.roundsExecuted;
    }

    /**
     * @notice Get execution progress for a user
     */
    function getConfigProgress(address user) external view returns (
        bool active,
        uint256 numRounds,
        uint256 roundsExecuted,
        uint256 roundsRemaining,
        uint256 percentComplete
    ) {
        AutoConfig storage config = configs[user];
        active = config.active;
        numRounds = config.numRounds;
        roundsExecuted = config.roundsExecuted;

        if (roundsExecuted >= numRounds) {
            roundsRemaining = 0;
            percentComplete = 100;
        } else {
            roundsRemaining = numRounds - roundsExecuted;
            percentComplete = (roundsExecuted * 100) / numRounds;
        }
    }

    /**
     * @notice Get strategy definition
     */
    function getStrategy(uint8 strategyId) external view returns (uint8 fixedBlocks, bool exists, bool active) {
        StrategyDef storage s = strategies[strategyId];
        return (s.fixedBlocks, s.exists, s.active);
    }

    /**
     * @notice Get number of active users
     */
    function getActiveUserCount() external view returns (uint256) {
        return activeUsers.length;
    }

    /**
     * @notice Get active users (paginated)
     * @param offset Starting index
     * @param limit Max number to return
     */
    function getActiveUsers(uint256 offset, uint256 limit) external view returns (address[] memory users) {
        uint256 total = activeUsers.length;
        if (offset >= total) return new address[](0);

        uint256 end = offset + limit;
        if (end > total) end = total;
        uint256 count = end - offset;

        users = new address[](count);
        for (uint256 i = 0; i < count; i++) {
            users[i] = activeUsers[offset + i];
        }
    }

    /**
     * @notice Check if user can be executed this round
     */
    function canExecute(address user) external view returns (bool executable, string memory reason) {
        AutoConfig storage config = configs[user];

        if (!config.active) return (false, "Not active");
        if (!gridMining.gameStarted()) return (false, "Game not started");

        uint64 currentRound = gridMining.currentRoundId();
        if (lastRoundPlayed[user] >= currentRound) return (false, "Already played");

        if (config.roundsExecuted >= config.numRounds) return (false, "Round limit reached");

        (, uint256 endTime, , , , , , , , , bool settled) = gridMining.rounds(currentRound);
        if (settled || block.timestamp >= endTime) return (false, "Round not active");

        return (true, "");
    }

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

    receive() external payable {
        // Allow receiving ETH 
    }
}
Chain explorer4546msChain node81ms