MemeFolioDistributorV2
0xb288130e52d5f0125e7c03461bcd77f7d504f3bc
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
17,157 bytes
ABI entries
155
65 read · 21 write
License
none
Contract information
- Address
- 0xb288130e52d5f0125e7c03461bcd77f7d504f3bc
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x4d1480c607…e84adf3d12
- Creation tx
- 0xae3f4eccc3…e0d9c47897
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (65)
ADMIN_TRANSFER_DELAY() → uint48
COMMUNITY_TOKEN_COUNT() → uint256
COMMUNITY_TOKEN_UPDATE_DELAY() → uint256
COMMUNITY_TOKEN_UPDATE_EXPIRY() → uint256
CORE_TOKEN_COUNT() → uint256
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
communityBasketInitialized() → bool
communityController() → address
communityTokenAt(uint256) → address
communityUpdateNonce() → uint256
completedCycleIds(bytes32) → bool
coreTokenAt(uint256) → address
coreUpdateNonce() → uint256
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
isCoreRewardToken(address) → bool
isPendingCoreRewardToken(address) → bool
isPendingRewardToken(address) → bool
isProtectedRewardToken(address) → bool
isRewardToken(address) → bool
lastCompletedHolderCount() → uint256
lastProcessedHolder() → address
legacyDistributor() → address
maxDistributionBatchSize() → uint256
minHolderBalance() → uint256
nextDistribution() → uint256
owner() → address
paidHolderCount() → uint256
paused() → bool
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
rewardTokenAt(uint256) → address
rewardTokenCount() → uint256
snapshotBlock() → uint256
snapshotBlockHash() → bytes32
sourceToken() → address
supportsInterface(bytes4) → bool
treasury() → address
usedCycleIds(bytes32) → bool
Events (23)
BatchPaidCommunityTokenUpdateExecutedCommunityTokenUpdateExpiredCommunityTokenUpdateProposedCoreTokenUpdateCancelledCoreTokenUpdateExecutedCoreTokenUpdateProposedCycleCancelledCycleStartedDefaultAdminDelayChangeCanceledDefaultAdminDelayChangeScheduledDefaultAdminTransferCanceledDefaultAdminTransferScheduledDistributionCompletedForeignTokenRescuedNativeRescuedPausedPayoutSkippedRetiredRewardDustTransferredRoleAdminChangedRoleGrantedRoleRevokedUnpaused
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "admin",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "address",
"name": "treasury_",
"type": "address"
},
{
"internalType": "address",
"name": "communityController_",
"type": "address"
},
{
"internalType": "address",
"name": "legacyDistributor_",
"type": "address"
},
{
"internalType": "contract IMemeFolioSnapshotTokenV2",
"name": "sourceToken_",
"type": "address"
},
{
"internalType": "address[5]",
"name": "coreTokens_",
"type": "address[5]"
},
{
"internalType": "address[3]",
"name": "communityTokens_",
"type": "address[3]"
},
{
"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": "expiresAt",
"type": "uint256"
}
],
"name": "CommunityTokenUpdateNotExpired",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "expiresAt",
"type": "uint256"
}
],
"name": "CommunityTokenUpdatePastExpiry",
"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": [
{
"internalType": "uint256",
"name": "supplied",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "currentTimestamp",
"type": "uint256"
}
],
"name": "InvalidInitialDistributionTimestamp",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "InvalidRewardTokenBalance",
"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": "NoPendingCommunityUpdate",
"type": "error"
},
{
"inputs": [],
"name": "NoPendingCoreUpdate",
"type": "error"
},
{
"inputs": [],
"name": "PendingRewardTokenUpdate",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "ProtectedToken",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "maximum",
"type": "uint256"
}
],
"name": "RewardTokenBalanceTooHigh",
"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"
},
{
"inputs": [
{
"internalType": "address",
"name": "caller",
"type": "address"
}
],
"name": "UnauthorizedCommunityController",
"type": "error"
},
{
"inputs": [],
"name": "UnfinalizedCommunityRound",
"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": "proposalId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "address[3]",
"name": "previousTokens",
"type": "address[3]"
},
{
"indexed": false,
"internalType": "address[3]",
"name": "tokens",
"type": "address[3]"
}
],
"name": "CommunityTokenUpdateExecuted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "proposalId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint256",
"name": "expiredAt",
"type": "uint256"
}
],
"name": "CommunityTokenUpdateExpired",
"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[3]",
"name": "tokens",
"type": "address[3]"
}
],
"name": "CommunityTokenUpdateProposed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "proposalId",
"type": "bytes32"
}
],
"name": "CoreTokenUpdateCancelled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "proposalId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "address[5]",
"name": "previousTokens",
"type": "address[5]"
},
{
"indexed": false,
"internalType": "address[5]",
"name": "tokens",
"type": "address[5]"
}
],
"name": "CoreTokenUpdateExecuted",
"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[5]",
"name": "tokens",
"type": "address[5]"
}
],
"name": "CoreTokenUpdateProposed",
"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": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "treasury",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "RetiredRewardDustTransferred",
"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": "COMMUNITY_TOKEN_COUNT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "COMMUNITY_TOKEN_UPDATE_DELAY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "COMMUNITY_TOKEN_UPDATE_EXPIRY",
"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": "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": "cancelCoreTokenUpdate",
"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": "communityBasketInitialized",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"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": [],
"name": "communityUpdateNonce",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"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": "coreUpdateNonce",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"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": "executeCommunityTokenUpdate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "executeCoreTokenUpdate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "expectedHolderCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "expireCommunityTokenUpdate",
"outputs": [],
"stateMutability": "nonpayable",
"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": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "isPendingCoreRewardToken",
"outputs": [
{
"internalType": "bool",
"name": "pendingCore",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "isPendingRewardToken",
"outputs": [
{
"internalType": "bool",
"name": "pending",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "isProtectedRewardToken",
"outputs": [
{
"internalType": "bool",
"name": "protected_",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "isRewardToken",
"outputs": [
{
"internalType": "bool",
"name": "active",
"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": "legacyDistributor",
"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": "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": [
{
"internalType": "address[3]",
"name": "tokens",
"type": "address[3]"
}
],
"name": "proposeCommunityTokens",
"outputs": [
{
"internalType": "bytes32",
"name": "proposalId",
"type": "bytes32"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[5]",
"name": "tokens",
"type": "address[5]"
}
],
"name": "proposeCoreTokens",
"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": "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 IMemeFolioSnapshotTokenV2",
"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 IMemeFolioSnapshotTokenV2 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 IMemeFolioCommunityRoundState {
function hasUnfinalizedRound() external view returns (bool);
}
/// @title MemeFolioDistributorV2
/// @notice Snapshot-based proportional distributions across five core tokens, expanding to eight
/// only after holder voting initializes the three community slots.
/// @dev Core administration remains with the delayed admin. Community slots are proposed only by
/// the immutable voting controller after its six-hour public vote and are then permissionlessly
/// executable between reward cycles without a second duplicate delay.
contract MemeFolioDistributorV2 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 REWARD_TOKEN_COUNT = 8;
uint256 public constant REWARD_TOKEN_UPDATE_DELAY = 6 hours;
uint256 public constant COMMUNITY_TOKEN_UPDATE_DELAY = 0;
uint256 public constant COMMUNITY_TOKEN_UPDATE_EXPIRY = 7 days;
IMemeFolioSnapshotTokenV2 public immutable sourceToken;
address public immutable treasury;
address public immutable communityController;
address public immutable legacyDistributor;
uint256 public immutable minHolderBalance;
uint256 public immutable maxDistributionBatchSize;
IERC20[8] private _rewardTokens;
address[5] private _pendingCoreTokens;
address[3] private _pendingCommunityTokens;
address[] private _excludedAccounts;
uint256[] private _pots;
mapping(address token => bool active) public isRewardToken;
mapping(address token => bool core) private _isActiveCoreRewardToken;
mapping(address token => bool pendingCore) public isPendingCoreRewardToken;
mapping(address token => bool pending) public isPendingRewardToken;
mapping(address token => bool protected_) public isProtectedRewardToken;
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 pendingCoreProposalId;
bytes32 public pendingCommunityProposalId;
uint256 public pendingCoreExecutableAt;
uint256 public pendingCommunityExecutableAt;
uint256 public pendingCommunityExpiresAt;
uint256 public coreUpdateNonce;
uint256 public communityUpdateNonce;
uint256 public snapshotBlock;
bytes32 public snapshotBlockHash;
uint256 public expectedHolderCount;
uint256 public paidHolderCount;
uint256 public lastCompletedHolderCount;
uint256 public eligible;
uint256 public processedEligibleBalance;
uint256 public cursor;
uint256 public nextDistribution;
address public lastProcessedHolder;
bool public cycleActive;
bool public communityBasketInitialized;
error ActiveCycleMismatch(bytes32 expected, bytes32 actual);
error BatchTooLarge(uint256 supplied, uint256 maximum);
error CycleAlreadyUsed(bytes32 cycleId);
error CycleAlreadyPaid(uint256 holdersPaid);
error CycleInProgress();
error CommunityTokenUpdateNotExpired(uint256 expiresAt);
error CommunityTokenUpdatePastExpiry(uint256 expiresAt);
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 InvalidHolderOrder(address previous, address current);
error InvalidInitialDistributionTimestamp(uint256 supplied, uint256 currentTimestamp);
error InvalidRewardTokenBalance(address token);
error InvalidSnapshotBlock(uint256 snapshotBlock, uint256 currentBlock);
error NativeTransferFailed();
error NoCycle();
error NoDistributionPot();
error NoEligibleHolders();
error NoPendingCoreUpdate();
error NoPendingCommunityUpdate();
error PendingRewardTokenUpdate();
error ProtectedToken(address token);
error RewardTokenBalanceTooHigh(address token, uint256 balance, uint256 maximum);
error RewardTokenUpdateNotReady(uint256 executableAt);
error SnapshotMetadataMismatch();
error TooEarly(uint256 nextAllowedTimestamp);
error UnfinalizedCommunityRound();
error UnauthorizedCommunityController(address caller);
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 CoreTokenUpdateProposed(
bytes32 indexed proposalId, uint256 executableAt, address[5] tokens
);
event CoreTokenUpdateExecuted(
bytes32 indexed proposalId, address[5] previousTokens, address[5] tokens
);
event CoreTokenUpdateCancelled(bytes32 indexed proposalId);
event CommunityTokenUpdateProposed(
bytes32 indexed proposalId, uint256 executableAt, address[3] tokens
);
event CommunityTokenUpdateExecuted(
bytes32 indexed proposalId, address[3] previousTokens, address[3] tokens
);
event CommunityTokenUpdateExpired(bytes32 indexed proposalId, uint256 expiredAt);
event RetiredRewardDustTransferred(
address indexed token, address indexed treasury, uint256 amount
);
constructor(
address admin,
address operator,
address treasury_,
address communityController_,
address legacyDistributor_,
IMemeFolioSnapshotTokenV2 sourceToken_,
address[5] memory coreTokens_,
address[3] memory communityTokens_,
uint256 maxDistributionBatchSize_,
address[] memory excludedAccounts_,
uint256 initialDistributionTimestamp_
) AccessControlDefaultAdminRules(ADMIN_TRANSFER_DELAY, admin) {
if (
operator == address(0) || treasury_ == address(0) || communityController_ == address(0)
|| communityController_.code.length == 0 || legacyDistributor_ == address(0)
|| legacyDistributor_.code.length == 0 || address(sourceToken_) == address(0)
|| address(sourceToken_).code.length == 0
) revert InvalidAddress();
if (maxDistributionBatchSize_ == 0 || maxDistributionBatchSize_ > 100) {
revert InvalidBatchSize();
}
if (initialDistributionTimestamp_ != 0 && initialDistributionTimestamp_ < block.timestamp) {
revert InvalidInitialDistributionTimestamp(
initialDistributionTimestamp_, block.timestamp
);
}
sourceToken = sourceToken_;
treasury = treasury_;
communityController = communityController_;
legacyDistributor = legacyDistributor_;
minHolderBalance = sourceToken_.minHolderBalance();
if (minHolderBalance == 0) revert InvalidAddress();
maxDistributionBatchSize = maxDistributionBatchSize_;
nextDistribution = initialDistributionTimestamp_;
address[8] memory initialTokens;
for (uint256 i; i < CORE_TOKEN_COUNT; ++i) {
initialTokens[i] = coreTokens_[i];
}
bool hasInitialCommunityToken;
for (uint256 i; i < COMMUNITY_TOKEN_COUNT; ++i) {
initialTokens[CORE_TOKEN_COUNT + i] = communityTokens_[i];
if (communityTokens_[i] != address(0)) hasInitialCommunityToken = true;
}
uint256 initialRewardTokenCount =
hasInitialCommunityToken ? REWARD_TOKEN_COUNT : CORE_TOKEN_COUNT;
_validateBasket(initialTokens, initialRewardTokenCount);
communityBasketInitialized = hasInitialCommunityToken;
for (uint256 i; i < initialRewardTokenCount; ++i) {
address token = initialTokens[i];
_rewardTokens[i] = IERC20(token);
isRewardToken[token] = true;
isProtectedRewardToken[token] = true;
if (i < CORE_TOKEN_COUNT) _isActiveCoreRewardToken[token] = true;
_exclude(token);
}
_exclude(admin);
_exclude(operator);
_exclude(treasury_);
_exclude(address(sourceToken_));
_exclude(address(this));
_exclude(legacyDistributor_);
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 _activeRewardTokenCount();
}
function rewardTokenAt(uint256 index) external view returns (address) {
return address(_rewardTokens[index]);
}
function coreTokenAt(uint256 index) external view returns (address) {
return address(_rewardTokens[index]);
}
function isCoreRewardToken(address token) external view returns (bool) {
return _isActiveCoreRewardToken[token] || isPendingCoreRewardToken[token];
}
function communityTokenAt(uint256 index) external view returns (address) {
return address(_rewardTokens[CORE_TOKEN_COUNT + index]);
}
function pendingCoreTokenAt(uint256 index) external view returns (address) {
if (pendingCoreProposalId == bytes32(0)) revert NoPendingCoreUpdate();
return _pendingCoreTokens[index];
}
function pendingCommunityTokenAt(uint256 index) external view returns (address) {
if (pendingCommunityProposalId == bytes32(0)) revert NoPendingCommunityUpdate();
return _pendingCommunityTokens[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 proposeCommunityTokens(address[3] calldata tokens)
external
returns (bytes32 proposalId)
{
if (msg.sender != communityController) {
revert UnauthorizedCommunityController(msg.sender);
}
_prepareCommunityProposalQueue();
_validateCommunityBasket(tokens);
for (uint256 i; i < COMMUNITY_TOKEN_COUNT; ++i) {
_pendingCommunityTokens[i] = tokens[i];
isPendingRewardToken[tokens[i]] = true;
}
uint256 nonce = ++communityUpdateNonce;
proposalId = keccak256(abi.encode(block.chainid, address(this), "COMMUNITY", nonce, tokens));
uint256 executableAt = block.timestamp + COMMUNITY_TOKEN_UPDATE_DELAY;
uint256 expiresAt = executableAt + COMMUNITY_TOKEN_UPDATE_EXPIRY;
pendingCommunityProposalId = proposalId;
pendingCommunityExecutableAt = executableAt;
pendingCommunityExpiresAt = expiresAt;
emit CommunityTokenUpdateProposed(proposalId, executableAt, tokens);
}
function executeCommunityTokenUpdate() external nonReentrant {
bytes32 proposalId = pendingCommunityProposalId;
if (proposalId == bytes32(0)) revert NoPendingCommunityUpdate();
if (cycleActive) revert CycleInProgress();
uint256 executableAt = pendingCommunityExecutableAt;
// forge-lint: disable-next-line(block-timestamp)
if (block.timestamp < executableAt) revert RewardTokenUpdateNotReady(executableAt);
uint256 expiresAt = pendingCommunityExpiresAt;
// forge-lint: disable-next-line(block-timestamp)
if (block.timestamp >= expiresAt) revert CommunityTokenUpdatePastExpiry(expiresAt);
address[3] memory previousTokens;
address[8] memory nextBasket = _activeBasket();
for (uint256 i; i < COMMUNITY_TOKEN_COUNT; ++i) {
previousTokens[i] = address(_rewardTokens[CORE_TOKEN_COUNT + i]);
nextBasket[CORE_TOKEN_COUNT + i] = _pendingCommunityTokens[i];
}
_validateFullBasket(nextBasket);
_retireRemovedTokens(previousTokens, nextBasket);
for (uint256 i; i < COMMUNITY_TOKEN_COUNT; ++i) {
address token = _pendingCommunityTokens[i];
_rewardTokens[CORE_TOKEN_COUNT + i] = IERC20(token);
_activateRewardToken(token, false);
}
communityBasketInitialized = true;
_clearPendingCommunityUpdate();
delete _pots;
emit CommunityTokenUpdateExecuted(proposalId, previousTokens, _communityBasket());
}
function expireCommunityTokenUpdate() external {
bytes32 proposalId = pendingCommunityProposalId;
if (proposalId == bytes32(0)) revert NoPendingCommunityUpdate();
uint256 expiresAt = pendingCommunityExpiresAt;
// forge-lint: disable-next-line(block-timestamp)
if (block.timestamp < expiresAt) revert CommunityTokenUpdateNotExpired(expiresAt);
_clearPendingCommunityUpdate();
emit CommunityTokenUpdateExpired(proposalId, expiresAt);
}
function proposeCoreTokens(address[5] calldata tokens)
external
onlyRole(DEFAULT_ADMIN_ROLE)
returns (bytes32 proposalId)
{
if (IMemeFolioCommunityRoundState(communityController).hasUnfinalizedRound()) {
revert UnfinalizedCommunityRound();
}
_requireNoPendingUpdate();
_validateCoreBasket(tokens);
for (uint256 i; i < CORE_TOKEN_COUNT; ++i) {
_pendingCoreTokens[i] = tokens[i];
isPendingRewardToken[tokens[i]] = true;
isPendingCoreRewardToken[tokens[i]] = true;
}
uint256 nonce = ++coreUpdateNonce;
proposalId = keccak256(abi.encode(block.chainid, address(this), "CORE", nonce, tokens));
uint256 executableAt = block.timestamp + REWARD_TOKEN_UPDATE_DELAY;
pendingCoreProposalId = proposalId;
pendingCoreExecutableAt = executableAt;
emit CoreTokenUpdateProposed(proposalId, executableAt, tokens);
}
function cancelCoreTokenUpdate() external onlyRole(DEFAULT_ADMIN_ROLE) {
bytes32 proposalId = pendingCoreProposalId;
if (proposalId == bytes32(0)) revert NoPendingCoreUpdate();
_clearPendingCoreUpdate();
emit CoreTokenUpdateCancelled(proposalId);
}
function executeCoreTokenUpdate()
external
onlyRole(DEFAULT_ADMIN_ROLE)
whenPaused
nonReentrant
{
bytes32 proposalId = pendingCoreProposalId;
if (proposalId == bytes32(0)) revert NoPendingCoreUpdate();
if (cycleActive) revert CycleInProgress();
uint256 executableAt = pendingCoreExecutableAt;
// forge-lint: disable-next-line(block-timestamp)
if (block.timestamp < executableAt) revert RewardTokenUpdateNotReady(executableAt);
address[5] memory previousTokens;
address[8] memory nextBasket = _activeBasket();
for (uint256 i; i < CORE_TOKEN_COUNT; ++i) {
previousTokens[i] = address(_rewardTokens[i]);
nextBasket[i] = _pendingCoreTokens[i];
_isActiveCoreRewardToken[previousTokens[i]] = false;
}
_validateBasket(nextBasket, _activeRewardTokenCount());
_retireRemovedTokens(previousTokens, nextBasket);
for (uint256 i; i < CORE_TOKEN_COUNT; ++i) {
address token = _pendingCoreTokens[i];
_rewardTokens[i] = IERC20(token);
_activateRewardToken(token, true);
}
_clearPendingCoreUpdate();
delete _pots;
emit CoreTokenUpdateExecuted(proposalId, previousTokens, _coreBasket());
}
function startCycle(bytes32 cycleId, uint256 snapshotBlock_, bytes32 snapshotBlockHash_)
external
onlyRole(OPERATOR_ROLE)
whenNotPaused
nonReentrant
{
if (cycleActive) revert CycleInProgress();
// 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;
delete _pots;
uint256 rewardCount = _activeRewardTokenCount();
for (uint256 i; i < rewardCount; ++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_);
}
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);
lastCompletedHolderCount = holderCount;
for (uint256 i; i < _pots.length; ++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();
}
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);
}
function rescueForeignToken(IERC20 token, uint256 amount)
external
onlyRole(DEFAULT_ADMIN_ROLE)
whenPaused
nonReentrant
{
address tokenAddress = address(token);
if (tokenAddress == address(0)) revert InvalidAddress();
if (
isRewardToken[tokenAddress] || isPendingRewardToken[tokenAddress]
|| 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 _activateRewardToken(address token, bool core) private {
isRewardToken[token] = true;
_isActiveCoreRewardToken[token] = core;
isProtectedRewardToken[token] = true;
_exclude(token);
}
function _retireRemovedTokens(address[3] memory previousTokens, address[8] memory nextBasket)
private
{
for (uint256 i; i < COMMUNITY_TOKEN_COUNT; ++i) {
_retireIfRemoved(previousTokens[i], nextBasket);
}
}
function _retireRemovedTokens(address[5] memory previousTokens, address[8] memory nextBasket)
private
{
for (uint256 i; i < CORE_TOKEN_COUNT; ++i) {
_retireIfRemoved(previousTokens[i], nextBasket);
}
}
function _retireIfRemoved(address token, address[8] memory nextBasket) private {
if (token == address(0)) return;
for (uint256 i; i < REWARD_TOKEN_COUNT; ++i) {
if (nextBasket[i] == token) return;
}
uint256 balance = _strictBalanceOf(IERC20(token));
uint256 maximum = lastCompletedHolderCount;
if (balance > maximum) revert RewardTokenBalanceTooHigh(token, balance, maximum);
isRewardToken[token] = false;
_isActiveCoreRewardToken[token] = false;
if (balance != 0) IERC20(token).safeTransfer(treasury, balance);
emit RetiredRewardDustTransferred(token, treasury, balance);
}
function _validateCommunityBasket(address[3] calldata tokens) private view {
address[8] memory basket = _activeBasket();
for (uint256 i; i < COMMUNITY_TOKEN_COUNT; ++i) {
basket[CORE_TOKEN_COUNT + i] = tokens[i];
}
_validateFullBasket(basket);
}
function _validateCoreBasket(address[5] calldata tokens) private view {
address[8] memory basket = _activeBasket();
for (uint256 i; i < CORE_TOKEN_COUNT; ++i) {
basket[i] = tokens[i];
}
_validateBasket(basket, _activeRewardTokenCount());
}
function _validateFullBasket(address[8] memory basket) private view {
_validateBasket(basket, REWARD_TOKEN_COUNT);
}
function _validateBasket(address[8] memory basket, uint256 count) private view {
for (uint256 i; i < count; ++i) {
address token = basket[i];
if (
token == address(0) || token == address(sourceToken) || token == address(this)
|| token.code.length == 0
) revert InvalidAddress();
for (uint256 j; j < i; ++j) {
if (basket[j] == token) revert InvalidAddress();
}
}
}
function _requireNoPendingUpdate() private view {
if (pendingCoreProposalId != bytes32(0) || pendingCommunityProposalId != bytes32(0)) {
revert PendingRewardTokenUpdate();
}
}
/// @dev A successful vote has priority over a stale admin proposal. A previous community vote
/// remains protected for its full public execution window and is only cleared once expired.
function _prepareCommunityProposalQueue() private {
bytes32 communityProposalId = pendingCommunityProposalId;
if (communityProposalId != bytes32(0)) {
uint256 expiresAt = pendingCommunityExpiresAt;
// forge-lint: disable-next-line(block-timestamp)
if (block.timestamp < expiresAt) revert PendingRewardTokenUpdate();
_clearPendingCommunityUpdate();
emit CommunityTokenUpdateExpired(communityProposalId, expiresAt);
}
bytes32 coreProposalId = pendingCoreProposalId;
if (coreProposalId != bytes32(0)) {
_clearPendingCoreUpdate();
emit CoreTokenUpdateCancelled(coreProposalId);
}
}
function _clearPendingCoreUpdate() private {
for (uint256 i; i < CORE_TOKEN_COUNT; ++i) {
address token = _pendingCoreTokens[i];
isPendingRewardToken[token] = false;
isPendingCoreRewardToken[token] = false;
}
delete _pendingCoreTokens;
pendingCoreProposalId = bytes32(0);
pendingCoreExecutableAt = 0;
}
function _clearPendingCommunityUpdate() private {
for (uint256 i; i < COMMUNITY_TOKEN_COUNT; ++i) {
isPendingRewardToken[_pendingCommunityTokens[i]] = false;
}
delete _pendingCommunityTokens;
pendingCommunityProposalId = bytes32(0);
pendingCommunityExecutableAt = 0;
pendingCommunityExpiresAt = 0;
}
function _activeBasket() private view returns (address[8] memory basket) {
for (uint256 i; i < REWARD_TOKEN_COUNT; ++i) {
basket[i] = address(_rewardTokens[i]);
}
}
function _coreBasket() private view returns (address[5] memory basket) {
for (uint256 i; i < CORE_TOKEN_COUNT; ++i) {
basket[i] = address(_rewardTokens[i]);
}
}
function _communityBasket() private view returns (address[3] memory basket) {
for (uint256 i; i < COMMUNITY_TOKEN_COUNT; ++i) {
basket[i] = address(_rewardTokens[CORE_TOKEN_COUNT + i]);
}
}
function _activeRewardTokenCount() private view returns (uint256) {
return communityBasketInitialized ? REWARD_TOKEN_COUNT : CORE_TOKEN_COUNT;
}
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 = _rewardTokens[tokenIndex];
if (!token.trySafeTransfer(holder, amount)) {
emit PayoutSkipped(cycleId, address(token), holder, amount);
}
}
}
/// @dev A malformed balance getter is zero so one reward cannot stop unrelated payouts.
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) }
}
}
/// @dev Retirement is irreversible, so its accounting must not tolerate a bad balance getter.
function _strictBalanceOf(IERC20 token) private view returns (uint256 balance) {
(bool success, bytes memory result) =
address(token).staticcall(abi.encodeCall(IERC20.balanceOf, (address(this))));
if (!success || result.length != 32) revert InvalidRewardTokenBalance(address(token));
balance = abi.decode(result, (uint256));
}
}
Chain explorer4340msChain node96ms