A Better Treasury System

After aggregating the historical votes, one thing that might be useful is to quantify the consistency of voting, which can be sort of thought of how many sub-arrays of votes of non-linearly increasing order there are:

// Returns an array of sub-arrays of consecutive numbers.
// ie [1, 2, 3, 5, 6, 8, 9]
// return [[1, 2, 3], [5, 6], [8, 9]]
export const consistency = (array) => {
  const sorted = asc(array);
  return sorted.reduce((r, n) => {
    const lastSubArray = r[r.length - 1];

    if (!lastSubArray || lastSubArray[lastSubArray.length - 1] !== n - 1) {
      r.push([]);
    }

    r[r.length - 1].push(n);

    return r;
  }, []);
};

// Given an array, return a new array with the last _threshold_ amount of items from a lastValue
//     For example given:
//         - the array [1, 3, 4, 6, 7, 8, 9, 11, 12],
//         - a last value of 15
//         - a threshold of 5
//     This will return a new array [11, 12]
export const lastValues = (array, lastValue, threshold) => {
  const sorted = asc(array);
  return sorted.filter((x) => {
    return lastValue - threshold < x;
  });
};

// The window of the last votes we want to check consistency for.
//      For example, if the last referendum was 143 and there's a window of 5,
//      we want to check votes [139, 140, 141, 142, 143]
const RECENT_WINDOW = 5;

const LAST_REFERENDUM_WEIGHT = 15;
const RECENT_REFERENDUM_WEIGHT = 5;
const BASE_REFERENDUM_WEIGHT = 2;

// Inputs:
//    votes: an array of all the votes of a validator
//    lastReferendum: the last (or current) on chain referendum
// Returns:
//    baseDemocracyScore: the base score, sans multipliers
//    totalConsistencyMultiplier: the multiplier for the consistency of all votes
//    lastConsistencyMultiplier: the mulitiplier for the consistency of the recent window (ie the last 5 votes)
//    totalDemocracyScore: the base score * multipliers
// Scoring:
//     consistency is quantified as the batches of consecutive votes.
//
//     if someone has the votes [0, 1, 2, 4, 5, 6, 8, 10, 13],
//         there are 5 separate streaks of consistency: [0, 1, 2], [4, 5, 6], [8], [10], [13]
//
//     ideally we want to reward people that have the fewest separate streaks of consistency
//       (ie we want them to have fewer, longer consecutive streams of votes)
//
//     these consistency streaks are used to determine two different multipliers:
//        total consistency: how consistent are they with all votes
//        last consistency: how consistent are they within the recent window of votes (ie the last 5)
//
//     the multiplier is calculated as: 1 + 1 / consistency_streaks
//          resulting the more separate steams of consistency, the lower the multiplier
//
//      the total score is calculated as (base_score * lastMultiplier * totalMultipler)
//
//      The total score is capped at 250, the last consistency multiplier at 2x and the total consistency multiplier at 1.5x
//
//  Desired Outcome:
//     We want to balance the fact that new people may not have the number of votes as people that have
//       been voting for a while, so ideally if people start voting on the recent referenda (and are consistent)
//       they can achieve a good score from the multipliers.
//     We want to still benefit people that have been voting on lots of things though, so the points
//       they get from those gradually decrease over time (but still add up)
export const scoreDemocracyVotes = (
  votes: number[],
  lastReferendum: number
) => {
  if ((votes && votes?.length == 0) || !lastReferendum) {
    return {
      baseDemocracyScore: 0,
      totalConsistencyMultiplier: 0,
      lastConsistencyMultiplier: 0,
      totalDemocracyScore: 0,
    };
  }
  // Make sure votes are in ascending order
  const sorted = asc(votes);

  // Calculate the base democracy score:
  //     - if the referendum is the last/current, add 15 points
  //     - if the referendum is one of the last 3 most recent referenda, add 5 points per vote
  //     - everything else add 2 points per vote
  let demScore = 0;
  for (const referendum of votes) {
    if (referendum == lastReferendum) {
      demScore += LAST_REFERENDUM_WEIGHT;
    } else if (lastReferendum - referendum <= 3) {
      demScore += RECENT_REFERENDUM_WEIGHT;
    } else {
      demScore += BASE_REFERENDUM_WEIGHT;
    }
  }

  // Get the consistency sub-arrays for all votes
  const totalConsistency = consistency(sorted);

  //
  const lastConsistency = consistency(
    lastValues(sorted, lastReferendum, RECENT_WINDOW)
  );

  // The consistency of all historical votes, capped at 1.5x
  const totalConsistencyMultiplier =
    totalConsistency?.length > 0
      ? Math.min(1 + 1 / totalConsistency.length, 1.5)
      : 1;

  // The consistency of only the last _threshold_ votes
  const lastConsistencyMultiplier =
    lastConsistency?.length > 0 ? 1 + (1 / lastConsistency.length) * 1.5 : 1;

  // Calculate the total score, capping it at 400 points
  const totalDemScore = Math.min(
    demScore * totalConsistencyMultiplier * lastConsistencyMultiplier,
    400
  );

  return {
    baseDemocracyScore: demScore || 0,
    totalConsistencyMultiplier: totalConsistencyMultiplier || 0,
    lastConsistencyMultiplier: lastConsistencyMultiplier || 0,
    totalDemocracyScore: totalDemScore || 0,
  };
};

Of which some of the functions can be found here.

Using both the # of histical votes + the consistency you would randomize the top say ~50 accounts and present them to the user in the modal to select which ones they might want to delegate to.

The next step of this would be selecting which tracks they would want to delegate to (all of paritcular ones), of which regardless you will need to do a big batch tx of the following:

conts tx = api.tx.convictionVoting.delegate(class, delegationAddress, conviction, balance)

So if they want to delegate to all ~15 tracks it would be a batch tx of 15 of those txs. You would then prompt the user to sign and submit the tx.

The banner / toast might also want to detect something like how long ago they may have delegated to someone, and if that is too old, it would suggest revising their delegation since it was done a while ago.

The biggest challenges here is in querying, storing, aggregating that information, and being able to serve it to the user in a way that coforms to a good product ux experience, which would likely involve making a dedicated backend, indexer, db, and serving that via apis.

Staking Workflow

Compared to the above workflow, integrating this into staking products is much simpler.

In the modal where they select the amount of validators, there would be an optional checkbox for “also delegate governance votes to this validator”. This should be unchecked by default, but with a tag next it it saying it’s highly recommended that they so do.

If they check it it would bring a next modal where they would select which governance tracks they want to delegate for (either all or particular ones).

Selecting this would include these txs:

conts tx = api.tx.convictionVoting.delegate(class, delegationAddress, conviction, balance)

Since selecting validators already has a few txs in it that are already a batch tx, this would just include these additional ones in there as well.

Integrating this into staking would require zero additional querying, indexing, or aggregating, making it both from an implemenation standpoint and product / ux standpoint much easier and quicker to implement.

Challenges and Considerations

Overall I think the hardest part of making any products in the substrate space is giving the right and appropriate kind of data to users in a simplified and aggregated fashion, and doing so in a way that is performant and snappy. The reason most people don’t like Polkadot.js is because of all the data that is presented to people, and the time it takes to query the chain. This can be solved by having good backends that index and aggregate it, and only serve the minimal viable information to people via apis.

If we want to encourage people to delegate to people in the network, we want to make sure that this aligns with goals of the network, which means there is some informed decision making, but the user is not bombarded with a bunch of additional information that they may not need to know.

Additionally, from a product perspective, we want users to be able to succeed in these tasks with the smallest amount of friction possible, meaning they don’t go through a long list of modals or tasks to do, or have to make a million specific choices.

It’s within our best interest to combine these decisions where possible (ie the staking workflow have a simple checkbox that also includes delegation), or to aggregate the governance info where possible in a governace app so the user doesn’t need to click through a million things or scroll through a million things to try to determine something.

Making good products that actually have an impact of getting engagement is really hard, but some of the above is some factors that I consider help towards this goal. I would be curious to hear others thoughts on what they think of this, and what other factors might be good to include.