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
- Creator
- 0x4d1480c607…e84adf3d12
- 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