dao-governance-framework/forum-network/public/classes/validation-pool.js

153 lines
6.1 KiB
JavaScript
Raw Normal View History

import { CryptoUtil } from "./crypto.js";
2022-11-13 12:23:30 -06:00
import { Vote } from "./vote.js";
import { Voter } from "./voter.js";
import { Actor } from "./actor.js";
2022-11-12 16:20:42 -06:00
import params from "./params.js";
const ValidationPoolStates = Object.freeze({
2022-11-12 16:20:42 -06:00
OPEN: "OPEN",
CLOSED: "CLOSED",
});
2022-11-13 12:23:30 -06:00
export class ValidationPool extends Actor {
2022-11-17 08:30:06 -06:00
constructor(bench, authorId, {signingPublicKey, fee, duration, tokenLossRatio, contentiousDebate = false}, name, scene) {
2022-11-13 12:23:30 -06:00
super(name, scene);
2022-11-12 16:20:42 -06:00
// If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio()
if (!contentiousDebate && (tokenLossRatio < 0 || tokenLossRatio > 1 || [null, undefined].includes(tokenLossRatio))) {
throw new Error(`Token loss ratio must be in the range [0, 1]; got ${tokenLossRatio}`)
}
if (duration < params.voteDuration.min || (params.voteDuration.max && duration > params.voteDuration.max) || [null, undefined].includes(duration)) {
throw new Error(`Duration must be in the range [${params.voteDuration.min}, ${params.voteDuration.max ?? 'Inf'}]; got ${duration}`);
}
this.state = ValidationPoolStates.OPEN;
2022-11-12 16:20:42 -06:00
this.votes = new Map();
this.voters = new Map();
2022-11-13 12:23:30 -06:00
this.bench = bench;
2022-11-12 16:20:42 -06:00
this.id = CryptoUtil.randomUUID();
this.dateStart = new Date();
this.authorId = authorId;
this.fee = fee;
this.duration = duration;
this.tokenLossRatio = tokenLossRatio;
this.contentiousDebate = contentiousDebate;
this.tokens = {
for: fee * params.mintingRatio * params.stakeForWin,
against: fee * params.mintingRatio * (1 - params.stakeForWin),
2022-11-17 08:30:06 -06:00
// author: fee * params.mintingRatio * params.stakeForAuthor,
2022-11-12 16:20:42 -06:00
}
// TODO: Consider availability stakes
2022-11-17 08:30:06 -06:00
this.castVote(signingPublicKey, true, this.tokens.for, 0);
2022-11-12 16:20:42 -06:00
}
2022-11-13 12:23:30 -06:00
castVote(signingPublicKey, position, stake, lockingTime) {
const vote = new Vote(position, stake, lockingTime);
if (this.state === ValidationPoolStates.CLOSED) {
2022-11-13 12:23:30 -06:00
throw new Error(`Validation pool ${this.id} is closed`);
2022-11-12 16:20:42 -06:00
}
if (this.duration && new Date() - this.dateStart > this.duration) {
2022-11-13 12:23:30 -06:00
throw new Error(`Validation pool ${this.id} has expired, no new votes may be cast`);
2022-11-12 16:20:42 -06:00
}
this.votes.set(signingPublicKey, vote);
}
listVotes(position) {
return new Map(Array.from(this.votes.entries())
.filter(([_, vote]) => vote.position === position));
}
2022-11-13 12:23:30 -06:00
revealIdentity(signingPublicKey, reputationPublicKey) {
2022-11-12 16:20:42 -06:00
if (!this.votes.get(signingPublicKey)) {
throw new Error("Must vote before revealing identity");
}
2022-11-13 12:23:30 -06:00
const voter = this.bench.voters.get(reputationPublicKey) ?? new Voter(reputationPublicKey);
voter.addVoteRecord(this);
this.bench.voters.set(reputationPublicKey, voter);
2022-11-12 16:20:42 -06:00
this.voters.set(signingPublicKey, voter);
if (this.votes.size === this.voters.size) {
// All voters have revealed their reputation public keys
// Now we can evaluate winning conditions
this.state = ValidationPoolStates.CLOSED;
2022-11-12 16:20:42 -06:00
const result = this.evaluateWinningConditions();
this.applyTokenLocking();
this.distributeTokens(result);
2022-11-13 12:23:30 -06:00
this.deactivate();
2022-11-12 16:20:42 -06:00
}
}
getTokenLossRatio() {
if (!this.contentiousDebate) {
return this.tokenLossRatio;
}
const elapsed = new Date() - this.dateStart;
let stageDuration = params.contentiousDebate.period / 2;
let stage = 0;
let t = 0;
while (true) {
t += stageDuration;
stageDuration /= 2;
if (t > elapsed) {
break;
}
stage += 1;
if (stage >= params.contentiousDebate.stages - 1) {
break;
}
}
return stage / (params.contentiousDebate.stages - 1);
}
applyTokenLocking() {
// Before evaluating the winning conditions,
// we need to make sure any staked tokens are locked for the
// specified amounts of time.
for (const [signingPublicKey, {stake, lockingTime}] of this.votes.entries()) {
const voter = this.voters.get(signingPublicKey);
2022-11-13 12:23:30 -06:00
this.bench.reputations.lockTokens(voter.reputationPublicKey, stake, lockingTime);
2022-11-12 16:20:42 -06:00
// TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties.
}
}
evaluateWinningConditions() {
const getVoteValue = ({stake, lockingTime}) => stake * Math.pow(lockingTime, params.lockingTimeExponent);
const getTotalValue = (position) => Array.from(this.listVotes(position).values())
.map(getVoteValue).reduce((acc, cur) => acc += cur, 0);
const upvoteValue = getTotalValue(true);
const downvoteValue = getTotalValue(false);
2022-11-13 12:23:30 -06:00
const activeAvailableReputation = this.bench.getTotalActiveAvailableReputation();
2022-11-12 16:20:42 -06:00
const votePasses = upvoteValue >= params.winningRatio * downvoteValue;
const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
// TODO: If quorum is not met, what should happen?
if (!quorumMet) {
2022-11-13 12:23:30 -06:00
this.deactivate();
2022-11-17 08:30:06 -06:00
// console.log("Quorum is not met", {upvoteValue, downvoteValue, activeAvailableReputation});
2022-11-12 16:20:42 -06:00
throw new Error("Quorum is not met");
}
return votePasses && quorumMet;
}
distributeTokens(result) {
// Reward the author
// TODO: If the vote fails, distribute tokens.author among winning voters
2022-11-17 08:30:06 -06:00
if (result === true) {
// console.log("awarding to author", {id: this.authorId, tokens: this.tokens.for});
this.bench.reputations.addTokens(this.authorId, this.tokens.for);
// Reward the vote winners, in proportion to their stakes
const tokensForWinners = this.tokens.against;
const winningVotes = this.listVotes(result);
const totalStakes = Array.from(winningVotes.values())
.map(({stake}) => stake).reduce((acc, cur) => acc += cur, 0);
if (!totalStakes) {
return;
}
for (const [signingPublicKey, {stake}] of winningVotes.entries()) {
const {reputationPublicKey} = this.voters.get(signingPublicKey);
const reward = tokensForWinners * stake / totalStakes;
// console.log("awarding to winning voter", {id: reputationPublicKey, tokens: reward, stake, totalStakes, tokensForWinners});
this.bench.reputations.addTokens(reputationPublicKey, reward);
}
2022-11-12 16:20:42 -06:00
}
}
}