Skip to content

Commit 25055c0

Browse files
authored
fix: normalize copeland score with voting power (#1131)
* fix: normalize copeland score with voting power * v0.12.53
1 parent 17da754 commit 25055c0

File tree

4 files changed

+293
-68
lines changed

4 files changed

+293
-68
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@snapshot-labs/snapshot.js",
3-
"version": "0.12.52",
3+
"version": "0.12.53",
44
"repository": "snapshot-labs/snapshot.js",
55
"license": "MIT",
66
"main": "dist/snapshot.cjs.js",

src/voting/__snapshots__/copeland.spec.js.snap

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,27 @@
22

33
exports[`Partial ranking 1`] = `
44
[
5-
4,
6-
6,
7-
2,
5+
1.6666666666666665,
6+
2.5,
7+
0.8333333333333333,
88
0,
99
]
1010
`;
1111

1212
exports[`getScores 1`] = `
1313
[
14-
5,
15-
5,
16-
2,
14+
1.6666666666666667,
15+
1.6666666666666667,
16+
0.6666666666666666,
1717
0,
1818
]
1919
`;
2020

2121
exports[`getScores 2`] = `
2222
[
23-
5,
24-
5,
25-
2,
23+
1.6666666666666667,
24+
1.6666666666666667,
25+
0.6666666666666666,
2626
0,
2727
]
2828
`;
@@ -45,16 +45,32 @@ exports[`getScores 4`] = `
4545
]
4646
`;
4747

48+
exports[`getScores with 0 votes 1`] = `
49+
[
50+
0,
51+
0,
52+
0,
53+
]
54+
`;
55+
56+
exports[`getScores with mixed voting powers 1`] = `
57+
[
58+
668334.3333333333,
59+
334167.1666666666,
60+
0,
61+
]
62+
`;
63+
4864
exports[`getScoresByStrategy 1`] = `
4965
[
5066
[
51-
5,
67+
1.6666666666666667,
5268
],
5369
[
54-
5,
70+
1.6666666666666667,
5571
],
5672
[
57-
2,
73+
0.6666666666666666,
5874
],
5975
[
6076
0,
@@ -65,13 +81,13 @@ exports[`getScoresByStrategy 1`] = `
6581
exports[`getScoresByStrategy 2`] = `
6682
[
6783
[
68-
5,
84+
1.6666666666666667,
6985
],
7086
[
71-
5,
87+
1.6666666666666667,
7288
],
7389
[
74-
2,
90+
0.6666666666666666,
7591
],
7692
[
7793
0,
@@ -82,19 +98,19 @@ exports[`getScoresByStrategy 2`] = `
8298
exports[`getScoresByStrategy 3`] = `
8399
[
84100
[
85-
5,
86-
5,
87-
5,
101+
1.6666666666666667,
102+
1.6666666666666667,
103+
1.6666666666666667,
88104
],
89105
[
90-
5,
91-
5,
92-
5,
106+
1.6666666666666667,
107+
1.6666666666666667,
108+
1.6666666666666667,
93109
],
94110
[
95-
2,
96-
2,
97-
2,
111+
0.6666666666666666,
112+
0.6666666666666666,
113+
0.6666666666666666,
98114
],
99115
[
100116
0,
@@ -107,19 +123,19 @@ exports[`getScoresByStrategy 3`] = `
107123
exports[`getScoresByStrategy 4`] = `
108124
[
109125
[
110-
5,
111-
5,
112-
5,
126+
1.6666666666666667,
127+
1.6666666666666667,
128+
1.6666666666666667,
113129
],
114130
[
115-
5,
116-
5,
117-
5,
131+
1.6666666666666667,
132+
1.6666666666666667,
133+
1.6666666666666667,
118134
],
119135
[
120-
2,
121-
2,
122-
2,
136+
0.6666666666666666,
137+
0.6666666666666666,
138+
0.6666666666666666,
123139
],
124140
[
125141
0,
@@ -129,6 +145,23 @@ exports[`getScoresByStrategy 4`] = `
129145
]
130146
`;
131147

148+
exports[`getScoresByStrategy normalizes correctly 1`] = `
149+
[
150+
[
151+
7.5,
152+
22.5,
153+
],
154+
[
155+
7.5,
156+
22.5,
157+
],
158+
[
159+
7.5,
160+
22.5,
161+
],
162+
]
163+
`;
164+
132165
exports[`getScoresTotal 1`] = `4`;
133166

134167
exports[`getScoresTotal 2`] = `12`;

src/voting/copeland.spec.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,44 @@ const votesWithInvalidChoices2 = () => {
4949
return [...invalidVotes, ...example2().votes];
5050
};
5151

52+
// Helper function to create example with decimal voting powers
53+
const exampleWithDecimals = () => {
54+
const proposal = {
55+
choices: ['Alice', 'Bob', 'Carol']
56+
};
57+
const strategies = [{ name: 'ticket', network: '1', params: {} }];
58+
const votes = [
59+
{ choice: [1, 2, 3], balance: 1.5, scores: [1.5] },
60+
{ choice: [2, 1, 3], balance: 2.75, scores: [2.75] },
61+
{ choice: [3, 2, 1], balance: 0.25, scores: [0.25] }
62+
];
63+
64+
return {
65+
proposal,
66+
strategies,
67+
votes
68+
};
69+
};
70+
71+
// Helper function to create example with high voting powers
72+
const exampleWithHighPowers = () => {
73+
const proposal = {
74+
choices: ['Alice', 'Bob', 'Carol']
75+
};
76+
const strategies = [{ name: 'ticket', network: '1', params: {} }];
77+
const votes = [
78+
{ choice: [1, 2, 3], balance: 1000000, scores: [1000000] },
79+
{ choice: [2, 1, 3], balance: 2500000, scores: [2500000] },
80+
{ choice: [3, 2, 1], balance: 1500000, scores: [1500000] }
81+
];
82+
83+
return {
84+
proposal,
85+
strategies,
86+
votes
87+
};
88+
};
89+
5290
// Test cases for getScores method
5391
test.each([
5492
[example.proposal, example.votes, example.strategies],
@@ -81,6 +119,42 @@ test.each([
81119
expect(copeland.getScoresByStrategy()).toMatchSnapshot();
82120
});
83121

122+
// Add test for verifying strategy normalization
123+
test('getScoresByStrategy normalizes correctly', () => {
124+
const proposal = {
125+
choices: ['Alice', 'Bob', 'Carol']
126+
};
127+
const strategies = [
128+
{ name: 'ticket', network: '1', params: {} },
129+
{ name: 'erc20-balance-of', network: '1', params: {} }
130+
];
131+
const votes = [
132+
{ choice: [1, 2, 3], balance: 10, scores: [5, 15] },
133+
{ choice: [2, 3, 1], balance: 20, scores: [10, 30] },
134+
{ choice: [3, 1, 2], balance: 15, scores: [7.5, 22.5] }
135+
];
136+
137+
const copeland = new CopelandVoting(proposal, votes, strategies, [1]);
138+
139+
const scoresByStrategy = copeland.getScoresByStrategy();
140+
expect(scoresByStrategy).toMatchSnapshot();
141+
142+
// Verify totals per strategy
143+
const strategyTotals = [22.5, 67.5]; // Sum of all votes per strategy
144+
145+
// Sum the scores for each strategy
146+
const strategyTotalResults = [0, 0];
147+
for (let i = 0; i < strategies.length; i++) {
148+
for (let j = 0; j < proposal.choices.length; j++) {
149+
strategyTotalResults[i] += scoresByStrategy[j][i];
150+
}
151+
}
152+
153+
// Verify total voting power is preserved for each strategy
154+
expect(strategyTotalResults[0]).toBeCloseTo(strategyTotals[0], 5);
155+
expect(strategyTotalResults[1]).toBeCloseTo(strategyTotals[1], 5);
156+
});
157+
84158
// Test cases for getScoresTotal method
85159
test.each([
86160
[example.proposal, example.votes, example.strategies],
@@ -127,3 +201,30 @@ test('Partial ranking', () => {
127201
);
128202
expect(copeland.getScores()).toMatchSnapshot();
129203
});
204+
205+
test('getScores with mixed voting powers', () => {
206+
const proposal = {
207+
choices: ['Alice', 'Bob', 'Carol']
208+
};
209+
const votes = [
210+
{ choice: [1, 2, 3], balance: 1000000.75, scores: [1000000.75] },
211+
{ choice: [2, 3, 1], balance: 0.25, scores: [0.25] },
212+
{ choice: [3, 1, 2], balance: 2500.5, scores: [2500.5] }
213+
];
214+
const copeland = new CopelandVoting(proposal, votes, example.strategies, [1]);
215+
const scores = copeland.getScores();
216+
expect(scores).toMatchSnapshot();
217+
// Verify total voting power is preserved
218+
expect(scores.reduce((a, b) => a + b)).toBeCloseTo(1002501.5, 5);
219+
});
220+
221+
test('getScores with 0 votes', () => {
222+
const proposal = {
223+
choices: ['Alice', 'Bob', 'Carol']
224+
};
225+
const votes = [];
226+
const copeland = new CopelandVoting(proposal, votes, example.strategies, [1]);
227+
const scores = copeland.getScores();
228+
expect(scores).toMatchSnapshot();
229+
// Verify total voting power is preserved
230+
});

0 commit comments

Comments
 (0)