MemeFolioCommunityVoting

0x96e791bfc70cd540b6b3be903699bf908bbc5fb7

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
9,297 bytes
ABI entries
83
34 read · 12 write
License
none

Contract information

Address
0x96e791bfc70cd540b6b3be903699bf908bbc5fb7
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x46b8d58c95…a41a785406

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (34)

ADMIN_TRANSFER_DELAY()uint48
DEFAULT_ADMIN_ROLE()bytes32
MAX_CANDIDATE_COUNT()uint256
MIN_CANDIDATE_COUNT()uint256
RESULT_LOCK_DURATION()uint256
ROUND_DURATION()uint256
ROUND_MANAGER_ROLE()bytes32
VOTING_MINIMUM()uint256
WINNER_COUNT()uint256
ballotOf(uint256, address)bool, uint256, uint8, uint8[3]
candidateAt(uint256, uint256)address
currentRoundId()uint256
defaultAdmin()address
defaultAdminDelay()uint48
defaultAdminDelayIncreaseWait()uint48
distributor()address
getRoleAdmin(bytes32)bytes32
hasRole(bytes32, address)bool
hasUnfinalizedRound()bool
nextRoundOpensAt()uint256
owner()address
pendingDefaultAdmin()address, uint48
pendingDefaultAdminDelay()uint48, uint48
roundCandidateCount(uint256)uint256
roundClosesAt(uint256)uint256
roundCount()uint256
roundFinalized(uint256)bool
roundOpensAt(uint256)uint256
roundSnapshotBlock(uint256)uint256
roundSuccessful(uint256)bool
roundWinners(uint256)address[3]
scoreAt(uint256, uint256)uint256
sourceToken()address
supportsInterface(bytes4)bool

Events (11)

BallotCastDefaultAdminDelayChangeCanceledDefaultAdminDelayChangeScheduledDefaultAdminTransferCanceledDefaultAdminTransferScheduledDistributorBoundRoleAdminChangedRoleGrantedRoleRevokedRoundFinalizedRoundOpened

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "admin",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "roundManager",
        "type": "address"
      },
      {
        "internalType": "contract IMemeFolioVotingToken",
        "name": "sourceToken_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AccessControlBadConfirmation",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint48",
        "name": "schedule",
        "type": "uint48"
      }
    ],
    "name": "AccessControlEnforcedDefaultAdminDelay",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AccessControlEnforcedDefaultAdminRules",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "defaultAdmin",
        "type": "address"
      }
    ],
    "name": "AccessControlInvalidDefaultAdmin",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "neededRole",
        "type": "bytes32"
      }
    ],
    "name": "AccessControlUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AlreadyVoted",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BelowVotingMinimum",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "CoreRewardCandidate",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DistributorAlreadyBound",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DistributorNotBound",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DuplicateCandidate",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DuplicateChoice",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidCandidate",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidCandidateCount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidCandidateIndex",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidChoiceCount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidDistributor",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidRound",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RoundAlreadyFinalized",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "nextRoundOpensAt",
        "type": "uint256"
      }
    ],
    "name": "RoundCooldownActive",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RoundInProgress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RoundStillOpen",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "bits",
        "type": "uint8"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "SafeCastOverflowedUintDowncast",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "VotingClosed",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "roundId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "voter",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "weight",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "choiceCount",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "candidate0",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "candidate1",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "candidate2",
        "type": "uint8"
      }
    ],
    "name": "BallotCast",
    "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": "address",
        "name": "distributor",
        "type": "address"
      }
    ],
    "name": "DistributorBound",
    "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": "uint256",
        "name": "roundId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "successful",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "address[3]",
        "name": "winners",
        "type": "address[3]"
      }
    ],
    "name": "RoundFinalized",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "roundId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "snapshotBlock",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "opensAt",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "closesAt",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "address[]",
        "name": "candidates",
        "type": "address[]"
      }
    ],
    "name": "RoundOpened",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "ADMIN_TRANSFER_DELAY",
    "outputs": [
      {
        "internalType": "uint48",
        "name": "",
        "type": "uint48"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "DEFAULT_ADMIN_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_CANDIDATE_COUNT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_CANDIDATE_COUNT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "RESULT_LOCK_DURATION",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ROUND_DURATION",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ROUND_MANAGER_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "VOTING_MINIMUM",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "WINNER_COUNT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptDefaultAdminTransfer",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "roundId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "voter",
        "type": "address"
      }
    ],
    "name": "ballotOf",
    "outputs": [
      {
        "internalType": "bool",
        "name": "voted",
        "type": "bool"
      },
      {
        "internalType": "uint256",
        "name": "weight",
        "type": "uint256"
      },
      {
        "internalType": "uint8",
        "name": "choiceCount",
        "type": "uint8"
      },
      {
        "internalType": "uint8[3]",
        "name": "choices",
        "type": "uint8[3]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newAdmin",
        "type": "address"
      }
    ],
    "name": "beginDefaultAdminTransfer",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "contract IMemeFolioCommunityDistributor",
        "name": "distributor_",
        "type": "address"
      }
    ],
    "name": "bindDistributor",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "cancelDefaultAdminTransfer",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "roundId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "index",
        "type": "uint256"
      }
    ],
    "name": "candidateAt",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "choiceCount",
        "type": "uint8"
      },
      {
        "internalType": "uint8",
        "name": "candidate0",
        "type": "uint8"
      },
      {
        "internalType": "uint8",
        "name": "candidate1",
        "type": "uint8"
      },
      {
        "internalType": "uint8",
        "name": "candidate2",
        "type": "uint8"
      }
    ],
    "name": "castVote",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint48",
        "name": "newDelay",
        "type": "uint48"
      }
    ],
    "name": "changeDefaultAdminDelay",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "currentRoundId",
    "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": [],
    "name": "distributor",
    "outputs": [
      {
        "internalType": "contract IMemeFolioCommunityDistributor",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "roundId",
        "type": "uint256"
      }
    ],
    "name": "finalizeRound",
    "outputs": [
      {
        "internalType": "address[3]",
        "name": "winners",
        "type": "address[3]"
      }
    ],
    "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": [],
    "name": "hasUnfinalizedRound",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "nextRoundOpensAt",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address[]",
        "name": "candidates",
        "type": "address[]"
      }
    ],
    "name": "openRound",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "roundId",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "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": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "renounceRole",
    "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": "rollbackDefaultAdminDelay",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "roundId",
        "type": "uint256"
      }
    ],
    "name": "roundCandidateCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "roundId",
        "type": "uint256"
      }
    ],
    "name": "roundClosesAt",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "roundCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "roundId",
        "type": "uint256"
      }
    ],
    "name": "roundFinalized",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "roundId",
        "type": "uint256"
      }
    ],
    "name": "roundOpensAt",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "roundId",
        "type": "uint256"
      }
    ],
    "name": "roundSnapshotBlock",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "roundId",
        "type": "uint256"
      }
    ],
    "name": "roundSuccessful",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "roundId",
        "type": "uint256"
      }
    ],
    "name": "roundWinners",
    "outputs": [
      {
        "internalType": "address[3]",
        "name": "",
        "type": "address[3]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "roundId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "index",
        "type": "uint256"
      }
    ],
    "name": "scoreAt",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "sourceToken",
    "outputs": [
      {
        "internalType": "contract IMemeFolioVotingToken",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes4",
        "name": "interfaceId",
        "type": "bytes4"
      }
    ],
    "name": "supportsInterface",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

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

import {
    AccessControlDefaultAdminRules
} from "@openzeppelin/contracts/access/extensions/AccessControlDefaultAdminRules.sol";

interface IMemeFolioVotingToken {
    function getPastBalance(address account, uint256 blockNumber) external view returns (uint256);
}

interface IMemeFolioCommunityDistributor {
    function sourceToken() external view returns (IMemeFolioVotingToken);
    function communityController() external view returns (address);
    function isCoreRewardToken(address token) external view returns (bool);
    function proposeCommunityTokens(address[3] calldata tokens) external;
}

/// @title MemeFolioCommunityVoting
/// @notice Historical-balance approval voting for three community reward-token slots.
/// @dev The contract never transfers or escrows mETF. Every ballot uses the voter's checkpointed
///      balance from the completed block immediately before the round opened.
contract MemeFolioCommunityVoting is AccessControlDefaultAdminRules {
    uint48 public constant ADMIN_TRANSFER_DELAY = 2 days;
    uint256 public constant ROUND_DURATION = 6 hours;
    uint256 public constant RESULT_LOCK_DURATION = 24 hours;
    uint256 public constant MIN_CANDIDATE_COUNT = 14;
    uint256 public constant MAX_CANDIDATE_COUNT = 20;
    uint256 public constant WINNER_COUNT = 3;
    uint256 public constant VOTING_MINIMUM = 200_000 ether;
    bytes32 public constant ROUND_MANAGER_ROLE = keccak256("ROUND_MANAGER_ROLE");

    struct Ballot {
        bool voted;
        uint256 weight;
        uint8 choiceCount;
        uint8[3] choices;
    }

    struct Round {
        uint256 snapshotBlock;
        uint256 opensAt;
        uint256 closesAt;
        uint8 candidateCount;
        bool finalized;
        bool successful;
        address[20] candidates;
        uint256[20] scores;
        address[3] winners;
        mapping(address voter => Ballot ballot) ballots;
    }

    IMemeFolioVotingToken public immutable sourceToken;
    IMemeFolioCommunityDistributor public distributor;

    uint256 public roundCount;
    uint256 public currentRoundId;

    mapping(uint256 roundId => Round round) private _rounds;

    error AlreadyVoted();
    error BelowVotingMinimum();
    error CoreRewardCandidate();
    error DistributorAlreadyBound();
    error DistributorNotBound();
    error DuplicateCandidate();
    error DuplicateChoice();
    error InvalidAddress();
    error InvalidCandidate();
    error InvalidCandidateCount();
    error InvalidCandidateIndex();
    error InvalidChoiceCount();
    error InvalidDistributor();
    error InvalidRound();
    error RoundAlreadyFinalized();
    error RoundCooldownActive(uint256 nextRoundOpensAt);
    error RoundInProgress();
    error RoundStillOpen();
    error VotingClosed();

    event DistributorBound(address indexed distributor);
    event RoundOpened(
        uint256 indexed roundId,
        uint256 indexed snapshotBlock,
        uint256 opensAt,
        uint256 closesAt,
        address[] candidates
    );
    event BallotCast(
        uint256 indexed roundId,
        address indexed voter,
        uint256 weight,
        uint8 choiceCount,
        uint8 candidate0,
        uint8 candidate1,
        uint8 candidate2
    );
    event RoundFinalized(uint256 indexed roundId, bool successful, address[3] winners);

    constructor(address admin, address roundManager, IMemeFolioVotingToken sourceToken_)
        AccessControlDefaultAdminRules(ADMIN_TRANSFER_DELAY, admin)
    {
        if (
            roundManager == address(0) || address(sourceToken_) == address(0)
                || address(sourceToken_).code.length == 0
        ) revert InvalidAddress();

        sourceToken = sourceToken_;
        _grantRole(ROUND_MANAGER_ROLE, roundManager);
    }

    function bindDistributor(IMemeFolioCommunityDistributor distributor_)
        external
        onlyRole(DEFAULT_ADMIN_ROLE)
    {
        if (address(distributor_) == address(0) || address(distributor_).code.length == 0) {
            revert InvalidDistributor();
        }
        if (address(distributor_.sourceToken()) != address(sourceToken)) {
            revert InvalidDistributor();
        }
        if (distributor_.communityController() != address(this)) revert InvalidDistributor();
        if (address(distributor) != address(0)) revert DistributorAlreadyBound();

        distributor = distributor_;
        emit DistributorBound(address(distributor_));
    }

    function hasUnfinalizedRound() external view returns (bool) {
        uint256 roundId = currentRoundId;
        return roundId != 0 && !_rounds[roundId].finalized;
    }

    function openRound(address[] calldata candidates)
        external
        onlyRole(ROUND_MANAGER_ROLE)
        returns (uint256 roundId)
    {
        IMemeFolioCommunityDistributor distributor_ = distributor;
        if (address(distributor_) == address(0)) revert DistributorNotBound();

        uint256 previousRoundId = currentRoundId;
        if (previousRoundId != 0 && !_rounds[previousRoundId].finalized) {
            revert RoundInProgress();
        }

        if (previousRoundId != 0) {
            uint256 nextOpen = _rounds[previousRoundId].closesAt + RESULT_LOCK_DURATION;
            if (block.timestamp < nextOpen) revert RoundCooldownActive(nextOpen);
        }

        uint256 candidateCount = candidates.length;
        if (candidateCount < MIN_CANDIDATE_COUNT || candidateCount > MAX_CANDIDATE_COUNT) {
            revert InvalidCandidateCount();
        }

        for (uint256 i; i < candidateCount; ++i) {
            address candidate = candidates[i];
            if (
                candidate == address(sourceToken) || candidate == address(0)
                    || candidate.code.length == 0
            ) revert InvalidCandidate();
            if (distributor_.isCoreRewardToken(candidate)) {
                revert CoreRewardCandidate();
            }
            for (uint256 j; j < i; ++j) {
                if (candidate == candidates[j]) revert DuplicateCandidate();
            }
        }

        roundId = ++roundCount;
        currentRoundId = roundId;
        Round storage round = _rounds[roundId];
        round.snapshotBlock = block.number - 1;
        round.opensAt = block.timestamp;
        round.closesAt = block.timestamp + ROUND_DURATION;
        round.candidateCount = uint8(candidateCount);
        for (uint256 i; i < candidateCount; ++i) {
            round.candidates[i] = candidates[i];
        }

        emit RoundOpened(roundId, round.snapshotBlock, round.opensAt, round.closesAt, candidates);
    }

    function castVote(uint8 choiceCount, uint8 candidate0, uint8 candidate1, uint8 candidate2)
        external
    {
        uint256 roundId = currentRoundId;
        if (roundId == 0) revert InvalidRound();
        Round storage round = _rounds[roundId];
        if (block.timestamp < round.opensAt || block.timestamp >= round.closesAt) {
            revert VotingClosed();
        }

        Ballot storage ballot = round.ballots[msg.sender];
        if (ballot.voted) revert AlreadyVoted();
        if (choiceCount == 0 || choiceCount > WINNER_COUNT) {
            revert InvalidChoiceCount();
        }

        uint8[3] memory choices = [candidate0, candidate1, candidate2];
        for (uint256 i; i < choiceCount; ++i) {
            uint8 candidateIndex = choices[i];
            if (candidateIndex >= round.candidateCount) {
                revert InvalidCandidateIndex();
            }
            for (uint256 j; j < i; ++j) {
                if (candidateIndex == choices[j]) revert DuplicateChoice();
            }
        }

        uint256 weight = sourceToken.getPastBalance(msg.sender, round.snapshotBlock);
        if (weight < VOTING_MINIMUM) revert BelowVotingMinimum();

        ballot.voted = true;
        ballot.weight = weight;
        ballot.choiceCount = choiceCount;
        ballot.choices = choices;

        for (uint256 i; i < choiceCount; ++i) {
            round.scores[choices[i]] += weight;
        }

        emit BallotCast(
            roundId, msg.sender, weight, choiceCount, candidate0, candidate1, candidate2
        );
    }

    function finalizeRound(uint256 roundId) external returns (address[3] memory winners) {
        if (roundId == 0 || roundId > roundCount) revert InvalidRound();
        Round storage round = _rounds[roundId];
        if (round.finalized) revert RoundAlreadyFinalized();
        if (block.timestamp < round.closesAt) revert RoundStillOpen();

        uint256[3] memory winningScores;
        for (uint256 i; i < round.candidateCount; ++i) {
            uint256 candidateScore = round.scores[i];
            if (candidateScore == 0) continue;
            address candidate = round.candidates[i];

            for (uint256 rank; rank < WINNER_COUNT; ++rank) {
                if (_ranksBefore(candidateScore, candidate, winningScores[rank], winners[rank])) {
                    for (uint256 shift = WINNER_COUNT - 1; shift > rank; --shift) {
                        winningScores[shift] = winningScores[shift - 1];
                        winners[shift] = winners[shift - 1];
                    }
                    winningScores[rank] = candidateScore;
                    winners[rank] = candidate;
                    break;
                }
            }
        }

        round.finalized = true;
        if (winningScores[WINNER_COUNT - 1] == 0) {
            emit RoundFinalized(roundId, false, round.winners);
            return round.winners;
        }

        round.successful = true;
        round.winners = winners;
        distributor.proposeCommunityTokens(winners);
        emit RoundFinalized(roundId, true, winners);
    }

    function candidateAt(uint256 roundId, uint256 index) external view returns (address) {
        _requireRound(roundId);
        if (index >= _rounds[roundId].candidateCount) revert InvalidCandidateIndex();
        return _rounds[roundId].candidates[index];
    }

    function scoreAt(uint256 roundId, uint256 index) external view returns (uint256) {
        _requireRound(roundId);
        if (index >= _rounds[roundId].candidateCount) revert InvalidCandidateIndex();
        return _rounds[roundId].scores[index];
    }

    function roundCandidateCount(uint256 roundId) external view returns (uint256) {
        _requireRound(roundId);
        return _rounds[roundId].candidateCount;
    }

    function nextRoundOpensAt() external view returns (uint256) {
        uint256 roundId = currentRoundId;
        if (roundId == 0) return 0;
        return _rounds[roundId].closesAt + RESULT_LOCK_DURATION;
    }

    function ballotOf(uint256 roundId, address voter)
        external
        view
        returns (bool voted, uint256 weight, uint8 choiceCount, uint8[3] memory choices)
    {
        _requireRound(roundId);
        Ballot storage ballot = _rounds[roundId].ballots[voter];
        return (ballot.voted, ballot.weight, ballot.choiceCount, ballot.choices);
    }

    function roundWinners(uint256 roundId) external view returns (address[3] memory) {
        _requireRound(roundId);
        return _rounds[roundId].winners;
    }

    function roundSnapshotBlock(uint256 roundId) external view returns (uint256) {
        _requireRound(roundId);
        return _rounds[roundId].snapshotBlock;
    }

    function roundOpensAt(uint256 roundId) external view returns (uint256) {
        _requireRound(roundId);
        return _rounds[roundId].opensAt;
    }

    function roundClosesAt(uint256 roundId) external view returns (uint256) {
        _requireRound(roundId);
        return _rounds[roundId].closesAt;
    }

    function roundFinalized(uint256 roundId) external view returns (bool) {
        _requireRound(roundId);
        return _rounds[roundId].finalized;
    }

    function roundSuccessful(uint256 roundId) external view returns (bool) {
        _requireRound(roundId);
        return _rounds[roundId].successful;
    }

    function _requireRound(uint256 roundId) private view {
        if (roundId == 0 || roundId > roundCount) revert InvalidRound();
    }

    function _ranksBefore(
        uint256 candidateScore,
        address candidate,
        uint256 rankedScore,
        address rankedCandidate
    ) private pure returns (bool) {
        return candidateScore > rankedScore
            || (candidateScore == rankedScore && candidate < rankedCandidate);
    }
}
Chain explorer3208msChain node87ms