MemeFolioThresholdDistributor
0x202fad2e905077bfa3d658455b287b85eeb58dc5
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
16,270 bytes
ABI entries
141
67 read · 16 write
License
none
Contract information
- Address
- 0x202fad2e905077bfa3d658455b287b85eeb58dc5
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x4d1480c607…e84adf3d12
- Creation tx
- 0x0718416c8c…35fb70b06a
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (67)
ADMIN_TRANSFER_DELAY() → uint48
COMMUNITY_TOKEN_COUNT() → uint256
CORE_TOKEN_COUNT() → uint256
DEFAULT_ADMIN_ROLE() → bytes32
DISTRIBUTION_INTERVAL() → uint256
MAX_REWARD_TOKEN_COUNT() → uint256
OPERATOR_ROLE() → bytes32
PAUSER_ROLE() → bytes32
activeCycleId() → bytes32
cancelledCycleIds(bytes32) → bool
communityController() → address
communityTokenAt(uint256) → address
completedCycleIds(bytes32) → bool
coreTokenAt(uint256) → address
cursor() → uint256
cycleActive() → bool
cyclePhase() → uint8
cycleRewardTokenAt(uint256) → address
cycleRewardTokenCount() → uint256
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
isCoreRewardToken(address) → bool
lastCompletedHolderCount() → uint256
lastProcessedHolder() → address
lastVerifiedHolder() → address
maxDistributionBatchSize() → uint256
minHolderBalance() → uint256
nextDistribution() → uint256
owner() → address
paidHolderCount() → uint256
paused() → bool
payoutHolderSetHash() → bytes32
pendingCommunityExecutableAt() → uint256
pendingCommunityExpiresAt() → uint256
pendingCommunityProposalId() → bytes32
pendingCommunityTokenAt(uint256) → address
pendingCoreExecutableAt() → uint256
pendingCoreProposalId() → bytes32
pendingCoreTokenAt(uint256) → address
pendingDefaultAdmin() → address, uint48
pendingDefaultAdminDelay() → uint48, uint48
potAt(uint256) → uint256
processedEligibleBalance() → uint256
processedSourceEligibleBalance() → uint256
rewardRegistry() → address
rewardTokenAt(uint256) → address
rewardTokenCount() → uint256
snapshotBlock() → uint256
snapshotBlockHash() → bytes32
sourceEligible() → uint256
sourceExpectedHolderCount() → uint256
sourceHolderMinimum() → uint256
sourceToken() → address
supportsInterface(bytes4) → bool
thresholdVerificationRequired() → bool
treasury() → address
usedCycleIds(bytes32) → bool
verificationCursor() → uint256
verifiedHolderSetHash() → bytes32
Events (18)
BatchPaidCycleCancelledCycleStartedDefaultAdminDelayChangeCanceledDefaultAdminDelayChangeScheduledDefaultAdminTransferCanceledDefaultAdminTransferScheduledDistributionCompletedForeignTokenRescuedNativeRescuedPausedPayoutSkippedRoleAdminChangedRoleGrantedRoleRevokedSnapshotVerifiedUnpausedVerificationBatchProcessed
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "admin",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "address",
"name": "treasury_",
"type": "address"
},
{
"internalType": "contract IMemeFolioThresholdSnapshotToken",
"name": "sourceToken_",
"type": "address"
},
{
"internalType": "contract IMemeFolioRewardRegistry",
"name": "rewardRegistry_",
"type": "address"
},
{
"internalType": "uint256",
"name": "minHolderBalance_",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "maxDistributionBatchSize_",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "excludedAccounts_",
"type": "address[]"
},
{
"internalType": "uint256",
"name": "initialDistributionTimestamp_",
"type": "uint256"
}
],
"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": [],
"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": [
{
"internalType": "bytes32",
"name": "processed",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "expected",
"type": "bytes32"
}
],
"name": "HolderSetHashMismatch",
"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": [
{
"internalType": "uint256",
"name": "supplied",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "currentTimestamp",
"type": "uint256"
}
],
"name": "InvalidInitialDistributionTimestamp",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "supplied",
"type": "uint256"
}
],
"name": "InvalidRewardTokenCount",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "snapshotBlock",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "currentBlock",
"type": "uint256"
}
],
"name": "InvalidSnapshotBlock",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "sourceMinimum",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "rewardMinimum",
"type": "uint256"
}
],
"name": "InvalidThreshold",
"type": "error"
},
{
"inputs": [],
"name": "NativeTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "NoCycle",
"type": "error"
},
{
"inputs": [],
"name": "NoDistributionPot",
"type": "error"
},
{
"inputs": [],
"name": "NoEligibleHolders",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "ProtectedToken",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"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"
},
{
"inputs": [],
"name": "VerificationAlreadyComplete",
"type": "error"
},
{
"inputs": [],
"name": "VerificationIncomplete",
"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": "sourceHolders",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "sourceEligibleBalance",
"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": "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": true,
"internalType": "bytes32",
"name": "cycleId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint256",
"name": "rewardHolders",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "rewardEligibleBalance",
"type": "uint256"
},
{
"indexed": false,
"internalType": "bytes32",
"name": "holderSetHash",
"type": "bytes32"
}
],
"name": "SnapshotVerified",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "Unpaused",
"type": "event"
},
{
"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": "VerificationBatchProcessed",
"type": "event"
},
{
"inputs": [],
"name": "ADMIN_TRANSFER_DELAY",
"outputs": [
{
"internalType": "uint48",
"name": "",
"type": "uint48"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "COMMUNITY_TOKEN_COUNT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "CORE_TOKEN_COUNT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"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": "MAX_REWARD_TOKEN_COUNT",
"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": "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": [
{
"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": [],
"name": "communityController",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "communityTokenAt",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "cycleId",
"type": "bytes32"
}
],
"name": "completedCycleIds",
"outputs": [
{
"internalType": "bool",
"name": "completed",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "coreTokenAt",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"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": "cyclePhase",
"outputs": [
{
"internalType": "enum MemeFolioThresholdDistributor.CyclePhase",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "cycleRewardTokenAt",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "cycleRewardTokenCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"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": "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": "isCoreRewardToken",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "lastCompletedHolderCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "lastProcessedHolder",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "lastVerifiedHolder",
"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": "payoutHolderSetHash",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingCommunityExecutableAt",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingCommunityExpiresAt",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingCommunityProposalId",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "pendingCommunityTokenAt",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingCoreExecutableAt",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingCoreProposalId",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "pendingCoreTokenAt",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"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": "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": [],
"name": "processedSourceEligibleBalance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"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": [],
"name": "rewardRegistry",
"outputs": [
{
"internalType": "contract IMemeFolioRewardRegistry",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"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": "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": "sourceEligible",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "sourceExpectedHolderCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "sourceHolderMinimum",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "sourceToken",
"outputs": [
{
"internalType": "contract IMemeFolioThresholdSnapshotToken",
"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": "thresholdVerificationRequired",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "pure",
"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"
},
{
"inputs": [],
"name": "verificationCursor",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "verifiedHolderSetHash",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "cycleId",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "expectedCursor",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "holders",
"type": "address[]"
}
],
"name": "verifyBatch",
"outputs": [],
"stateMutability": "nonpayable",
"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 { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
interface IMemeFolioThresholdSnapshotToken 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);
}
interface IMemeFolioRewardRegistry {
function rewardTokenCount() external view returns (uint256);
function rewardTokenAt(uint256 index) external view returns (address);
function coreTokenAt(uint256 index) external view returns (address);
function communityTokenAt(uint256 index) external view returns (address);
function isCoreRewardToken(address token) external view returns (bool);
function communityController() external view returns (address);
function pendingCoreProposalId() external view returns (bytes32);
function pendingCoreExecutableAt() external view returns (uint256);
function pendingCoreTokenAt(uint256 index) external view returns (address);
function pendingCommunityProposalId() external view returns (bytes32);
function pendingCommunityExecutableAt() external view returns (uint256);
function pendingCommunityExpiresAt() external view returns (uint256);
function pendingCommunityTokenAt(uint256 index) external view returns (address);
}
/// @title MemeFolioThresholdDistributor
/// @notice Preserves the token's complete historical-holder reconciliation while applying a higher
/// immutable reward threshold in a separate verification phase before any payout occurs.
/// @dev The existing governed distributor remains the reward-token registry so an in-flight voting
/// round and future community basket changes do not need to be migrated or restarted.
contract MemeFolioThresholdDistributor 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 = 15 minutes;
uint256 public constant CORE_TOKEN_COUNT = 5;
uint256 public constant COMMUNITY_TOKEN_COUNT = 3;
uint256 public constant MAX_REWARD_TOKEN_COUNT = 8;
enum CyclePhase {
IDLE,
VERIFYING,
PAYING
}
IMemeFolioThresholdSnapshotToken public immutable sourceToken;
IMemeFolioRewardRegistry public immutable rewardRegistry;
address public immutable treasury;
uint256 public immutable sourceHolderMinimum;
uint256 public immutable minHolderBalance;
uint256 public immutable maxDistributionBatchSize;
address[] private _excludedAccounts;
IERC20[] private _cycleRewardTokens;
uint256[] private _pots;
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;
CyclePhase public cyclePhase;
bytes32 public activeCycleId;
uint256 public snapshotBlock;
bytes32 public snapshotBlockHash;
uint256 public sourceExpectedHolderCount;
uint256 public sourceEligible;
uint256 public verificationCursor;
uint256 public processedSourceEligibleBalance;
address public lastVerifiedHolder;
uint256 public expectedHolderCount;
uint256 public paidHolderCount;
uint256 public eligible;
uint256 public processedEligibleBalance;
uint256 public cursor;
uint256 public nextDistribution;
address public lastProcessedHolder;
bytes32 public verifiedHolderSetHash;
bytes32 public payoutHolderSetHash;
uint256 public lastCompletedHolderCount;
bool public cycleActive;
error ActiveCycleMismatch(bytes32 expected, bytes32 actual);
error BatchTooLarge(uint256 supplied, uint256 maximum);
error CycleAlreadyPaid(uint256 holdersPaid);
error CycleAlreadyUsed(bytes32 cycleId);
error CycleInProgress();
error EmptyBatch();
error EligibleBalanceExceeded(uint256 processed, uint256 expected);
error EligibleBalanceMismatch(uint256 processed, uint256 expected);
error HolderBelowMinimum(address holder, uint256 balance, uint256 minimum);
error HolderSetHashMismatch(bytes32 processed, bytes32 expected);
error InvalidAddress();
error InvalidBatchSize();
error InvalidCursor(uint256 expected, uint256 actual);
error InvalidHolderCount();
error InvalidHolderOrder(address previous, address current);
error InvalidInitialDistributionTimestamp(uint256 supplied, uint256 currentTimestamp);
error InvalidRewardTokenCount(uint256 supplied);
error InvalidSnapshotBlock(uint256 snapshotBlock, uint256 currentBlock);
error InvalidThreshold(uint256 sourceMinimum, uint256 rewardMinimum);
error NativeTransferFailed();
error NoCycle();
error NoDistributionPot();
error NoEligibleHolders();
error ProtectedToken(address token);
error SnapshotMetadataMismatch();
error TooEarly(uint256 nextAllowedTimestamp);
error VerificationIncomplete();
error VerificationAlreadyComplete();
event CycleStarted(
bytes32 indexed cycleId,
uint256 indexed snapshotBlock,
bytes32 snapshotBlockHash,
uint256 sourceHolders,
uint256 sourceEligibleBalance
);
event VerificationBatchProcessed(bytes32 indexed cycleId, uint256 fromIndex, uint256 toIndex);
event SnapshotVerified(
bytes32 indexed cycleId,
uint256 rewardHolders,
uint256 rewardEligibleBalance,
bytes32 holderSetHash
);
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);
constructor(
address admin,
address operator,
address treasury_,
IMemeFolioThresholdSnapshotToken sourceToken_,
IMemeFolioRewardRegistry rewardRegistry_,
uint256 minHolderBalance_,
uint256 maxDistributionBatchSize_,
address[] memory excludedAccounts_,
uint256 initialDistributionTimestamp_
) AccessControlDefaultAdminRules(ADMIN_TRANSFER_DELAY, admin) {
if (
operator == address(0) || treasury_ == address(0) || address(sourceToken_) == address(0)
|| address(sourceToken_).code.length == 0 || address(rewardRegistry_) == address(0)
|| address(rewardRegistry_).code.length == 0
) revert InvalidAddress();
if (maxDistributionBatchSize_ == 0 || maxDistributionBatchSize_ > 100) {
revert InvalidBatchSize();
}
if (initialDistributionTimestamp_ != 0 && initialDistributionTimestamp_ < block.timestamp) {
revert InvalidInitialDistributionTimestamp(
initialDistributionTimestamp_, block.timestamp
);
}
uint256 sourceMinimum = sourceToken_.minHolderBalance();
if (sourceMinimum == 0 || minHolderBalance_ <= sourceMinimum) {
revert InvalidThreshold(sourceMinimum, minHolderBalance_);
}
sourceToken = sourceToken_;
rewardRegistry = rewardRegistry_;
treasury = treasury_;
sourceHolderMinimum = sourceMinimum;
minHolderBalance = minHolderBalance_;
maxDistributionBatchSize = maxDistributionBatchSize_;
nextDistribution = initialDistributionTimestamp_;
_exclude(admin);
_exclude(operator);
_exclude(treasury_);
_exclude(address(sourceToken_));
_exclude(address(rewardRegistry_));
_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 thresholdVerificationRequired() external pure returns (bool) {
return true;
}
function communityController() external view returns (address) {
return rewardRegistry.communityController();
}
function rewardTokenCount() external view returns (uint256) {
return rewardRegistry.rewardTokenCount();
}
function rewardTokenAt(uint256 index) external view returns (address) {
return rewardRegistry.rewardTokenAt(index);
}
function coreTokenAt(uint256 index) external view returns (address) {
return rewardRegistry.coreTokenAt(index);
}
function communityTokenAt(uint256 index) external view returns (address) {
return rewardRegistry.communityTokenAt(index);
}
function isCoreRewardToken(address token) external view returns (bool) {
return rewardRegistry.isCoreRewardToken(token);
}
function pendingCoreProposalId() external view returns (bytes32) {
return rewardRegistry.pendingCoreProposalId();
}
function pendingCoreExecutableAt() external view returns (uint256) {
return rewardRegistry.pendingCoreExecutableAt();
}
function pendingCoreTokenAt(uint256 index) external view returns (address) {
return rewardRegistry.pendingCoreTokenAt(index);
}
function pendingCommunityProposalId() external view returns (bytes32) {
return rewardRegistry.pendingCommunityProposalId();
}
function pendingCommunityExecutableAt() external view returns (uint256) {
return rewardRegistry.pendingCommunityExecutableAt();
}
function pendingCommunityExpiresAt() external view returns (uint256) {
return rewardRegistry.pendingCommunityExpiresAt();
}
function pendingCommunityTokenAt(uint256 index) external view returns (address) {
return rewardRegistry.pendingCommunityTokenAt(index);
}
function cycleRewardTokenCount() external view returns (uint256) {
return _cycleRewardTokens.length;
}
function cycleRewardTokenAt(uint256 index) external view returns (address) {
return address(_cycleRewardTokens[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];
}
function startCycle(bytes32 cycleId, uint256 snapshotBlock_, bytes32 snapshotBlockHash_)
external
onlyRole(OPERATOR_ROLE)
whenNotPaused
nonReentrant
{
if (cycleActive) revert CycleInProgress();
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 sourceEligible_ = sourceToken.getPastEligibleSupply(snapshotBlock_);
uint256 rewardCount = rewardRegistry.rewardTokenCount();
if (rewardCount != CORE_TOKEN_COUNT && rewardCount != MAX_REWARD_TOKEN_COUNT) {
revert InvalidRewardTokenCount(rewardCount);
}
delete _cycleRewardTokens;
delete _pots;
bool hasPot;
for (uint256 i; i < rewardCount; ++i) {
address token = rewardRegistry.rewardTokenAt(i);
_validateRewardToken(token, i);
IERC20 reward = IERC20(token);
_cycleRewardTokens.push(reward);
uint256 pot = _safeBalanceOf(reward);
_pots.push(pot);
if (pot != 0) hasPot = true;
}
if (!hasPot) revert NoDistributionPot();
for (uint256 i; i < _excludedAccounts.length; ++i) {
(holderCount, sourceEligible_) = _subtractExcluded(
_excludedAccounts[i], holderCount, sourceEligible_, snapshotBlock_
);
}
for (uint256 i; i < rewardCount; ++i) {
address token = address(_cycleRewardTokens[i]);
if (!excludedFromRewards[token]) {
(holderCount, sourceEligible_) =
_subtractExcluded(token, holderCount, sourceEligible_, snapshotBlock_);
}
}
if (holderCount == 0 || sourceEligible_ == 0) revert NoEligibleHolders();
usedCycleIds[cycleId] = true;
activeCycleId = cycleId;
snapshotBlock = snapshotBlock_;
snapshotBlockHash = snapshotBlockHash_;
sourceExpectedHolderCount = holderCount;
sourceEligible = sourceEligible_;
verificationCursor = 0;
processedSourceEligibleBalance = 0;
lastVerifiedHolder = address(0);
expectedHolderCount = 0;
paidHolderCount = 0;
eligible = 0;
processedEligibleBalance = 0;
cursor = 0;
lastProcessedHolder = address(0);
verifiedHolderSetHash = bytes32(0);
payoutHolderSetHash = bytes32(0);
cyclePhase = CyclePhase.VERIFYING;
cycleActive = true;
nextDistribution = block.timestamp + DISTRIBUTION_INTERVAL;
emit CycleStarted(cycleId, snapshotBlock_, snapshotBlockHash_, holderCount, sourceEligible_);
}
function verifyBatch(bytes32 cycleId, uint256 expectedCursor, address[] calldata holders)
external
onlyRole(OPERATOR_ROLE)
whenNotPaused
nonReentrant
{
if (!cycleActive) revert NoCycle();
if (cyclePhase != CyclePhase.VERIFYING) revert VerificationAlreadyComplete();
if (cycleId != activeCycleId) revert ActiveCycleMismatch(activeCycleId, cycleId);
if (expectedCursor != verificationCursor) {
revert InvalidCursor(verificationCursor, expectedCursor);
}
uint256 count = holders.length;
if (count == 0) revert EmptyBatch();
if (count > maxDistributionBatchSize) {
revert BatchTooLarge(count, maxDistributionBatchSize);
}
uint256 end = verificationCursor + count;
if (end > sourceExpectedHolderCount) revert InvalidHolderCount();
address previous = lastVerifiedHolder;
uint256 processedSource = processedSourceEligibleBalance;
uint256 rewardHolderCount = expectedHolderCount;
uint256 rewardEligibleBalance = eligible;
bytes32 holderSetHash = verifiedHolderSetHash;
for (uint256 i; i < count; ++i) {
address holder = holders[i];
if (holder <= previous) revert InvalidHolderOrder(previous, holder);
if (_isExcludedForCycle(holder)) revert InvalidAddress();
uint256 balance = sourceToken.getPastBalance(holder, snapshotBlock);
if (balance < sourceHolderMinimum) {
revert HolderBelowMinimum(holder, balance, sourceHolderMinimum);
}
previous = holder;
processedSource += balance;
if (processedSource > sourceEligible) {
revert EligibleBalanceExceeded(processedSource, sourceEligible);
}
if (balance >= minHolderBalance) {
++rewardHolderCount;
rewardEligibleBalance += balance;
holderSetHash = keccak256(abi.encode(holderSetHash, holder, balance));
}
}
uint256 from = verificationCursor;
verificationCursor = end;
processedSourceEligibleBalance = processedSource;
lastVerifiedHolder = previous;
expectedHolderCount = rewardHolderCount;
eligible = rewardEligibleBalance;
verifiedHolderSetHash = holderSetHash;
emit VerificationBatchProcessed(cycleId, from, end);
if (end == sourceExpectedHolderCount) {
if (processedSource != sourceEligible) {
revert EligibleBalanceMismatch(processedSource, sourceEligible);
}
if (rewardHolderCount == 0 || rewardEligibleBalance == 0) revert NoEligibleHolders();
cyclePhase = CyclePhase.PAYING;
emit SnapshotVerified(cycleId, rewardHolderCount, rewardEligibleBalance, holderSetHash);
}
}
function distributeBatch(bytes32 cycleId, uint256 expectedCursor, address[] calldata holders)
external
onlyRole(OPERATOR_ROLE)
whenNotPaused
nonReentrant
{
if (!cycleActive) revert NoCycle();
if (cyclePhase != CyclePhase.PAYING) revert VerificationIncomplete();
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 end = cursor + count;
if (end > expectedHolderCount) revert InvalidHolderCount();
address previous = lastProcessedHolder;
uint256 processed = processedEligibleBalance;
bytes32 holderSetHash = payoutHolderSetHash;
for (uint256 i; i < count; ++i) {
address holder = holders[i];
if (holder <= previous) revert InvalidHolderOrder(previous, holder);
if (_isExcludedForCycle(holder)) revert InvalidAddress();
uint256 balance = sourceToken.getPastBalance(holder, snapshotBlock);
if (balance < minHolderBalance) {
revert HolderBelowMinimum(holder, balance, minHolderBalance);
}
previous = holder;
processed += balance;
if (processed > eligible) revert EligibleBalanceExceeded(processed, eligible);
holderSetHash = keccak256(abi.encode(holderSetHash, holder, balance));
_payHolder(cycleId, holder, balance, eligible);
}
uint256 from = cursor;
cursor = end;
paidHolderCount = end;
processedEligibleBalance = processed;
lastProcessedHolder = previous;
payoutHolderSetHash = holderSetHash;
emit BatchPaid(cycleId, from, end);
if (end == expectedHolderCount) {
if (processed != eligible) revert EligibleBalanceMismatch(processed, eligible);
if (holderSetHash != verifiedHolderSetHash) {
revert HolderSetHashMismatch(holderSetHash, verifiedHolderSetHash);
}
cycleActive = false;
cyclePhase = CyclePhase.IDLE;
completedCycleIds[cycleId] = true;
activeCycleId = bytes32(0);
lastCompletedHolderCount = expectedHolderCount;
for (uint256 i; i < _pots.length; ++i) {
emit DistributionCompleted(
cycleId, address(_cycleRewardTokens[i]), _pots[i], expectedHolderCount
);
}
}
}
function pause() external onlyRole(PAUSER_ROLE) {
_pause();
}
function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) {
_unpause();
}
function cancelCurrentCycle() external onlyRole(DEFAULT_ADMIN_ROLE) whenPaused nonReentrant {
bytes32 cycleId = activeCycleId;
if (cycleId == bytes32(0)) revert NoCycle();
if (cyclePhase == CyclePhase.PAYING && cursor != 0) revert CycleAlreadyPaid(cursor);
cycleActive = false;
cyclePhase = CyclePhase.IDLE;
activeCycleId = bytes32(0);
cancelledCycleIds[cycleId] = true;
emit CycleCancelled(cycleId);
}
function rescueForeignToken(IERC20 token, uint256 amount)
external
onlyRole(DEFAULT_ADMIN_ROLE)
whenPaused
nonReentrant
{
address tokenAddress = address(token);
if (tokenAddress == address(0)) revert InvalidAddress();
if (_isProtectedRewardToken(tokenAddress)) revert ProtectedToken(tokenAddress);
token.safeTransfer(treasury, amount);
emit ForeignTokenRescued(tokenAddress, 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 _subtractExcluded(
address account,
uint256 holderCount,
uint256 sourceEligible_,
uint256 snapshotBlock_
) private view returns (uint256, uint256) {
uint256 balance = sourceToken.getPastBalance(account, snapshotBlock_);
if (balance >= sourceHolderMinimum) return (holderCount - 1, sourceEligible_ - balance);
return (holderCount, sourceEligible_);
}
function _validateRewardToken(address token, uint256 index) private view {
if (
token == address(0) || token == address(sourceToken) || token == address(this)
|| token.code.length == 0
) revert InvalidAddress();
for (uint256 i; i < index; ++i) {
if (address(_cycleRewardTokens[i]) == token) revert InvalidAddress();
}
}
function _isExcludedForCycle(address account) private view returns (bool) {
if (excludedFromRewards[account]) return true;
for (uint256 i; i < _cycleRewardTokens.length; ++i) {
if (address(_cycleRewardTokens[i]) == account) return true;
}
return false;
}
function _isProtectedRewardToken(address token) private view returns (bool) {
for (uint256 i; i < _cycleRewardTokens.length; ++i) {
if (address(_cycleRewardTokens[i]) == token) return true;
}
uint256 activeCount = rewardRegistry.rewardTokenCount();
for (uint256 i; i < activeCount; ++i) {
if (rewardRegistry.rewardTokenAt(i) == token) return true;
}
if (rewardRegistry.pendingCoreProposalId() != bytes32(0)) {
for (uint256 i; i < CORE_TOKEN_COUNT; ++i) {
if (rewardRegistry.pendingCoreTokenAt(i) == token) return true;
}
}
if (rewardRegistry.pendingCommunityProposalId() != bytes32(0)) {
for (uint256 i; i < COMMUNITY_TOKEN_COUNT; ++i) {
if (rewardRegistry.pendingCommunityTokenAt(i) == token) return true;
}
}
return false;
}
function _exclude(address account) private {
if (account == address(0)) revert InvalidAddress();
if (excludedFromRewards[account]) return;
excludedFromRewards[account] = true;
_excludedAccounts.push(account);
}
function _payHolder(bytes32 cycleId, address holder, uint256 balance, uint256 eligible_)
private
{
for (uint256 tokenIndex; tokenIndex < _pots.length; ++tokenIndex) {
uint256 amount = Math.mulDiv(_pots[tokenIndex], balance, eligible_);
if (amount == 0) continue;
IERC20 token = _cycleRewardTokens[tokenIndex];
if (!token.trySafeTransfer(holder, amount)) {
emit PayoutSkipped(cycleId, address(token), holder, amount);
}
}
}
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 explorer3104msChain node96ms