Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ STARKNET_RPC_URL= # optional
AUTH_SECRET=1dfd84a695705665668c260222ded178d1f1d62d251d7bee8148428dac6d0487 # optional
WALLETCONNECT_PROJECT_ID=e6454bd61aba40b786e866a69bd4c5c6
REOWN_SECRET= # optional
LAST_CB=1
1 change: 1 addition & 0 deletions src/helpers/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export async function getProposal(space, id) {
params: {}
};
proposal.choices = jsonParse(proposal.choices);
proposal.vp_value_by_strategy = jsonParse(proposal.vp_value_by_strategy, []);

return proposal;
}
Expand Down
22 changes: 22 additions & 0 deletions src/helpers/entityValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
type Vote = {
vp_by_strategy: number[];
};

type Proposal = {
vp_value_by_strategy: number[];
};

/**
* Calculates the total vote value based on the voting power and the proposal's value per strategy.
* @returns The total vote value, in the currency unit specified by the proposal's vp_value_by_strategy values
**/
export function getVoteValue(proposal: Proposal, vote: Vote): number {
if (proposal.vp_value_by_strategy.length !== vote.vp_by_strategy.length) {
throw new Error('invalid data to compute vote value');
}

return proposal.vp_value_by_strategy.reduce(
(sum, value, index) => sum + value * vote.vp_by_strategy[index],
0
);
}
21 changes: 20 additions & 1 deletion src/writer/delete-proposal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ export async function verify(body): Promise<any> {
}

export async function action(body): Promise<void> {
const BATCH_SIZE = 1000;

const msg = jsonParse(body.msg);
const proposal = await getProposal(msg.space, msg.payload.proposal);

const voters = await db.queryAsync(`SELECT voter FROM votes WHERE proposal = ?`, [
const voters = await db.queryAsync(`SELECT voter, vp_value FROM votes WHERE proposal = ?`, [
msg.payload.proposal
]);
const id = msg.payload.proposal;
Expand Down Expand Up @@ -53,4 +55,21 @@ export async function action(body): Promise<void> {
}

await db.queryAsync(queries, parameters);

const votersWithVpValue = voters.filter(v => v.vp_value > 0);
if (votersWithVpValue.length > 0) {
for (let i = 0; i < votersWithVpValue.length; i += BATCH_SIZE) {
const batch = votersWithVpValue.slice(i, i + BATCH_SIZE);
const vpQueries = batch
.map(
() =>
`UPDATE leaderboard SET vp_value = GREATEST(vp_value - ?, 0) WHERE user = ? AND space = ?;`
)
.join('\n ');

const vpParams = batch.flatMap(voter => [voter.vp_value, voter.voter, msg.space]);

await db.queryAsync(vpQueries, vpParams);
}
}
}
27 changes: 22 additions & 5 deletions src/writer/vote.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { capture } from '@snapshot-labs/snapshot-sentry';
import snapshot from '@snapshot-labs/snapshot.js';
import { getProposal } from '../helpers/actions';
import { getVoteValue } from '../helpers/entityValue';
import log from '../helpers/log';
import db from '../helpers/mysql';
import { captureError, hasStrategyOverride, jsonParse } from '../helpers/utils';
import { updateProposalAndVotes } from '../scores';

const LAST_CB = parseInt(process.env.LAST_CB ?? '1');
const scoreAPIUrl = process.env.SCORE_API_URL || 'https://score.snapshot.org';

// async function isLimitReached(space) {
Expand Down Expand Up @@ -116,6 +119,18 @@ export async function action(body, ipfs, receipt, id, context): Promise<void> {
const withOverride = hasStrategyOverride(context.proposal.strategies);
if (vpState === 'final' && withOverride) vpState = 'pending';

// Get proposal voting power value
// Value is set on creation, and not updated on vote update
let vpValue = 0;
let cb = 0;

try {
vpValue = getVoteValue(context.proposal, context.vp);
cb = LAST_CB;
} catch (e: any) {
capture(e, { msg, proposalId, context });
}

const params = {
id,
ipfs,
Expand All @@ -130,7 +145,8 @@ export async function action(body, ipfs, receipt, id, context): Promise<void> {
vp: context.vp.vp,
vp_by_strategy: JSON.stringify(context.vp.vp_by_strategy),
vp_state: vpState,
cb: 0
vp_value: vpValue,
cb
};

// Check if voter already voted
Expand Down Expand Up @@ -171,6 +187,7 @@ export async function action(body, ipfs, receipt, id, context): Promise<void> {
proposalId,
msg.space,
created,
vpValue,
voter,
msg.space
]
Expand All @@ -180,12 +197,12 @@ export async function action(body, ipfs, receipt, id, context): Promise<void> {
await db.queryAsync(
`
INSERT INTO votes SET ?;
INSERT INTO leaderboard (space, user, vote_count, last_vote)
VALUES(?, ?, 1, ?)
ON DUPLICATE KEY UPDATE vote_count = vote_count + 1, last_vote = ?;
INSERT INTO leaderboard (space, user, vote_count, last_vote, vp_value)
VALUES(?, ?, 1, ?, ?)
ON DUPLICATE KEY UPDATE vote_count = vote_count + 1, last_vote = VALUES(last_vote), vp_value = vp_value + VALUES(vp_value);
UPDATE spaces SET vote_count = vote_count + 1 WHERE id = ?;
`,
[params, msg.space, voter, created, created, msg.space]
[params, msg.space, voter, created, vpValue, msg.space]
);
}

Expand Down
6 changes: 5 additions & 1 deletion test/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ CREATE TABLE votes (
vp DECIMAL(64,30) NOT NULL,
vp_by_strategy JSON NOT NULL,
vp_state VARCHAR(24) NOT NULL,
vp_value DECIMAL(13,3) NOT NULL DEFAULT 0.000,
cb INT(11) NOT NULL,
PRIMARY KEY (voter, space, proposal),
UNIQUE KEY id (id),
Expand All @@ -111,6 +112,7 @@ CREATE TABLE votes (
INDEX app (app),
INDEX vp (vp),
INDEX vp_state (vp_state),
INDEX vp_value (vp_value),
INDEX cb (cb)
);

Expand Down Expand Up @@ -188,11 +190,13 @@ CREATE TABLE leaderboard (
vote_count SMALLINT UNSIGNED NOT NULL DEFAULT '0',
proposal_count SMALLINT UNSIGNED NOT NULL DEFAULT '0',
last_vote BIGINT,
vp_value DECIMAL(13,3) NOT NULL DEFAULT 0.000,
PRIMARY KEY user_space (user,space),
INDEX space (space),
INDEX vote_count (vote_count),
INDEX proposal_count (proposal_count),
INDEX last_vote (last_vote)
INDEX last_vote (last_vote),
INDEX vp_value (vp_value)
);

CREATE TABLE skins (
Expand Down
71 changes: 71 additions & 0 deletions test/unit/helpers/entityValue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { getVoteValue } from '../../../src/helpers/entityValue';

describe('getVoteValue', () => {
it('should calculate correct vote value with single strategy', () => {
const proposal = { vp_value_by_strategy: [2.5] };
const vote = { vp_by_strategy: [100] };

const result = getVoteValue(proposal, vote);

expect(result).toBe(250);
});

it('should calculate correct vote value with multiple strategies', () => {
const proposal = { vp_value_by_strategy: [1.5, 3.0, 0.5] };
const vote = { vp_by_strategy: [100, 50, 200] };

const result = getVoteValue(proposal, vote);

expect(result).toBe(400); // (1.5 * 100) + (3.0 * 50) + (0.5 * 200) = 150 + 150 + 100 = 400
});

it('should return 0 when vote has no voting power', () => {
const proposal = { vp_value_by_strategy: [2.0, 1.5] };
const vote = { vp_by_strategy: [0, 0] };

const result = getVoteValue(proposal, vote);

expect(result).toBe(0);
});

it('should return 0 when proposal has no value per strategy', () => {
const proposal = { vp_value_by_strategy: [0, 0] };
const vote = { vp_by_strategy: [100, 50] };

const result = getVoteValue(proposal, vote);

expect(result).toBe(0);
});

it('should handle decimal values correctly', () => {
const proposal = { vp_value_by_strategy: [0.1, 0.25] };
const vote = { vp_by_strategy: [10, 20] };

const result = getVoteValue(proposal, vote);

expect(result).toBe(6); // (0.1 * 10) + (0.25 * 20) = 1 + 5 = 6
});

it('should throw error when strategy arrays have different lengths', () => {
const proposal = { vp_value_by_strategy: [1.0, 2.0] };
const vote = { vp_by_strategy: [100] };

expect(() => getVoteValue(proposal, vote)).toThrow('invalid data to compute vote value');
});

it('should throw error when vote has more strategies than proposal', () => {
const proposal = { vp_value_by_strategy: [1.0] };
const vote = { vp_by_strategy: [100, 50] };

expect(() => getVoteValue(proposal, vote)).toThrow('invalid data to compute vote value');
});

it('should handle empty arrays', () => {
const proposal = { vp_value_by_strategy: [] };
const vote = { vp_by_strategy: [] };

const result = getVoteValue(proposal, vote);

expect(result).toBe(0);
});
});