UNPKG

@swaptoshi/governance-module

Version:

Klayr governance on-chain module

357 lines 19.2 kB
"use strict"; 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