Voting Challenges
votingChallenges: require a weighted quorum of on-chain votes from named voters, with partial votes, reset after execution, and a timelock.
A voting challenge requires named voters to approve the transfer on-chain, each with a weight. It is the standard's multisig: N-of-M signers, weighted governance, or a timelocked vault.
Shape
A complete approvalCriteria with the votingChallenges array open. Folded lines are defaults.
{
"approvalAmounts": {
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"maxNumTransfers": {
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"overridesFromOutgoingApprovals": true,
"votingChallenges": [
{
"proposalId": "proposal-1",
"quorumThreshold": "50",
"voters": [
{
"address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"weight": "100"
},
{
"address": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"weight": "200"
},
{
"address": "bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf",
"weight": "50"
}
]
}
],
"userApprovalSettings": {
"userRoyalties": { "percentage": "0", "payoutAddress": "" }
}
}{
"merkleChallenges": [],
"predeterminedBalances": {
"manualBalances": [],
"incrementedBalances": {
"startBalances": [],
"incrementTokenIdsBy": "0",
"incrementOwnershipTimesBy": "0",
"durationFromTimestamp": "0",
"allowOverrideTimestamp": false,
"recurringOwnershipTimes": {
"startTime": "0",
"intervalLength": "0",
"chargePeriodLength": "0"
},
"allowOverrideWithAnyValidToken": false,
"allowAmountScaling": false,
"maxScalingMultiplier": "0"
},
"orderCalculationMethod": {
"useOverallNumTransfers": false,
"usePerToAddressNumTransfers": false,
"usePerFromAddressNumTransfers": false,
"usePerInitiatedByAddressNumTransfers": false,
"useMerkleChallengeLeafIndex": false,
"challengeTrackerId": ""
}
},
"approvalAmounts": {
"overallApprovalAmount": "0",
"perToAddressApprovalAmount": "0",
"perFromAddressApprovalAmount": "0",
"perInitiatedByAddressApprovalAmount": "0",
"amountTrackerId": "",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"maxNumTransfers": {
"overallMaxNumTransfers": "0",
"perToAddressMaxNumTransfers": "0",
"perFromAddressMaxNumTransfers": "0",
"perInitiatedByAddressMaxNumTransfers": "0",
"amountTrackerId": "",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"coinTransfers": [],
"requireToEqualsInitiatedBy": false,
"requireFromEqualsInitiatedBy": false,
"requireToDoesNotEqualInitiatedBy": false,
"requireFromDoesNotEqualInitiatedBy": false,
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": false,
"autoDeletionOptions": {
"afterOneUse": false,
"afterOverallMaxNumTransfers": false,
"allowCounterpartyPurge": false,
"allowPurgeIfExpired": false
},
"mustOwnTokens": [],
"dynamicStoreChallenges": [],
"ethSignatureChallenges": [],
"senderChecks": {
"mustBeEvmContract": false,
"mustNotBeEvmContract": false,
"mustBeLiquidityPool": false,
"mustNotBeLiquidityPool": false
},
"recipientChecks": {
"mustBeEvmContract": false,
"mustNotBeEvmContract": false,
"mustBeLiquidityPool": false,
"mustNotBeLiquidityPool": false
},
"initiatorChecks": {
"mustBeEvmContract": false,
"mustNotBeEvmContract": false,
"mustBeLiquidityPool": false,
"mustNotBeLiquidityPool": false
},
"altTimeChecks": {
"offlineHours": [],
"offlineDays": [],
"offlineMonths": [],
"offlineDaysOfMonth": [],
"offlineWeeksOfYear": [],
"timezoneOffsetMinutes": "0",
"timezoneOffsetNegative": false
},
"mustPrioritize": false,
"votingChallenges": [
{
"proposalId": "proposal-1",
"quorumThreshold": "50",
"voters": [
{
"address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"weight": "100"
},
{
"address": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"weight": "200"
},
{
"address": "bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf",
"weight": "50"
}
],
"uri": "",
"customData": "",
"resetAfterExecution": false,
"delayAfterQuorum": "0"
}
],
"allowBackedMinting": false,
"allowSpecialWrapping": false,
"evmQueryChallenges": [],
"userApprovalSettings": {
"allowedDenoms": [],
"disableUserCoinTransfers": false,
"userRoyalties": { "percentage": "0", "payoutAddress": "" }
}
}interface VotingChallenge {
proposalId: string;
quorumThreshold: string;
voters: Voter[];
uri?: string;
customData?: string;
resetAfterExecution?: boolean;
delayAfterQuorum?: string;
}
interface Voter {
address: string;
weight: string;
}| Field | Type | Description |
|---|---|---|
proposalId | string | Scopes the votes. A new ID starts a fresh tally. |
quorumThreshold | Uint | Percentage (0-100) of total possible weight that must vote yes |
voters | Voter[] | Voters and weights. Every weight must be above 0. |
resetAfterExecution | bool | Clear all votes after a transfer uses this challenge |
delayAfterQuorum | Uint (ms) | Minimum time between reaching quorum and executing. 0 means none. |
uri, customData | string | Explain to voters what they are approving |
Votes are cast with MsgCastVote:
| Field | Description |
|---|---|
proposalId | The challenge's proposal ID |
voter | The voter's address. Must be in voters. |
yesWeight | 0-100. Percent of the voter's weight allocated to yes; the rest is no. |
Ask your agent:
Add a transfer approval to collection 1 that needs 2 of 3 votes from alice, bob, and carol before any transfer executes.The MCP builder tools (add_approval) produce the objects on this page.
How It Works
- A voter casts a vote. The chain stores it under
collectionId-approverAddress-approvalLevel-approvalId-proposalId-voterAddress. Casting again overwrites. - On a transfer, the chain sums every voter's yes contribution:
weight * yesWeight / 100. A voter with no vote contributes 0. percentage = totalYes * 100 / totalPossibleWeight, wheretotalPossibleWeightis the sum of all weights, voted or not.- The challenge passes when
percentage >= quorumThreshold, and, ifdelayAfterQuorumis set, whendelayAfterQuorummilliseconds have passed since quorum was first reached.
Read votes with GetVote and GetVotes.
Partial Votes
yesWeight | Yes | No |
|---|---|---|
100 | 100% | 0% |
70 | 70% | 30% |
50 | 50% | 50% |
0 | 0% | 100% |
Worked Example
Voters alice (100), bob (200), carol (50). Total possible weight 350. Threshold 50%.
- alice votes 100% yes: 100
- bob votes 50% yes: 100
- carol does not vote: 0
- Total yes 200.
200 * 100 / 350 = 57%. 57 >= 50, so the challenge passes.
Abstaining voters count as no. Set thresholds with expected participation in mind.
Reset After Execution
With resetAfterExecution: true, a successful transfer clears every vote on the challenge. The next transfer needs a fresh round of approval under the same proposalId. Use it for vaults where every withdrawal must be approved, or any recurring multisig. Without it, votes persist and every later matching transfer passes until someone changes their vote.
Delay After Quorum
When delayAfterQuorum is above 0, the chain records the time quorum is first reached (on the vote that crosses the threshold) and rejects transfers until that time plus the delay. If votes drop below quorum the timestamp clears and the clock restarts on the next crossing. Use it for timelocks: 86400000 (24 hours) gives stakeholders time to review or revoke before a large transfer executes.
Examples
Unanimous 3-of-3:
{
"votingChallenges": [
{
"proposalId": "multisig-1",
"quorumThreshold": "100",
"voters": [
{
"address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"weight": "1"
},
{
"address": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"weight": "1"
},
{
"address": "bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf",
"weight": "1"
}
]
}
]
}{
"votingChallenges": [
{
"proposalId": "multisig-1",
"quorumThreshold": "100",
"voters": [
{
"address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"weight": "1"
},
{
"address": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"weight": "1"
},
{
"address": "bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf",
"weight": "1"
}
],
"uri": "",
"customData": "",
"resetAfterExecution": false,
"delayAfterQuorum": "0"
}
]
}Weighted governance (alice as founder, bob as investor, carol as community), 66% of 1600 (1056) must vote yes:
{
"votingChallenges": [
{
"proposalId": "governance-1",
"quorumThreshold": "66",
"voters": [
{
"address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"weight": "1000"
},
{
"address": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"weight": "500"
},
{
"address": "bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf",
"weight": "100"
}
]
}
]
}{
"votingChallenges": [
{
"proposalId": "governance-1",
"quorumThreshold": "66",
"voters": [
{
"address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"weight": "1000"
},
{
"address": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"weight": "500"
},
{
"address": "bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf",
"weight": "100"
}
],
"uri": "",
"customData": "",
"resetAfterExecution": false,
"delayAfterQuorum": "0"
}
]
}Any one of three:
{
"votingChallenges": [
{
"proposalId": "flexible-1",
"quorumThreshold": "30",
"voters": [
{
"address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"weight": "100"
},
{
"address": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"weight": "100"
},
{
"address": "bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf",
"weight": "100"
}
]
}
]
}{
"votingChallenges": [
{
"proposalId": "flexible-1",
"quorumThreshold": "30",
"voters": [
{
"address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"weight": "100"
},
{
"address": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"weight": "100"
},
{
"address": "bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf",
"weight": "100"
}
],
"uri": "",
"customData": "",
"resetAfterExecution": false,
"delayAfterQuorum": "0"
}
]
}Failure Conditions
- The voter is not in
voters yesWeightis above 100- Yes percentage is below
quorumThreshold proposalIddoes not match a challenge on the approval- No voters, or a voter with zero weight
delayAfterQuorumhas not elapsed
Security
- Changing
proposalIdresets the tally. Use unique IDs per challenge so old votes cannot carry over. - Votes are on-chain, scoped to the collection, approver, level, approval, and proposal, and can only be changed by the voter.
- Weight distribution is the security model. One dominant weight is one point of failure; equal weights make a plain multisig.
- Put the proposal's purpose in
uriorcustomDataso voters know what they approve.