@swaptoshi/governance-module
Version:
Klayr governance on-chain module
357 lines • 19.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Proposal = void 0;
const codec_1 = require("@klayr/codec");
const utils = require("@klayr/utils");
const decimal_js_1 = require("decimal.js");
const types_1 = require("../../types");
const proposal_1 = require("../proposal");
const base_1 = require("./base");
const utils_1 = require("../../utils");
const utils_2 = require("@swaptoshi/utils");
const proposal_created_1 = require("../../events/proposal_created");
const next_available_proposal_id_1 = require("../next_available_proposal_id");
const constants_1 = require("../../constants");
const schema_1 = require("../../schema");
const proposal_voted_1 = require("../../events/proposal_voted");
const proposal_set_attributes_1 = require("../../events/proposal_set_attributes");
const queue_1 = require("../queue");
const delegated_vote_1 = require("../delegated_vote");
const vote_changed_1 = require("../../events/vote_changed");
const casted_vote_1 = require("../casted_vote");
const boosted_account_1 = require("../boosted_account");
const vote_score_1 = require("../vote_score");
const proposal_voter_1 = require("../proposal_voter");
const payload_1 = require("../../utils/payload");
class Proposal extends base_1.BaseInstance {
constructor(stores, events, config, genesisConfig, moduleName, governableConfigRegistry, proposal, key) {
super(proposal_1.ProposalStore, stores, events, config, genesisConfig, moduleName, key);
this.title = '';
this.summary = '';
this.deposited = BigInt(0);
this.author = Buffer.alloc(0);
this.turnout = { for: BigInt(0), against: BigInt(0), abstain: BigInt(0) };
this.parameters = {
createdHeight: 0,
startHeight: 0,
quorumHeight: 0,
endHeight: 0,
executionHeight: 0,
maxBoostDuration: 0,
boostFactor: 1,
enableBoosting: false,
enableTurnoutBias: false,
quorumMode: types_1.QuorumMode.FOR_AGAINST_ABSTAIN,
quorumTreshold: '0',
};
this.voteSummary = { for: BigInt(0), against: BigInt(0), abstain: BigInt(0) };
this.status = 0;
this.actions = [];
this.attributes = [];
if (proposal)
Object.assign(this, utils.objects.cloneDeep(proposal));
this.nextAvailableIdStore = stores.get(next_available_proposal_id_1.NextAvailableProposalIdStore);
this.proposalQueueStore = stores.get(queue_1.ProposalQueueStore);
this.delegatedVoteStore = stores.get(delegated_vote_1.DelegatedVoteStore);
this.castedVoteStore = stores.get(casted_vote_1.CastedVoteStore);
this.boostedAccountStore = stores.get(boosted_account_1.BoostedAccountStore);
this.voteScoreStore = stores.get(vote_score_1.VoteScoreStore);
this.proposalVoterStore = stores.get(proposal_voter_1.ProposalVoterStore);
this.governableConfigRegistry = governableConfigRegistry;
}
toJSON() {
return utils.objects.cloneDeep(utils_2.object.serializer({
title: this.title,
summary: this.summary,
deposited: this.deposited,
author: this.author,
turnout: this.turnout,
parameters: this.parameters,
voteSummary: this.voteSummary,
status: this.status,
actions: this.actions,
attributes: this.attributes,
}));
}
toObject() {
return utils.objects.cloneDeep({
title: this.title,
summary: this.summary,
deposited: this.deposited,
author: this.author,
turnout: this.turnout,
parameters: this.parameters,
voteSummary: this.voteSummary,
status: this.status,
actions: this.actions,
attributes: this.attributes,
});
}
async verifyCreate(params) {
this._checkImmutableDependencies();
const config = await this.config.getConfig(this.immutableContext.context);
if (params.title.length >= constants_1.MAX_LENGTH_PROPOSAL_TITLE) {
throw new Error(`params.title should not be more than ${constants_1.MAX_LENGTH_PROPOSAL_TITLE} characters`);
}
if (params.summary.length >= constants_1.MAX_LENGTH_PROPOSAL_SUMMARY) {
throw new Error(`params.summary should not be more than ${constants_1.MAX_LENGTH_PROPOSAL_SUMMARY} characters`);
}
if (config.maxProposalActions >= 0 && params.actions.length > config.maxProposalActions) {
throw new Error(`exceeds max proposal actions of ${config.maxProposalActions}`);
}
const senderAvailableBalance = await this.tokenMethod.getAvailableBalance(this.immutableContext.context, this.immutableContext.senderAddress, this._getStakingTokenId());
const senderLockedBalance = await this.tokenMethod.getLockedAmount(this.immutableContext.context, this.immutableContext.senderAddress, this._getStakingTokenId(), constants_1.POS_MODULE_NAME);
const totalSupplyStore = await this.tokenMethod.getTotalSupply(this.immutableContext.context);
const index = totalSupplyStore.totalSupply.findIndex(supply => supply.tokenID.equals(this._getStakingTokenId()));
const proposalCreationMinBalance = (0, utils_1.parseBigintOrPercentage)(config.proposalCreationMinBalance, totalSupplyStore.totalSupply[index].totalSupply);
if (senderAvailableBalance + senderLockedBalance < proposalCreationMinBalance) {
throw new Error(`The sender's balance is below the required min balance of ${proposalCreationMinBalance.toString()} to create a proposal.`);
}
if (senderAvailableBalance < BigInt(config.proposalCreationDeposit)) {
throw new Error(`The sender doesn't have the required balance of ${config.proposalCreationDeposit} for the deposit.`);
}
for (const actions of params.actions) {
if (actions.type === 'config') {
const payload = codec_1.codec.decode(schema_1.configActionPayloadSchema, actions.payload);
const targetConfig = this.governableConfigRegistry.get(payload.moduleName);
const decodedValue = (0, payload_1.decodeConfigProposalValue)(targetConfig.schema, payload);
await targetConfig.dryRunSetConfigWithPath(this.immutableContext.context, payload.paramPath, decodedValue);
}
}
}
async create(params, verify = true) {
this._checkMutableDependencies();
if (verify)
await this.verifyCreate(params);
const { height } = this.mutableContext;
const config = await this.config.getConfig(this.mutableContext.context);
this.title = params.title;
this.summary = params.summary;
this.deposited = BigInt(config.proposalCreationDeposit);
this.author = this.mutableContext.senderAddress;
this.turnout = {
for: BigInt(0),
against: BigInt(0),
abstain: BigInt(0),
};
this.parameters = {
createdHeight: height,
startHeight: height + config.votingDelayDuration,
quorumHeight: height + config.quorumDuration,
endHeight: height + config.voteDuration,
executionHeight: height + config.executionDuration,
maxBoostDuration: config.maxBoostDuration,
boostFactor: config.boostFactor,
enableBoosting: config.enableBoosting,
enableTurnoutBias: config.enableTurnoutBias,
quorumMode: config.quorumMode,
quorumTreshold: config.quorumTreshold,
};
this.voteSummary = {
for: BigInt(0),
against: BigInt(0),
abstain: BigInt(0),
};
this.status = types_1.ProposalStatus.CREATED;
this.actions = params.actions;
this.attributes = params.attributes;
await this.tokenMethod.lock(this.mutableContext.context, this.mutableContext.senderAddress, this.moduleName, this._getStakingTokenId(), BigInt(config.proposalCreationDeposit));
const nextId = await this._getNextAvailableProposalId();
this.key = utils_2.bytes.numberToBytes(nextId.nextProposalId);
await this._registerQueue(nextId.nextProposalId);
await this._saveStore();
await this._increaseNextAvailableProposalId();
const events = this.events.get(proposal_created_1.ProposalCreatedEvent);
events.add(this.mutableContext.context, {
author: this.mutableContext.senderAddress,
proposalId: nextId.nextProposalId,
}, [this.mutableContext.senderAddress]);
}
async verifyVote(params) {
this._checkImmutableDependencies();
if (!(await this._isProposalExists(params.proposalId))) {
throw new Error(`proposal with id ${params.proposalId} doesn't exists`);
}
if (this.status !== types_1.ProposalStatus.ACTIVE) {
throw new Error(`proposal status is not active`);
}
if (![types_1.Votes.FOR, types_1.Votes.AGAINST, types_1.Votes.ABSTAIN].includes(params.decision)) {
throw new Error('invalid vote decision');
}
if (params.data.length >= constants_1.VOTE_DATA_MAX_LENGTH) {
throw new Error(`vote data exceed max length of ${constants_1.VOTE_DATA_MAX_LENGTH} characters`);
}
const senderDelegatedVoteState = await this.delegatedVoteStore.getOrDefault(this.immutableContext.context, this.immutableContext.senderAddress);
if (!senderDelegatedVoteState.outgoingDelegation.equals(Buffer.alloc(0))) {
throw new Error(`the sender is currently delegating their votes`);
}
}
async vote(params, verify = true) {
this._checkMutableDependencies();
if (verify)
await this.verifyVote(params);
const castedVote = await this.castedVoteStore.getOrDefault(this.mutableContext.context, this.mutableContext.senderAddress);
const proposalIndex = castedVote.activeVote.findIndex(vote => vote.proposalId === params.proposalId);
const baseScore = await this.voteScoreStore.getVoteScore(this.mutableContext.context, this.mutableContext.senderAddress);
if (proposalIndex !== -1) {
const boostedState = await this.boostedAccountStore.getOrDefault(this.mutableContext.context, this.mutableContext.senderAddress);
await this.subtractVote(baseScore, castedVote.activeVote[proposalIndex].decision, boostedState.targetHeight);
await this.addVote(baseScore, params.decision, boostedState.targetHeight);
const events = this.events.get(vote_changed_1.VoteChangedEvent);
events.add(this.mutableContext.context, {
proposalId: params.proposalId,
voterAddress: this.mutableContext.senderAddress,
oldDecision: castedVote.activeVote[proposalIndex].decision,
newDecision: params.decision,
data: params.data,
}, [this.mutableContext.senderAddress]);
await this._removeSenderDelegatedVoteFromProposal();
castedVote.activeVote[proposalIndex].decision = params.decision;
castedVote.activeVote[proposalIndex].data = params.data;
}
else {
const boostedState = await this.boostedAccountStore.getOrDefault(this.mutableContext.context, this.mutableContext.senderAddress);
castedVote.activeVote.push({ proposalId: params.proposalId, decision: params.decision, data: params.data });
await this.addVote(baseScore, params.decision, boostedState.targetHeight);
const events = this.events.get(proposal_voted_1.ProposalVotedEvent);
events.add(this.mutableContext.context, {
proposalId: params.proposalId,
voterAddress: this.mutableContext.senderAddress,
decision: params.decision,
data: params.data,
}, [this.mutableContext.senderAddress]);
}
await this.proposalVoterStore.addVoter(this.mutableContext.context, params.proposalId, this.mutableContext.senderAddress);
await this.castedVoteStore.set(this.mutableContext.context, this.mutableContext.senderAddress, castedVote);
await this._addSenderDelegatedVoteFromProposal();
}
async verifySetAttributes(params) {
this._checkImmutableDependencies();
if (!(await this._isProposalExists(params.proposalId))) {
throw new Error(`proposal with id ${params.proposalId} doesn't exists`);
}
if (!this._isProposalAuthor()) {
throw new Error(`sender is not the proposal author`);
}
}
async setAttributes(params, verify = true) {
this._checkMutableDependencies();
if (verify)
await this.verifySetAttributes(params);
const attribute = { key: params.key, data: params.data };
const index = this.attributes.findIndex(attr => attr.key === params.key);
if (index > -1) {
this.attributes[index] = attribute;
}
else {
this.attributes.push(attribute);
}
await this._saveStore();
const events = this.events.get(proposal_set_attributes_1.ProposalSetAttributesEvent);
events.add(this.mutableContext.context, {
proposalId: params.proposalId,
key: params.key,
}, [this.author]);
}
async getVoteScore(address) {
this._checkImmutableDependencies();
const castedVote = await this.castedVoteStore.getOrDefault(this.mutableContext.context, this.mutableContext.senderAddress);
const proposalIndex = castedVote.activeVote.findIndex(vote => vote.proposalId === utils_2.bytes.bytesToNumber(this.key));
if (proposalIndex === -1)
return BigInt(0);
const boostedState = await this.boostedAccountStore.getOrDefault(this.mutableContext.context, this.mutableContext.senderAddress);
const baseScore = await this.voteScoreStore.getVoteScore(this.mutableContext.context, address);
return this._calculateVoteScore(baseScore, boostedState.targetHeight);
}
async addVote(score, decision, boostingHeight) {
if (this.status !== types_1.ProposalStatus.ACTIVE)
return;
const decisionString = this._numberToDecisionString(decision);
this.voteSummary[decisionString] += this._calculateVoteScore(score, boostingHeight);
this.turnout[decisionString] += score;
await this._saveStore();
}
async subtractVote(score, decision, boostingHeight) {
if (this.status !== types_1.ProposalStatus.ACTIVE)
return;
const decisionString = this._numberToDecisionString(decision);
this.voteSummary[decisionString] -= this._calculateVoteScore(score, boostingHeight);
this.turnout[decisionString] -= score;
await this._saveStore();
}
async setStatus(status) {
this.status = status;
await this._saveStore();
}
async _removeSenderDelegatedVoteFromProposal() {
this._checkMutableDependencies();
if (!this.internalMethod)
throw new Error(`proposal instance is created without internalMethod dependencies`);
const delegatedVote = await this.delegatedVoteStore.getMutableDelegatedVote(this.mutableContext);
const incomingDelegationVoteScore = await delegatedVote.getIncomingDelegationVoteScore();
await this.internalMethod.updateProposalVoteSummaryByVoter(this.mutableContext.context, this.mutableContext.senderAddress, BigInt(0), incomingDelegationVoteScore);
}
async _addSenderDelegatedVoteFromProposal() {
this._checkMutableDependencies();
if (!this.internalMethod)
throw new Error(`proposal instance is created without internalMethod dependencies`);
const delegatedVote = await this.delegatedVoteStore.getMutableDelegatedVote(this.mutableContext);
const incomingDelegationVoteScore = await delegatedVote.getIncomingDelegationVoteScore();
await this.internalMethod.updateProposalVoteSummaryByVoter(this.mutableContext.context, this.mutableContext.senderAddress, incomingDelegationVoteScore, BigInt(0));
}
_calculateVoteScore(score, boostingHeight) {
if (this.parameters.enableBoosting) {
const boostMultiplier = (0, utils_1.getBoostMultiplier)(this.parameters.endHeight, boostingHeight, this.parameters.maxBoostDuration, this.parameters.boostFactor);
return BigInt(new decimal_js_1.default(score.toString()).mul(boostMultiplier).toFixed(0));
}
return score;
}
_numberToDecisionString(decision) {
if (decision === types_1.Votes.FOR)
return 'for';
if (decision === types_1.Votes.AGAINST)
return 'against';
if (decision === types_1.Votes.ABSTAIN)
return 'abstain';
throw new Error(`unknown decision value: ${decision}`);
}
async _registerQueue(proposalId) {
this._checkMutableDependencies();
const { height } = this.mutableContext;
const config = await this.config.getConfig(this.mutableContext.context);
await this._saveQueue(proposalId, height + config.votingDelayDuration, 'start');
await this._saveQueue(proposalId, height + config.voteDuration, 'ends');
await this._saveQueue(proposalId, height + config.quorumDuration, 'quorum');
await this._saveQueue(proposalId, height + config.executionDuration, 'execute');
}
async _saveQueue(proposalId, height, type) {
const proposalQueue = await this.proposalQueueStore.getOrDefault(this.mutableContext.context, utils_2.bytes.numberToBytes(height));
if (proposalQueue[type].length >= constants_1.MAX_PROPOSAL_QUEUE_PER_BLOCK)
throw new Error(`Exceeded MAX_PROPOSAL_QUEUE_PER_BLOCK of ${constants_1.MAX_PROPOSAL_QUEUE_PER_BLOCK}`);
if (proposalQueue[type].findIndex(id => id === proposalId) === -1)
proposalQueue[type].push(proposalId);
await this.proposalQueueStore.set(this.mutableContext.context, utils_2.bytes.numberToBytes(height), proposalQueue);
}
async _getNextAvailableProposalId() {
this._checkDependencies();
return this.nextAvailableIdStore.getOrDefault(this.mutableContext.context);
}
async _increaseNextAvailableProposalId() {
this._checkDependencies();
await this.nextAvailableIdStore.increase(this.mutableContext.context);
}
_getStakingTokenId() {
return Buffer.concat([Buffer.from(this.genesisConfig.chainID, 'hex'), Buffer.from('00000000', 'hex')]);
}
async _isProposalExists(proposalId) {
return this.instanceStore.has(this.immutableContext.context, this.instanceStore.getKey(proposalId));
}
_isProposalAuthor() {
if (Buffer.compare(this.immutableContext.senderAddress, this.author) !== 0) {
return false;
}
return true;
}
}
exports.Proposal = Proposal;
//# sourceMappingURL=proposal.js.map