MemeFolioDistributor

0x616f634efde75d9401defb6ef3f158223b5b9f23

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
12,679 bytes
ABI entries
121
46 read · 18 write
License
none

Contract information

Address
0x616f634efde75d9401defb6ef3f158223b5b9f23
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xb4259e0665…a6fa637c12

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (46)

ADMIN_TRANSFER_DELAY()uint48
DEFAULT_ADMIN_ROLE()bytes32
DISTRIBUTION_INTERVAL()uint256
OPERATOR_ROLE()bytes32
PAUSER_ROLE()bytes32
REWARD_TOKEN_COUNT()uint256
REWARD_TOKEN_UPDATE_DELAY()uint256
activeCycleId()bytes32
cancelledCycleIds(bytes32)bool
completedCycleIds(bytes32)bool
cursor()uint256
cycleActive()bool
defaultAdmin()address
defaultAdminDelay()uint48
defaultAdminDelayIncreaseWait()uint48
eligible()uint256
excludedAccountAt(uint256)address
excludedAccountCount()uint256
excludedFromRewards(address)bool
expectedHolderCount()uint256
getRoleAdmin(bytes32)bytes32
hasRole(bytes32, address)bool
isRewardToken(address)bool
lastProcessedHolder()address
maxDistributionBatchSize()uint256
minHolderBalance()uint256
nextDistribution()uint256
owner()address
paidHolderCount()uint256
paused()bool
pendingDefaultAdmin()address, uint48
pendingDefaultAdminDelay()uint48, uint48
pendingRewardTokenAt(uint256)address
pendingRewardTokenUpdateExecutableAt()uint256
pendingRewardTokenUpdateId()bytes32
potAt(uint256)uint256
processedEligibleBalance()uint256
rewardTokenAt(uint256)address
rewardTokenCount()uint256
rewardTokenUpdateNonce()uint256
snapshotBlock()uint256
snapshotBlockHash()bytes32
sourceToken()address
supportsInterface(bytes4)bool
treasury()address
usedCycleIds(bytes32)bool

Events (19)

BatchPaidCycleCancelledCycleStartedDefaultAdminDelayChangeCanceledDefaultAdminDelayChangeScheduledDefaultAdminTransferCanceledDefaultAdminTransferScheduledDistributionCompletedForeignTokenRescuedNativeRescuedPausedPayoutSkippedRewardTokenUpdateCancelledRewardTokenUpdateExecutedRewardTokenUpdateProposedRoleAdminChangedRoleGrantedRoleRevokedUnpaused

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "admin",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      },
      {
        "internalType": "contract IMemeFolioSnapshotToken",
        "name": "sourceToken_",
        "type": "address"
      },
      {
        "internalType": "contract IERC20[]",
        "name": "rewardTokens_",
        "type": "address[]"
      },
      {
        "internalType": "uint256",
        "name": "maxDistributionBatchSize_",
        "type": "uint256"
      },
      {
        "internalType": "address[]",
        "name": "excludedAccounts_",
        "type": "address[]"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AccessControlBadConfirmation",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint48",
        "name": "schedule",
        "type": "uint48"
      }
    ],
    "name": "AccessControlEnforcedDefaultAdminDelay",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AccessControlEnforcedDefaultAdminRules",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "defaultAdmin",
        "type": "address"
      }
    ],
    "name": "AccessControlInvalidDefaultAdmin",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "neededRole",
        "type": "bytes32"
      }
    ],
    "name": "AccessControlUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "expected",
        "type": "bytes32"
      },
      {
        "internalType": "bytes32",
        "name": "actual",
        "type": "bytes32"
      }
    ],
    "name": "ActiveCycleMismatch",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "supplied",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "maximum",
        "type": "uint256"
      }
    ],
    "name": "BatchTooLarge",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "holdersPaid",
        "type": "uint256"
      }
    ],
    "name": "CycleAlreadyPaid",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "cycleId",
        "type": "bytes32"
      }
    ],
    "name": "CycleAlreadyUsed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "CycleInProgress",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "processed",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "expected",
        "type": "uint256"
      }
    ],
    "name": "EligibleBalanceExceeded",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "processed",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "expected",
        "type": "uint256"
      }
    ],
    "name": "EligibleBalanceMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EmptyBatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EnforcedPause",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "holder",
        "type": "address"
      }
    ],
    "name": "ExcludedHolder",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExpectedPause",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "holder",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minimum",
        "type": "uint256"
      }
    ],
    "name": "HolderBelowMinimum",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidBatchSize",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "expected",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "actual",
        "type": "uint256"
      }
    ],
    "name": "InvalidCursor",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidHolderCount",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "previous",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "current",
        "type": "address"
      }
    ],
    "name": "InvalidHolderOrder",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidRewardTokenCount",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "snapshotBlock",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "currentBlock",
        "type": "uint256"
      }
    ],
    "name": "InvalidSnapshotBlock",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NativeTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoCycle",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoDistributionPot",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoEligibleHolders",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoPendingRewardTokenUpdate",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "ProtectedToken",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "executableAt",
        "type": "uint256"
      }
    ],
    "name": "RewardTokenUpdateNotReady",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "bits",
        "type": "uint8"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "SafeCastOverflowedUintDowncast",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SnapshotMetadataMismatch",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "nextAllowedTimestamp",
        "type": "uint256"
      }
    ],
    "name": "TooEarly",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "cycleId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fromIndex",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "toIndex",
        "type": "uint256"
      }
    ],
    "name": "BatchPaid",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "cycleId",
        "type": "bytes32"
      }
    ],
    "name": "CycleCancelled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "cycleId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "snapshotBlock",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bytes32",
        "name": "snapshotBlockHash",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "eligibleHolders",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "eligibleBalance",
        "type": "uint256"
      }
    ],
    "name": "CycleStarted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [],
    "name": "DefaultAdminDelayChangeCanceled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint48",
        "name": "newDelay",
        "type": "uint48"
      },
      {
        "indexed": false,
        "internalType": "uint48",
        "name": "effectSchedule",
        "type": "uint48"
      }
    ],
    "name": "DefaultAdminDelayChangeScheduled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [],
    "name": "DefaultAdminTransferCanceled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "newAdmin",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint48",
        "name": "acceptSchedule",
        "type": "uint48"
      }
    ],
    "name": "DefaultAdminTransferScheduled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "cycleId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "frozenPot",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "holders",
        "type": "uint256"
      }
    ],
    "name": "DistributionCompleted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "treasury",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "ForeignTokenRescued",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "treasury",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "NativeRescued",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Paused",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "cycleId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "holder",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "PayoutSkipped",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "proposalId",
        "type": "bytes32"
      }
    ],
    "name": "RewardTokenUpdateCancelled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "proposalId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "address[]",
        "name": "previousRewardTokens",
        "type": "address[]"
      },
      {
        "indexed": false,
        "internalType": "address[]",
        "name": "rewardTokens",
        "type": "address[]"
      }
    ],
    "name": "RewardTokenUpdateExecuted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "proposalId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "executableAt",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "address[]",
        "name": "rewardTokens",
        "type": "address[]"
      }
    ],
    "name": "RewardTokenUpdateProposed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "previousAdminRole",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "newAdminRole",
        "type": "bytes32"
      }
    ],
    "name": "RoleAdminChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "RoleGranted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "RoleRevoked",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Unpaused",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "ADMIN_TRANSFER_DELAY",
    "outputs": [
      {
        "internalType": "uint48",
        "name": "",
        "type": "uint48"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "DEFAULT_ADMIN_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "DISTRIBUTION_INTERVAL",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "OPERATOR_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PAUSER_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "REWARD_TOKEN_COUNT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "REWARD_TOKEN_UPDATE_DELAY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptDefaultAdminTransfer",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "activeCycleId",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newAdmin",
        "type": "address"
      }
    ],
    "name": "beginDefaultAdminTransfer",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "cancelCurrentCycle",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "cancelDefaultAdminTransfer",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "cancelRewardTokenUpdate",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "cycleId",
        "type": "bytes32"
      }
    ],
    "name": "cancelledCycleIds",
    "outputs": [
      {
        "internalType": "bool",
        "name": "cancelled",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint48",
        "name": "newDelay",
        "type": "uint48"
      }
    ],
    "name": "changeDefaultAdminDelay",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "cycleId",
        "type": "bytes32"
      }
    ],
    "name": "completedCycleIds",
    "outputs": [
      {
        "internalType": "bool",
        "name": "completed",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "cursor",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "cycleActive",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "defaultAdmin",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "defaultAdminDelay",
    "outputs": [
      {
        "internalType": "uint48",
        "name": "",
        "type": "uint48"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "defaultAdminDelayIncreaseWait",
    "outputs": [
      {
        "internalType": "uint48",
        "name": "",
        "type": "uint48"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "cycleId",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "expectedCursor",
        "type": "uint256"
      },
      {
        "internalType": "address[]",
        "name": "holders",
        "type": "address[]"
      }
    ],
    "name": "distributeBatch",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "eligible",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "index",
        "type": "uint256"
      }
    ],
    "name": "excludedAccountAt",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "excludedAccountCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "excludedFromRewards",
    "outputs": [
      {
        "internalType": "bool",
        "name": "excluded",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "executeRewardTokenUpdate",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "expectedHolderCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      }
    ],
    "name": "getRoleAdmin",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "grantRole",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "hasRole",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "isRewardToken",
    "outputs": [
      {
        "internalType": "bool",
        "name": "registered",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "lastProcessedHolder",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "maxDistributionBatchSize",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "minHolderBalance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "nextDistribution",
    "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": "paidHolderCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "paused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingDefaultAdmin",
    "outputs": [
      {
        "internalType": "address",
        "name": "newAdmin",
        "type": "address"
      },
      {
        "internalType": "uint48",
        "name": "schedule",
        "type": "uint48"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingDefaultAdminDelay",
    "outputs": [
      {
        "internalType": "uint48",
        "name": "newDelay",
        "type": "uint48"
      },
      {
        "internalType": "uint48",
        "name": "schedule",
        "type": "uint48"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "index",
        "type": "uint256"
      }
    ],
    "name": "pendingRewardTokenAt",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingRewardTokenUpdateExecutableAt",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingRewardTokenUpdateId",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "index",
        "type": "uint256"
      }
    ],
    "name": "potAt",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "processedEligibleBalance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "contract IERC20[]",
        "name": "rewardTokens_",
        "type": "address[]"
      }
    ],
    "name": "proposeRewardTokens",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "proposalId",
        "type": "bytes32"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "renounceRole",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "contract IERC20",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "rescueForeignToken",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "rescueNative",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "revokeRole",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "index",
        "type": "uint256"
      }
    ],
    "name": "rewardTokenAt",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "rewardTokenCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "rewardTokenUpdateNonce",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "rollbackDefaultAdminDelay",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "snapshotBlock",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "snapshotBlockHash",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "sourceToken",
    "outputs": [
      {
        "internalType": "contract IMemeFolioSnapshotToken",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "cycleId",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "snapshotBlock_",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "snapshotBlockHash_",
        "type": "bytes32"
      }
    ],
    "name": "startCycle",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes4",
        "name": "interfaceId",
        "type": "bytes4"
      }
    ],
    "name": "supportsInterface",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "unpause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "cycleId",
        "type": "bytes32"
      }
    ],
    "name": "usedCycleIds",
    "outputs": [
      {
        "internalType": "bool",
        "name": "used",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

import {
    AccessControlDefaultAdminRules
} from "@openzeppelin/contracts/access/extensions/AccessControlDefaultAdminRules.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";

interface IMemeFolioSnapshotToken is IERC20 {
    function minHolderBalance() external view returns (uint256);
    function getPastBalance(address account, uint256 blockNumber) external view returns (uint256);
    function getPastEligibleSupply(uint256 blockNumber) external view returns (uint256);
    function getPastEligibleHolderCount(uint256 blockNumber) external view returns (uint256);
}

/// @title MemeFolioDistributor
/// @notice INDEX-style, balance-proportional distribution for exactly five reward tokens.
/// @dev Historical balances and the threshold-eligible supply are read from the immutable source
///      token checkpoints. The operator can trigger a finalized snapshot block but cannot upload or
///      alter balances. Sorted holder batches are operator-only to prevent a third party from
///      poisoning the irreversible cursor with a valid but incomplete prefix.
contract MemeFolioDistributor is AccessControlDefaultAdminRules, Pausable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

    uint48 public constant ADMIN_TRANSFER_DELAY = 2 days;
    uint256 public constant DISTRIBUTION_INTERVAL = 1 hours;
    uint256 public constant REWARD_TOKEN_COUNT = 5;
    uint256 public constant REWARD_TOKEN_UPDATE_DELAY = 6 hours;

    IMemeFolioSnapshotToken public immutable sourceToken;
    address public immutable treasury;
    uint256 public immutable minHolderBalance;
    uint256 public immutable maxDistributionBatchSize;

    IERC20[] private _rewardTokens;
    IERC20[] private _pendingRewardTokens;
    address[] private _excludedAccounts;
    uint256[] private _pots;

    mapping(address token => bool registered) public isRewardToken;
    mapping(address account => bool excluded) public excludedFromRewards;
    mapping(bytes32 cycleId => bool used) public usedCycleIds;
    mapping(bytes32 cycleId => bool completed) public completedCycleIds;
    mapping(bytes32 cycleId => bool cancelled) public cancelledCycleIds;

    bytes32 public activeCycleId;
    bytes32 public pendingRewardTokenUpdateId;
    uint256 public pendingRewardTokenUpdateExecutableAt;
    uint256 public rewardTokenUpdateNonce;
    uint256 public snapshotBlock;
    bytes32 public snapshotBlockHash;
    uint256 public expectedHolderCount;
    uint256 public paidHolderCount;
    uint256 public eligible;
    uint256 public processedEligibleBalance;
    uint256 public cursor;
    uint256 public nextDistribution;
    address public lastProcessedHolder;
    bool public cycleActive;

    error ActiveCycleMismatch(bytes32 expected, bytes32 actual);
    error BatchTooLarge(uint256 supplied, uint256 maximum);
    error CycleAlreadyUsed(bytes32 cycleId);
    error CycleAlreadyPaid(uint256 holdersPaid);
    error CycleInProgress();
    error EmptyBatch();
    error EligibleBalanceExceeded(uint256 processed, uint256 expected);
    error EligibleBalanceMismatch(uint256 processed, uint256 expected);
    error ExcludedHolder(address holder);
    error HolderBelowMinimum(address holder, uint256 balance, uint256 minimum);
    error InvalidAddress();
    error InvalidBatchSize();
    error InvalidCursor(uint256 expected, uint256 actual);
    error InvalidHolderCount();
    error InvalidRewardTokenCount();
    error InvalidHolderOrder(address previous, address current);
    error InvalidSnapshotBlock(uint256 snapshotBlock, uint256 currentBlock);
    error NativeTransferFailed();
    error NoCycle();
    error NoDistributionPot();
    error NoEligibleHolders();
    error NoPendingRewardTokenUpdate();
    error ProtectedToken(address token);
    error RewardTokenUpdateNotReady(uint256 executableAt);
    error SnapshotMetadataMismatch();
    error TooEarly(uint256 nextAllowedTimestamp);

    event CycleStarted(
        bytes32 indexed cycleId,
        uint256 indexed snapshotBlock,
        bytes32 snapshotBlockHash,
        uint256 eligibleHolders,
        uint256 eligibleBalance
    );
    event BatchPaid(bytes32 indexed cycleId, uint256 fromIndex, uint256 toIndex);
    event PayoutSkipped(
        bytes32 indexed cycleId, address indexed token, address indexed holder, uint256 amount
    );
    event DistributionCompleted(
        bytes32 indexed cycleId, address indexed token, uint256 frozenPot, uint256 holders
    );
    event CycleCancelled(bytes32 indexed cycleId);
    event NativeRescued(address indexed treasury, uint256 amount);
    event ForeignTokenRescued(address indexed token, address indexed treasury, uint256 amount);
    event RewardTokenUpdateProposed(
        bytes32 indexed proposalId, uint256 executableAt, address[] rewardTokens
    );
    event RewardTokenUpdateCancelled(bytes32 indexed proposalId);
    event RewardTokenUpdateExecuted(
        bytes32 indexed proposalId, address[] previousRewardTokens, address[] rewardTokens
    );

    constructor(
        address admin,
        address operator,
        address treasury_,
        IMemeFolioSnapshotToken sourceToken_,
        IERC20[] memory rewardTokens_,
        uint256 maxDistributionBatchSize_,
        address[] memory excludedAccounts_
    ) AccessControlDefaultAdminRules(ADMIN_TRANSFER_DELAY, admin) {
        if (
            admin == address(0) || operator == address(0) || treasury_ == address(0)
                || address(sourceToken_) == address(0)
        ) revert InvalidAddress();
        if (rewardTokens_.length != REWARD_TOKEN_COUNT) revert InvalidRewardTokenCount();
        if (maxDistributionBatchSize_ == 0 || maxDistributionBatchSize_ > 100) {
            revert InvalidBatchSize();
        }

        sourceToken = sourceToken_;
        treasury = treasury_;
        minHolderBalance = sourceToken_.minHolderBalance();
        if (minHolderBalance == 0) revert InvalidAddress();
        maxDistributionBatchSize = maxDistributionBatchSize_;

        for (uint256 i; i < rewardTokens_.length; ++i) {
            IERC20 token = rewardTokens_[i];
            address tokenAddress = address(token);
            if (
                tokenAddress == address(0) || tokenAddress == address(sourceToken_)
                    || isRewardToken[tokenAddress]
            ) revert InvalidAddress();
            isRewardToken[tokenAddress] = true;
            _rewardTokens.push(token);
            _exclude(tokenAddress);
        }

        _exclude(admin);
        _exclude(operator);
        _exclude(treasury_);
        _exclude(address(sourceToken_));
        _exclude(address(this));
        for (uint256 i; i < excludedAccounts_.length; ++i) {
            _exclude(excludedAccounts_[i]);
        }

        _grantRole(OPERATOR_ROLE, operator);
        _grantRole(PAUSER_ROLE, admin);
    }

    receive() external payable { }

    function rewardTokenCount() external view returns (uint256) {
        return _rewardTokens.length;
    }

    function rewardTokenAt(uint256 index) external view returns (address) {
        return address(_rewardTokens[index]);
    }

    function pendingRewardTokenAt(uint256 index) external view returns (address) {
        if (pendingRewardTokenUpdateId == bytes32(0)) revert NoPendingRewardTokenUpdate();
        return address(_pendingRewardTokens[index]);
    }

    function potAt(uint256 index) external view returns (uint256) {
        return _pots[index];
    }

    function excludedAccountCount() external view returns (uint256) {
        return _excludedAccounts.length;
    }

    function excludedAccountAt(uint256 index) external view returns (address) {
        return _excludedAccounts[index];
    }

    /// @notice Announces a replacement five-token basket that can be activated after six hours.
    /// @dev Proposing does not pause or otherwise affect the currently active reward basket.
    function proposeRewardTokens(IERC20[] calldata rewardTokens_)
        external
        onlyRole(DEFAULT_ADMIN_ROLE)
        returns (bytes32 proposalId)
    {
        _validateProposedRewardTokens(rewardTokens_);

        bytes32 replacedProposalId = pendingRewardTokenUpdateId;
        if (replacedProposalId != bytes32(0)) {
            emit RewardTokenUpdateCancelled(replacedProposalId);
        }
        delete _pendingRewardTokens;
        address[] memory proposedAddresses = new address[](REWARD_TOKEN_COUNT);
        for (uint256 i; i < rewardTokens_.length; ++i) {
            IERC20 token = rewardTokens_[i];
            _pendingRewardTokens.push(token);
            proposedAddresses[i] = address(token);
        }

        uint256 nonce = ++rewardTokenUpdateNonce;
        proposalId = keccak256(abi.encode(block.chainid, address(this), nonce, proposedAddresses));
        uint256 executableAt = block.timestamp + REWARD_TOKEN_UPDATE_DELAY;
        pendingRewardTokenUpdateId = proposalId;
        pendingRewardTokenUpdateExecutableAt = executableAt;
        emit RewardTokenUpdateProposed(proposalId, executableAt, proposedAddresses);
    }

    /// @notice Cancels the currently announced basket replacement.
    function cancelRewardTokenUpdate() external onlyRole(DEFAULT_ADMIN_ROLE) {
        bytes32 proposalId = pendingRewardTokenUpdateId;
        if (proposalId == bytes32(0)) revert NoPendingRewardTokenUpdate();
        _clearPendingRewardTokenUpdate();
        emit RewardTokenUpdateCancelled(proposalId);
    }

    /// @notice Activates an announced basket after the public delay and between reward cycles.
    function executeRewardTokenUpdate()
        external
        onlyRole(DEFAULT_ADMIN_ROLE)
        whenPaused
        nonReentrant
    {
        bytes32 proposalId = pendingRewardTokenUpdateId;
        if (proposalId == bytes32(0)) revert NoPendingRewardTokenUpdate();
        if (cycleActive) revert CycleInProgress();
        uint256 executableAt = pendingRewardTokenUpdateExecutableAt;
        // Timestamp is the explicit public six-hour governance delay; small validator drift is immaterial.
        // forge-lint: disable-next-line(block-timestamp)
        if (block.timestamp < executableAt) revert RewardTokenUpdateNotReady(executableAt);

        address[] memory previousAddresses = new address[](REWARD_TOKEN_COUNT);
        address[] memory proposedAddresses = new address[](REWARD_TOKEN_COUNT);
        for (uint256 i; i < _rewardTokens.length; ++i) {
            address previous = address(_rewardTokens[i]);
            previousAddresses[i] = previous;
            isRewardToken[previous] = false;
        }

        delete _rewardTokens;
        for (uint256 i; i < _pendingRewardTokens.length; ++i) {
            IERC20 token = _pendingRewardTokens[i];
            address tokenAddress = address(token);
            _rewardTokens.push(token);
            proposedAddresses[i] = tokenAddress;
            isRewardToken[tokenAddress] = true;
            _exclude(tokenAddress);
        }
        delete _pots;
        _clearPendingRewardTokenUpdate();
        emit RewardTokenUpdateExecuted(proposalId, previousAddresses, proposedAddresses);
    }

    /// @notice Freezes the five reward pots against an immutable historical token snapshot.
    /// @dev The snapshot must be one of the 256 recent completed blocks so its hash can be verified
    ///      by the EVM. All balances and the denominator are then read from source-token checkpoints.
    function startCycle(bytes32 cycleId, uint256 snapshotBlock_, bytes32 snapshotBlockHash_)
        external
        onlyRole(OPERATOR_ROLE)
        whenNotPaused
        nonReentrant
    {
        if (cycleActive) revert CycleInProgress();
        // Timestamp is the intended hourly keeper gate; single-block drift is immaterial here.
        // forge-lint: disable-next-line(block-timestamp)
        if (block.timestamp < nextDistribution) revert TooEarly(nextDistribution);
        if (cycleId == bytes32(0) || snapshotBlockHash_ == bytes32(0)) {
            revert SnapshotMetadataMismatch();
        }
        if (
            snapshotBlock_ >= block.number || block.number - snapshotBlock_ > 256
                || blockhash(snapshotBlock_) != snapshotBlockHash_
        ) {
            revert InvalidSnapshotBlock(snapshotBlock_, block.number);
        }
        if (usedCycleIds[cycleId]) revert CycleAlreadyUsed(cycleId);

        uint256 holderCount = sourceToken.getPastEligibleHolderCount(snapshotBlock_);
        if (holderCount == 0) revert InvalidHolderCount();
        uint256 eligible_ = sourceToken.getPastEligibleSupply(snapshotBlock_);
        for (uint256 i; i < _excludedAccounts.length; ++i) {
            uint256 excludedBalance =
                sourceToken.getPastBalance(_excludedAccounts[i], snapshotBlock_);
            if (excludedBalance >= minHolderBalance) {
                eligible_ -= excludedBalance;
                --holderCount;
            }
        }
        if (eligible_ == 0) revert NoEligibleHolders();

        bool hasPot = false;
        delete _pots;
        for (uint256 i; i < _rewardTokens.length; ++i) {
            uint256 pot = _safeBalanceOf(_rewardTokens[i]);
            _pots.push(pot);
            if (pot != 0) hasPot = true;
        }
        if (!hasPot) revert NoDistributionPot();

        usedCycleIds[cycleId] = true;
        activeCycleId = cycleId;
        snapshotBlock = snapshotBlock_;
        snapshotBlockHash = snapshotBlockHash_;
        expectedHolderCount = holderCount;
        paidHolderCount = 0;
        eligible = eligible_;
        processedEligibleBalance = 0;
        cursor = 0;
        lastProcessedHolder = address(0);
        cycleActive = true;
        nextDistribution = block.timestamp + DISTRIBUTION_INTERVAL;
        emit CycleStarted(cycleId, snapshotBlock_, snapshotBlockHash_, holderCount, eligible_);
    }

    /// @notice Pays every reward token to the next bounded, strictly sorted holder page.
    /// @dev The historical balance is queried onchain for every holder. Amounts are
    ///      floor(pot * holderBalance / eligible). Dust and skipped transfers roll forward.
    function distributeBatch(bytes32 cycleId, uint256 expectedCursor, address[] calldata holders)
        external
        onlyRole(OPERATOR_ROLE)
        whenNotPaused
        nonReentrant
    {
        if (!cycleActive) revert NoCycle();
        if (cycleId != activeCycleId) revert ActiveCycleMismatch(activeCycleId, cycleId);
        if (expectedCursor != cursor) revert InvalidCursor(cursor, expectedCursor);
        uint256 count = holders.length;
        if (count == 0) revert EmptyBatch();
        if (count > maxDistributionBatchSize) {
            revert BatchTooLarge(count, maxDistributionBatchSize);
        }

        uint256 holderCount = expectedHolderCount;
        uint256 end = cursor + count;
        if (end > holderCount) revert InvalidHolderCount();
        uint256 eligible_ = eligible;

        address previousHolder = lastProcessedHolder;
        uint256 processedBalance = processedEligibleBalance;
        for (uint256 i; i < count; ++i) {
            address holder = holders[i];
            if (holder <= previousHolder) revert InvalidHolderOrder(previousHolder, holder);
            if (excludedFromRewards[holder]) revert ExcludedHolder(holder);
            uint256 balance = sourceToken.getPastBalance(holder, snapshotBlock);
            if (balance < minHolderBalance) {
                revert HolderBelowMinimum(holder, balance, minHolderBalance);
            }
            previousHolder = holder;
            processedBalance += balance;
            if (processedBalance > eligible_) {
                revert EligibleBalanceExceeded(processedBalance, eligible_);
            }
            _payHolder(cycleId, holder, balance, eligible_);
        }

        uint256 from = cursor;
        cursor = end;
        paidHolderCount = end;
        processedEligibleBalance = processedBalance;
        lastProcessedHolder = previousHolder;
        emit BatchPaid(cycleId, from, end);

        if (end == holderCount) {
            if (processedBalance != eligible_) {
                revert EligibleBalanceMismatch(processedBalance, eligible_);
            }
            cycleActive = false;
            completedCycleIds[cycleId] = true;
            activeCycleId = bytes32(0);
            uint256 rewardCount = _rewardTokens.length;
            for (uint256 i; i < rewardCount; ++i) {
                emit DistributionCompleted(
                    cycleId, address(_rewardTokens[i]), _pots[i], holderCount
                );
            }
        }
    }

    function pause() external onlyRole(PAUSER_ROLE) {
        _pause();
    }

    function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) {
        _unpause();
    }

    /// @notice Abandons a bad active cycle. Reward balances remain for the next cycle.
    function cancelCurrentCycle() external onlyRole(DEFAULT_ADMIN_ROLE) whenPaused nonReentrant {
        bytes32 cycleId = activeCycleId;
        if (cycleId == bytes32(0)) revert NoCycle();
        if (cursor != 0) revert CycleAlreadyPaid(cursor);
        cycleActive = false;
        activeCycleId = bytes32(0);
        cancelledCycleIds[cycleId] = true;
        emit CycleCancelled(cycleId);
    }

    /// @notice Recovers unrelated tokens only. Registered reward pots can never be rescued.
    function rescueForeignToken(IERC20 token, uint256 amount)
        external
        onlyRole(DEFAULT_ADMIN_ROLE)
        whenPaused
        nonReentrant
    {
        if (address(token) == address(0)) revert InvalidAddress();
        if (isRewardToken[address(token)]) revert ProtectedToken(address(token));
        token.safeTransfer(treasury, amount);
        emit ForeignTokenRescued(address(token), treasury, amount);
    }

    function rescueNative(uint256 amount)
        external
        onlyRole(DEFAULT_ADMIN_ROLE)
        whenPaused
        nonReentrant
    {
        (bool success,) = payable(treasury).call{ value: amount }("");
        if (!success) revert NativeTransferFailed();
        emit NativeRescued(treasury, amount);
    }

    function _exclude(address account) private {
        if (account == address(0)) revert InvalidAddress();
        if (excludedFromRewards[account]) return;
        excludedFromRewards[account] = true;
        _excludedAccounts.push(account);
    }

    function _validateProposedRewardTokens(IERC20[] calldata rewardTokens_) private view {
        if (rewardTokens_.length != REWARD_TOKEN_COUNT) revert InvalidRewardTokenCount();
        for (uint256 i; i < rewardTokens_.length; ++i) {
            address tokenAddress = address(rewardTokens_[i]);
            if (
                tokenAddress == address(0) || tokenAddress == address(sourceToken)
                    || tokenAddress == address(this) || tokenAddress.code.length == 0
            ) revert InvalidAddress();
            for (uint256 j; j < i; ++j) {
                if (address(rewardTokens_[j]) == tokenAddress) revert InvalidAddress();
            }
        }
    }

    function _clearPendingRewardTokenUpdate() private {
        delete _pendingRewardTokens;
        pendingRewardTokenUpdateId = bytes32(0);
        pendingRewardTokenUpdateExecutableAt = 0;
    }

    function _payHolder(bytes32 cycleId, address holder, uint256 balance, uint256 eligible_)
        private
    {
        for (uint256 tokenIndex; tokenIndex < _rewardTokens.length; ++tokenIndex) {
            uint256 amount = Math.mulDiv(_pots[tokenIndex], balance, eligible_);
            if (amount == 0) continue;
            IERC20 token = _rewardTokens[tokenIndex];
            if (!token.trySafeTransfer(holder, amount)) {
                emit PayoutSkipped(cycleId, address(token), holder, amount);
            }
        }
    }

    /// @dev A malformed/reverting balanceOf is treated as zero so one bad reward token cannot brick all five.
    function _safeBalanceOf(IERC20 token) private view returns (uint256 bal) {
        bytes memory payload = abi.encodeCall(IERC20.balanceOf, (address(this)));
        assembly ("memory-safe") {
            let success := staticcall(gas(), token, add(payload, 0x20), mload(payload), 0x00, 0x20)
            if and(success, iszero(lt(returndatasize(), 0x20))) { bal := mload(0x00) }
        }
    }
}
Chain explorer4773msChain node80ms