This proposal uses plutus minting policies to create valid "ballots" that are sent alongside datum "votes" to a centralized smart contract "ballot box" in order to perform verifiable on-chain voting in NFT projects that do not have a governance token.
This proposal is intended to provide a standard mechanism for non-fungible token (NFT) projects to perform on-chain verifiable votes using only their NFT assets. There are several proposed solutions for governance that involve using either a service provider (e.g., Summon) with native assets or the issuance of proprietary native assets. However, there are several issues with these approaches:
This standard provides a simple solution for this by using the underlying NFT to mint a one-time "ballot" token that can be used only for a specific voting topic. Projects that adopt this standard will be able to integrate with web viewers that track projects' governance votes and will also receive the benefits of verifiable on-chain voting without requiring issuance of a new native token.
We anticipate some potential use cases:
WarningThis specification is not intended for for governance against fungible tokens that cannot be labeled individually.
The basic analogy here is that of a traditional state or federal vote. Imagine a citizen who has a state ID (e.g., Driver's License) and wants to vote, as well as a central voting authority that counts all the ballots.
This specification follows the same process, but using tokens:
NoteBecause of the efficient UTxO model Cardano employs, steps #1 through #4 occur in a single transaction.
Every holder that participates in the vote will have their project NFT in a wallet that can be spent from (either hardware or software, typically accessed viaCIP-30). To create a ballot, the voting authority will create a Plutus minting policy with a specific combination of:
typeBallotMintingPolicy= {referencePolicyId:MintingPolicyHash,// Reference policy ID of the original NFT projectpollsClose:Time,// Polls close (as a Unix timestamp in milliseconds)assetNameMapping:func(ByteArray) ->ByteArray// Some function (potentially identity) to map reference NFT name 1-for-1 to ballot NFT name};
This Plutus minting policy will perform the following checks:
For the voter, each vote they wish to cast will require creating a separate "ballot" NFT. In the process, their reference NFT never leaves the original wallet. SampleHelios language pseudocode (functions elided for space) is as follows:
funcmain(redeemer: Redeemer, ctx: ScriptContext)-> Bool {tx: Tx= ctx.tx;minted_policy: MintingPolicyHash= ctx.get_current_minting_policy_hash(); redeemer.switch {Mint=> {polls_are_still_open(tx.time_range)&&ballots_are_validly_minted(tx.minted, minted_policy, tx.outputs)&&assets_locked_in_ballot_box(tx, tx.minted) },// Burn code elided for space... }}
Note
ballots_are_validly_minted()
includes all required and custom checks (e.g., the holder has sent the reference NFT to themselves intx.outputs
) to validate newly minted ballots
To cast the vote, the user sends the ballot NFT just created to a "ballot box". Note that for reasons specified inthe "attacks" section below this needs to occur during the same transaction that the ballot was minted in.
The datum is a simple object representing the voter who cast the vote and the vote itself:
typeVoteDatum= {voter:PubKeyHash,vote:object};
Thevoter
element is extremely important in this datum so that we know who minted the ballot NFT and who we should return it to. At the end of the ballot counting process, this user will receive their ballot NFT back.
Note that we are trying to avoid being overly prescriptive here with the specific vote type as we want the only limitations on the vote type to be those imposed by Cardano. Further iterations of this standard should discuss the potential for how to implement ranked-choice voting (RCV) inside of thisvote
object, support multiple-choice vote selection, and more.
Essentially, the "ballot box" is a smart contract with arbitrary logic decided upon by the authorized vote counter. Some examples include:
voter
datum of each UTxOBecause the ballot creation and vote casting process has already occurred on-chain we want to provide the maximum flexibility in the protocol here so that each project can decide what is best for their own community. Helios code for the simple case enumerated as #1 above would be:
constEXPECTED_SIGNER:PubKeyHash= PubKeyHash::new(#0123456789abcdef)funcmain(ctx: ScriptContext)-> Bool { ctx.tx.is_signed_by(EXPECTED_SIGNER)}
Given the flexible nature of the"ballot box" smart contract enumerated above, we propose a simple algorithm for counting votes and returning the ballots to the user:
vote
object in the UTxOs datumJavascript-like pseudocode using theLucid library for the above algorithm would be as follows:
functioncountVotes(ballotPolicyId,ballotBox) {var votesByAsset= {};constvotes=await lucid.utxosAt(ballotBox);for (constvoteof votes) {constvoteResult= Data.toJson(Data.from(vote.datum));for (constunitin vote.assets) {if (!unit.startsWith(ballotPolicyId)) {continue; }constvoteCount=Number(vote.assets[unit]);// Should always be 1 votesByAsset[unit]= { voter: voteResult.voter, vote: voteResult.vote, count: voteCount } } }return votesByAsset;}
There is no requirement that the "ballot counter" redeem all "ballots" from the "ballot box" and send them back to the respective voters, but we anticipate that this is what will happen in practice. We encourage further open-sourced code versions that enforce this requirement at the smart contract level.
Even if the ballot NFT is returned to the user, this will leave users with ada locked alongside these newly created assets, which can impose a financial hardship for certain project users.
We can add burn-specific code to our Plutus minting policy so that ballot creation does not impose a major financial burden on users:
funcmain(redeemer: Redeemer, ctx: ScriptContext)-> Bool {tx: Tx= ctx.tx;minted_policy: MintingPolicyHash= ctx.get_current_minting_policy_hash(); redeemer.switch {// Minting code elided for space...Burn=> { tx.minted.get_policy(minted_policy).all((asset_id:ByteArray,amount:Int) -> Bool {if (amount > 0) {print(asset_id.show()+" asset ID was minted not burned (quantity "+ amount.show()+")");false } else {true } }) } }}
The Helios code above simply checks that during a burn (as indicated by the Plutus minting policy'sredeemer
), the user is not attempting to mint a positive number of any assets. With this code,any Cardano wallet can burnany ballot minted as part of this protocol. Why so permissive? We want to ensure that each vote is bringing the minimal costs possible to the user. In providing this native burning mechanism we can free up the minUTxO that had been locked with the ballot, and enable the user to potentially participate in more votes they might not have otherwise. In addition, users who really do not like the specific commemorative NFTs or projects that choose to skip the "commemorative" aspect of ballot creation now have an easy way to dispose of "junk" assets.
There are several existing open-source protocols (e.g.,VoteAire) that use metadata to record votes in Cardano transactions without requiring any additional minting or smart contracts. However, since the vote counting occurs off-chain by validating metadata the vote counter is the ultimate arbiter of what is a "valid" vote. In this specification, the validity of the vote is ensured in the Ballot creation process, so that any vote in the ballot box is guaranteed to be valid. We strongly believe that moving the entire process into flexible on-chain logic will improve the transparency of the voting process and meet the needs of the use cases discussed in"Motivation" and"Ballot Box".
There is a question as to whether we should enforce the requirement that votes be burned when they are counted by the vote counter. However, we do not want that to be a standard as many users of NFT communities have expressed an interest in receiving commemorative NFTs (similar to an "I Voted" sticker). Instead, we propose that the ballot Plutus minting policy be burn-able by anyone who holds the NFT in their wallet. This way, locked ada can be reclaimed if the user has no further use for the commemorative NFT (see an example of this in theImplementation).
Imagine a user who decides to create ballots for the current vote, but not actually cast the vote by sending it to the ballot box. According to checks #1 and #2 inthe Plutus Minting Policy, this would be possible. After the ballot was created, the user could sell the reference NFT and wait until just before the polls close to surreptitiously cast a vote over the wishes of the new project owner. Check #3 in the minting policy during the mint transaction itself prevents this attack.
A user could wait until their first vote casting transaction completes, then create more ballots and resubmit to the ballot box. The result would be the creation of more assets that count toward the ultimate vote. However, Cardano helps us here by identifying tokens based on the concatenation of policy ID and asset identifier. So long as the mapping function in thePlutus minting policy for ballots is idempotent, each subsequent time the user votes the policy will create an additional fungible token with the same asset identifier. Then, the ballot counter can ignore any prior votes based on each unique asset identifier to avoid duplicate votes (see"'Ballot Counter' -> Authorized Wallet").
A user could attempt to create multiple ballots of the same name for a given reference NFT. If the reference NFT is actually a fungible token and not an NFT, then our assumptions will have been broken and this is an unsupported use case. But if our assumption that this is an NFT project are correct, then simply checking that the quantity minted is equal to the quantity spent inside of the Plutus minting policy will prevent this. Theexample code attached does just that.
During the construction of the ballot NFTs we allow the user to specify their vote alongside avoter
field indicating where their "ballot" NFT should be returned to once the vote is fully counted. Unfortunately, this is not strictly checked inside the Plutus minting policy's code (largely due to CPU/memory constraints). So, we rely on the user to provide an accurate return address, which means that there is the potential for someone who has not actually voted to receive a commemorative NFT. This does not impact the protocol though, as the "ballot" NFT was legally minted, just returned to the incorrect location. That user actually received a gift, as they can now burn the ballot and receive some small amount of dust.
There are several potential disadvantages to this proposal that may be avoided by the use of a native token or other voting mechanism. We enumerate some here explicitly so projects can understand where this protocol may or may not be appropriate to use:
In no particular order, we recommend the following implementation details that do not impact the protocol, but may impact user experience:
Due to the nature of Plutus minting policies and smart contracts, which derive policy identifiers and payment addresses from the actual source code, once a vote has been started it cannot change versions or code implementations. However, because the mechanism we propose here is just a reference architecture, between votes projects are free to change either the "ballot" Plutus minting policy or the "ballot box" smart contract as they see fit. There are no prior CIPs with which to conform with for backward interoperability.
This CIP is licensed underCC-BY-4.0.