Trust minimized, off-chain conviction voting

How to implement a conviction voting system using verifiable, off-chain data on Ceramic.

Trust minimized, off-chain conviction voting

Conviction Voting (CV) is a governance mechanism that allows users to signal their preference on various issues by staking their tokens on proposals. A proposal passes if it has sufficient weight allocated to it. The weight of a proposal is calculated based on the amount of tokens that have been staked and the amount of time these tokens have been staked. The introduction to conviction voting by Common Stack is a great primer if you want to learn more about how CV works.

Implementations of CV so far have been developed on the EVM, which makes the results and outcomes completely trustless. While there are some deployments like 1Hive and Common Stack, high gas costs and the sub-optimal user experience of transferring tokens for every vote make this quite problematic. Fortunately conviction voting systems can be securely implemented using off-chain, verifiable data structures. Building CV systems off-chain, as opposed to on-chain, avoids the high gas costs and poor user experience that plagues current implementations while still achieving verifiability and participant trust.

This article will explain how you can use IDX and Ceramic Network as an underlying data structure to build a conviction voting dapp.

Intro to Ceramic and IDX

Ceramic is a decentralized protocol for verifiable, version-controlled content. Ceramic is used to create mutable data on IPFS with permanent identifiers, called StreamIDs. All updates are signed and then periodically anchored into the blockchain for time-stamping and consensus. You can think of a single Ceramic stream as its own ledger which contains all the information needed to reconstruct the state of the stream in a completely trustless manner. For more details refer to the Ceramic specification.

IDX is a decentralized identity protocol built on top of Ceramic which makes it easy to associate data to a user, discover data a user has created, and build user-centric applications. You can think of IDX as a decentralized user table. Each user has their own index which is a map from definitions to records. Definitions are globally-defined descriptions of data created by developers. Each user has their own individual record for each definition which stores their content. For more information have a look at the IDX developer documentation.

System overview

The proposed conviction voting system is based on an ERC20 token that is periodically snapshotted. This means that the token balances of all users are not updated continuously but instead on a set interval, e.g. twice a day. Additionally, users can easily change their voting preferences at any time.

There are three main system components:

  1. User-created content including: 1) proposal documents which contain information for a given proposal, and 2) conviction documents where users set their preferences among all live proposals. Both proposal and conviction documents are stored as Ceramic TileDocuments.
  2. A snapshot service that takes snapshots of the balances of all users of a particular ERC20 token, calculates the total conviction weight for all proposals from users' conviction documents, and saves them into a conviction state document
  3. An oracle system that resolves passed proposals on-chain when the conviction state document has sufficient conviction weight for any given proposal

User-created content

Proposal document

Any user can create a proposal document when they want to request funds for a project. This document could contain any amount of information, but most important to be included is: context the caip-19 asset id (ERC20 token address) that is used for the conviction voting, beneficiary the receiver of the funds if the proposal passes, amount the amount of funds to send if the proposal passes.

Example content
This example is a proposal to get some funding for a project. Other proposals might include executing a transaction with call data, setting a protocol parameter, or updating some organizational document.

{
  context: "eip155:1/erc20:0x6b175474...", // ERC20 adddress as CAIP-19
  title: "Develop an off-chain conviction voting system",
  currency: "ETH",
  amount: 200,
  beneficiary: "0xabcdef",
  description: "Fund team X to implement off-chain conviction voting using Ceramic & IDX.",
  url: "https://ceramic.network"
}

Proposal document schema in CIP-87

Convictions document

Each participant in the conviction voting system creates their own convictions document when they first interact with the conviction voting dapp. This document contains the convictions the user has assigned to various proposals as a fraction of their entire token holdings. Every time they change their preference the underlying Ceramic document gets updated, signed, and eventually anchored on-chain.

A conviction voting dapp would create a definition as defined in CIP-87 and each user would store a convictions document as a record in their IDX.

Example content
In this example we see a single user's convictions document. They have created proposal2 and they have allocated different fractions of their token holdings to proposal 1, 2, and 3.

{
  context: "eip155:1/erc20:0x6b175474...", // ERC20 adddress as CAIP-19
  proposals: ["<proposal2-DocID>"],
  convictions: [{
    proposal: "<proposal1-DocID>",
    allocation: 0.2,
  }, {
    proposal: "<proposal2-DocID>",
    allocation: 0.5,
  }, {
    proposal: "<proposal3-DocID>",
    allocation: 0.3,
  }]
}

Preference document schema in CIP-87

Snapshot service

The snapshot service performs the task of periodically taking a snapshot of the balances of all token holders of the particular ERC20 being used. Using this information together with individual users' conviction documents, it calculates the total conviction and writes it to a conviction state document. This service is not trusted since anyone could compute the same conviction state by simply grabbing the data from the blockchain. The service executes at a set time interval, for example twice a day.

Syncing data & performing calculations

Since we are not restricted to calculate the conviction score for all proposals on-chain we can simply use the discrete version of the conviction voting algorithm which can be found at 1Hive/conviction-voting-cadcad. On every discrete time interval the snapshot service grabs all token holders and their balances from the blockchain (e.g. using TheGraph). The address of each token holder is used to look up their individual IDX document which should contain their convictions document. The total conviction y_t+1 can now be calculated for each proposal using the formula in Figure 1.

Figure 1: How to calculate the total conviction for each proposal j. Source: https://github.com/1Hive/conviction-voting-cadcad/blob/master/algorithm_overview.ipynb

Once total conviction for each proposal is calculated, the snapshot service also calculates the trigger function for each proposal. Finally the service updates the conviction state document with the new state.

Conviction state document

The conviction state document stores the full state of the conviction voting system. It is created and updated by the snapshot service. It contains two main arrays as well as supply information:

  • participants - all token holders, their current token balance, and the latest CommitID of their convictions document at the time the snapshot was made
  • proposals - all active proposals and their total conviction which has been calculated by the snapshot service
  • supply - the total supply of the ERC20 token that is used for governance

Example content
In this example we see that there are two participants who have a token balance and have created their own convictions documents. We also see that there are two active proposals, both of which have not yet been triggered, and have different levels of total conviction.

{
  context: "eip155:1/erc20:0x6b175474...", // ERC20 adddress as CAIP-19
  supply: 9001,
  participants: [{
    account: "0x12345...",
    balance: 1337,
    convictions: "<convictions1-CommitID>"
  }, {
    account: "0xabcde...",
    balance: 42,
    convictions: "<convictions2-CommitID>"
  }],
  proposals: [{
    proposal: "<proposal1-DocID>",
    totalConviction: 234,
    triggered: false
  }, {
    proposal: "<proposal2-DocID>",
    totalConviction: 432,
    triggered: false
  }]
}

Conviction state document schema in CIP-87

Oracle system

Once we have the conviction state document it's trivial to check the trigger function for any particular proposal. When a proposal has sufficient weight an oracle service (e.g. Chainlink) can be triggered to submit the data of the passed proposal on-chain. This would send the requested funds to the beneficiary in the proposal. It's also possible to use conviction voting to set smart contract parameters or proposal ranking (not execution). In these cases we would calculate a dynamic average instead of using a trigger function.

Executing proposals on-chain

In order to execute the transaction on-chain we can use an oracle system to get the relevant state from the conviction state document on-chain if a proposal has been triggered. Ideally this would be a set of oracles that can independently verify the state of this document before it's committed on-chain. However, in many cases it might be sufficient to use a simpler oracle system such as Chainlink to simply fetch the state of the document from the Ceramic gateway. The main data that needs to make it on-chain is beneficiary and amount.

Notes on security

Snapshot trust

The main point of trust in this system is the snapshot service. While anyone can independently verify that it has performed the proper calculations, this might not always happen. When implementing this CV system in production there likely needs to be some additional checks and balances that enable the snapshot service to be replaced. Alternatively a system may choose to require multiple snapshot service providers that are operated by independent parties.

Proposal manipulation

The proposal document could be updated to request a larger amount of funds after it has already started getting a large amount of conviction. In order to mitigate this attack, the snapshot service might require users to reconfirm their conviction for the proposal, without losing the time staked, before it is triggered.

Convictions reorg attack

Since Ceramic TileDocuments currently rely on the "earliest anchor wins" conflict resolution method, a user could perform a data withholding attack where their conviction would be counted twice. The attack would involve the user first updating their preference document to proposal A and anchoring it, but withholding this data by not submitting it to the Ceramic network. Then they would set their preference doc to proposal B and publicly submit this to the network. Once proposal B has been passed, they would publish their data about proposal A which would invalidate their conviction on proposal B (because A was anchored earlier) and it would look as if they have staked their tokens on proposal A for a long period of time. This entire problem is solved by the fact that the snapshot service sets the CommitID of the user's preference doc in the snapshot document. So if a user cheats like this it would be simple to see that they have double voted and all of their votes would be invalidated from that point in time.

Alternative implementations

IDX can be used to store any type of user-related data such as verifications for social accounts (Twitter, Github, Discord, Discourse, ...), a BrightID score, or any other similar information. This flexibility makes it possible to easily experiment with quadratic voting where accounts that are less likely to be sybils can influence the vote more.

Using an oracle to trigger actions on-chain would be ideal, but as a minimum viable product it might be simpler to just use the off-chain voting system as a way to generate signal. A trusted multi-sig can then be used to execute on-chain actions, similar to how snapshot.page is used.

Get involved!

This article outlines a basic framework for how off-chain conviction voting systems can be built, but there are still many details that need to be solved. If you're interested in contributing to this work, reach out in the Ceramic Discord or just go ahead and build it 🦍:

Additional resources


Thanks to Jeff Emmett, Griff Green, METADREAMER, and the 3Box Labs team for gifting feedback πŸ™πŸ»