Eligibility Parameters
BoardConfig controls who is allowed to propose and support initiatives via two separate fields:
proposerRequirements— gates who can callproposeInitiative/proposeInitiativeWithLocksupporterRequirements— gates who can callsupportInitiative
Both use the same ParticipantRequirements struct, and both are immutable after deployment.
struct ParticipantRequirements {
address token; // Token used for eligibility checks
uint256 minBalance; // Minimum current balance required (in wei)
uint256 minHoldingDuration; // Minimum blocks tokens must be held (requires IVotes)
uint256 minLockAmount; // Minimum tokens that must be locked per proposal/support
}Proposer Requirement Modes
Eligibility mode is inferred from the field values — there is no explicit enum. The three modes are:
Mode 1 — No Requirement (Open)
Anyone can propose regardless of token balance.
// Set minBalance and minHoldingDuration to 0
proposerRequirements: IAuthorizer.ParticipantRequirements({
token: address(underlyingToken), // still required (cannot be address(0))
minBalance: 0,
minHoldingDuration: 0,
minLockAmount: 0
})When to use: Fully open boards where any address can submit initiatives. Permissionless governance experiments, public suggestion boards, etc.
Mode 2 — Minimum Balance
Proposer must hold at least minBalance tokens at proposal time.
// Set minBalance > 0, leave minHoldingDuration as 0
proposerRequirements: IAuthorizer.ParticipantRequirements({
token: address(governanceToken),
minBalance: 100_000e18, // e.g. 100,000 tokens
minHoldingDuration: 0,
minLockAmount: 0
})The contract checks IERC20(token).balanceOf(proposer) >= minBalance at the time of the call.
When to use: Boards where token-holding alignment is a lightweight signal, but historical snapshot proofs aren't required. Works with any ERC20 token.
Mode 3 — Minimum Balance Held for Duration
Proposer must have held at least minBalance tokens continuously for at least minHoldingDuration
blocks before the proposal.
// Set both minBalance > 0 and minHoldingDuration > 0
proposerRequirements: IAuthorizer.ParticipantRequirements({
token: address(governanceToken),
minBalance: 50_000e18, // must currently hold ≥ 50,000 tokens
minHoldingDuration: 7200, // must have held them for ≥ 7,200 blocks (~24h on Ethereum mainnet)
minLockAmount: 0
})The contract calls IVotes(token).getPastVotes(proposer, block.number - minHoldingDuration) and
checks that the historical balance is also ≥ minBalance.
⚠️ Requires a Governor-style token (IVotes / ERC20Votes)
minHoldingDurationonly works with tokens that implement OpenZeppelin'sIVotesinterface (e.g.ERC20Votes,ERC721Votes). These tokens record checkpoint snapshots on every transfer/delegation, enabling historical balance queries.Plain ERC20 tokens do not support this — the contract will revert with
Signals_TokenHasNoCheckpointSupportifgetPastVotesis called on a non-IVotes token. If you configureminHoldingDuration > 0, yourtokenaddress must be IVotes-compatible.
When to use: Higher-stakes boards where you want to prevent last-minute token accumulation to game proposal rights. Common in DAO governance to require "skin in the game" for a minimum period.
minLockAmount — Minimum Lock Per Proposal
minLockAmount can be combined with any of the above modes. When set, proposeInitiativeWithLock
requires the caller to lock at least minLockAmount tokens alongside their proposal. Calling
proposeInitiative (without a lock) when minLockAmount > 0 will still revert.
proposerRequirements: IAuthorizer.ParticipantRequirements({
token: address(underlyingToken),
minBalance: 50_000e18,
minHoldingDuration: 0,
minLockAmount: 10_000e18 // proposer must lock at least 10,000 tokens
})Constraint: minLockAmount must be ≤ minBalance.
Constraints Summary
| Field | Constraint |
|---|---|
token | Cannot be address(0) |
minHoldingDuration > 0 | Requires minBalance > 0 and an IVotes-compatible token |
minLockAmount | Must be ≤ minBalance |
Violating these at initialization will revert with Signals_InvalidArguments.
Error Reference
| Error | Cause |
|---|---|
Signals_InsufficientTokens | Current balance < minBalance |
Signals_InsufficientTokenDuration | Historical balance < minBalance at block.number - minHoldingDuration |
Signals_TokenHasNoCheckpointSupport | Token doesn't implement IVotes; getPastVotes reverted |
Signals_InsufficientLockAmount | Lock amount < minLockAmount |
Signals_InvalidArguments | Invalid configuration at initialization (e.g. minHoldingDuration > 0 with minBalance == 0) |
Reading Requirements On-Chain
// Check proposer requirements
IAuthorizer.ParticipantRequirements memory reqs = signals.getProposerRequirements();
// Check supporter requirements
IAuthorizer.ParticipantRequirements memory reqs = signals.getParticipantRequirements();
// Check whether a specific account can propose with a given lock amount
bool canPropose = signals.accountCanPropose(account, lockAmount);
// Check whether a specific account can support with a given lock amount
bool canSupport = signals.accountCanSupport(account, lockAmount);