Skip to content

Commit d34844a

Browse files
Disputable Voting: Support quiet ending (#1205)
* DisputableVoting: disallow changing votes * DisputableVoting: split create and vote tests * DisputableVoting: fix can execute * DisputableVoting: implement quiet ending * DisputableVoting: Minor enhancements based on review from @bingen * DisputableVoting: Add tests for challenged states
1 parent b883322 commit d34844a

14 files changed

+1087
-380
lines changed

apps/voting-disputable/arapp.json

+10
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@
6868
"New execution delay",
6969
"Current execution delay"
7070
]
71+
},
72+
{
73+
"name": "Modify quiet ending configuration",
74+
"id": "MODIFY_QUIET_ENDING_CONFIGURATION",
75+
"params": [
76+
"New quiet ending period",
77+
"Current quiet ending period",
78+
"New quiet ending extension",
79+
"Current quiet ending extension"
80+
]
7181
}
7282
],
7383
"path": "contracts/DisputableVoting.sol"

apps/voting-disputable/contracts/DisputableVoting.sol

+177-49
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
const deployer = require('../helpers/deployer')(web3, artifacts)
2+
const { ARAGON_OS_ERRORS, VOTING_ERRORS } = require('../helpers/errors')
3+
const { VOTER_STATE, createVote, voteScript, getVoteState } = require('../helpers/voting')
4+
5+
const { ONE_DAY, bigExp, pct16, getEventArgument } = require('@aragon/contract-helpers-test')
6+
const { assertBn, assertRevert, assertEvent, assertAmountOfEvents } = require('@aragon/contract-helpers-test/src/asserts')
7+
8+
contract('Voting', ([_, owner, holder1, holder2, holder20, holder29, holder51, nonHolder]) => {
9+
let voting, token
10+
11+
const CONTEXT = '0xabcdef'
12+
const VOTE_DURATION = 5 * ONE_DAY
13+
const OVERRULE_WINDOW = ONE_DAY
14+
const EXECUTION_DELAY = 0
15+
const QUIET_ENDING_PERIOD = ONE_DAY
16+
const QUIET_ENDING_EXTENSION = ONE_DAY / 2
17+
const REQUIRED_SUPPORT = pct16(50)
18+
const MINIMUM_ACCEPTANCE_QUORUM = pct16(20)
19+
20+
beforeEach('deploy voting', async () => {
21+
voting = await deployer.deploy({ owner })
22+
})
23+
24+
describe('newVote', () => {
25+
it('it is a forwarder', async () => {
26+
assert.isTrue(await voting.isForwarder())
27+
})
28+
29+
context('when the app was not initialized', async () => {
30+
it('fails creating a vote', async () => {
31+
await assertRevert(createVote({ voting, script: false }), ARAGON_OS_ERRORS.APP_AUTH_FAILED)
32+
})
33+
34+
it('fails to forward actions', async () => {
35+
const { script } = await voteScript()
36+
await assertRevert(voting.forward(script, { from: holder51 }), VOTING_ERRORS.VOTING_CANNOT_FORWARD)
37+
})
38+
})
39+
40+
context('when the app was initialized', () => {
41+
beforeEach('initialize voting', async () => {
42+
token = await deployer.deployToken({})
43+
await voting.initialize(token.address, REQUIRED_SUPPORT, MINIMUM_ACCEPTANCE_QUORUM, VOTE_DURATION, OVERRULE_WINDOW, QUIET_ENDING_PERIOD, QUIET_ENDING_EXTENSION, EXECUTION_DELAY)
44+
})
45+
46+
context('when there is some supply', () => {
47+
let voteId
48+
49+
context('with normal token supply', () => {
50+
let script, executionTarget
51+
52+
beforeEach('mint tokens', async () => {
53+
await deployer.token.generateTokens(holder51, bigExp(51, 18))
54+
await deployer.token.generateTokens(holder29, bigExp(29, 18))
55+
await deployer.token.generateTokens(holder20, bigExp(20, 18))
56+
})
57+
58+
beforeEach('build script', async () => {
59+
({ script, executionTarget } = await voteScript())
60+
})
61+
62+
beforeEach('create vote', async () => {
63+
({ voteId, receipt } = await createVote({ voting, script, voteContext: CONTEXT, from: holder51 }))
64+
})
65+
66+
it('can be forwarded', async () => {
67+
const receipt = await voting.forward(script, { from: holder51 })
68+
voteId = getEventArgument(receipt, 'StartVote', 'voteId')
69+
70+
assertBn(voteId, 1, 'voting should have been created')
71+
})
72+
73+
it('emits an event', async () => {
74+
assertAmountOfEvents(receipt, 'StartVote')
75+
assertEvent(receipt, 'StartVote', { expectedArgs: { voteId, creator: holder51, context: CONTEXT } })
76+
})
77+
78+
it('has correct state', async () => {
79+
const { isOpen, isExecuted, snapshotBlock, support, quorum, overruleWindow, executionDelay, yeas, nays, votingPower, script: execScript } = await getVoteState(voting, voteId)
80+
81+
assert.isTrue(isOpen, 'vote should be open')
82+
assert.isFalse(isExecuted, 'vote should not be executed')
83+
assertBn(snapshotBlock, await web3.eth.getBlockNumber() - 1, 'snapshot block should be correct')
84+
assertBn(support, REQUIRED_SUPPORT, 'required support should be app required support')
85+
assertBn(quorum, MINIMUM_ACCEPTANCE_QUORUM, 'min quorum should be app min quorum')
86+
assertBn(overruleWindow, OVERRULE_WINDOW, 'default overrule window should be correct')
87+
assertBn(executionDelay, EXECUTION_DELAY, 'default execution delay should be correct')
88+
assertBn(yeas, 0, 'initial yea should be 0')
89+
assertBn(nays, 0, 'initial nay should be 0')
90+
assertBn(votingPower, bigExp(100, 18), 'voting power should be 100')
91+
assert.equal(execScript, script, 'script should be correct')
92+
assertBn(await voting.getVoterState(voteId, nonHolder), VOTER_STATE.ABSENT, 'nonHolder should not have voted')
93+
})
94+
95+
it('fails getting a vote out of bounds', async () => {
96+
await assertRevert(voting.getVote(voteId + 1), VOTING_ERRORS.VOTING_NO_VOTE)
97+
})
98+
})
99+
100+
context('token supply = 1', () => {
101+
beforeEach('mint tokens', async () => {
102+
await token.generateTokens(holder1, 1)
103+
})
104+
105+
it('new vote cannot be executed before voting', async () => {
106+
// Account creating vote does not have any tokens and therefore doesn't vote
107+
({ voteId } = await createVote({ voting, script: false }))
108+
109+
assert.isFalse(await voting.canExecute(voteId), 'vote cannot be executed')
110+
111+
await voting.vote(voteId, true, { from: holder1 })
112+
await voting.mockIncreaseTime(VOTE_DURATION)
113+
await voting.executeVote(voteId)
114+
115+
const { isOpen, isExecuted } = await getVoteState(voting, voteId)
116+
117+
assert.isFalse(isOpen, 'vote should be closed')
118+
assert.isTrue(isExecuted, 'vote should have been executed')
119+
})
120+
121+
it('creates a vote without executing', async () => {
122+
({ voteId } = await createVote({ voting, script: false, from: holder1 }))
123+
124+
const { isOpen, isExecuted } = await getVoteState(voting, voteId)
125+
assert.isTrue(isOpen, 'vote should be open')
126+
assert.isFalse(isExecuted, 'vote should not have been executed')
127+
})
128+
})
129+
130+
context('token supply = 3', () => {
131+
beforeEach('mint tokens', async () => {
132+
await token.generateTokens(holder1, 1)
133+
await token.generateTokens(holder2, 2)
134+
})
135+
136+
it('new vote cannot be executed even after holder2 voting', async () => {
137+
({ voteId } = await createVote({ voting, script: false }))
138+
139+
await voting.vote(voteId, true, { from: holder1 })
140+
await voting.vote(voteId, true, { from: holder2 })
141+
142+
const { isOpen, isExecuted } = await getVoteState(voting, voteId)
143+
assert.isTrue(isOpen, 'vote should still be open')
144+
assert.isFalse(isExecuted, 'vote should have not been executed')
145+
assert.isFalse(await voting.canExecute(voteId), 'vote cannot be executed')
146+
})
147+
148+
it('creating vote as holder2 does not execute vote', async () => {
149+
({ voteId } = await createVote({ voting, script: false, from: holder2 }))
150+
151+
const { isOpen, isExecuted } = await getVoteState(voting, voteId)
152+
assert.isTrue(isOpen, 'vote should still be open')
153+
assert.isFalse(isExecuted, 'vote should have not been executed')
154+
assert.isFalse(await voting.canExecute(voteId), 'vote cannot be executed')
155+
})
156+
})
157+
158+
context('with changing token supply', () => {
159+
beforeEach('mint tokens', async () => {
160+
await token.generateTokens(holder1, 1)
161+
await token.generateTokens(holder2, 1)
162+
})
163+
164+
it('uses the correct snapshot value if tokens are minted afterwards', async () => {
165+
// Create vote and afterwards generate some tokens
166+
({ voteId } = await createVote({ voting, script: false }))
167+
await token.generateTokens(holder2, 1)
168+
169+
const { snapshotBlock, votingPower } = await getVoteState(voting, voteId)
170+
171+
// Generating tokens advanced the block by one
172+
assertBn(snapshotBlock, await web3.eth.getBlockNumber() - 2, 'snapshot block should be correct')
173+
assertBn(votingPower, await token.totalSupplyAt(snapshotBlock), 'voting power should match snapshot supply')
174+
assertBn(votingPower, 2, 'voting power should be correct')
175+
})
176+
177+
it('uses the correct snapshot value if tokens are minted in the same block', async () => {
178+
// Create vote and generate some tokens in the same transaction
179+
// Requires the voting mock to be the token's owner
180+
await token.changeController(voting.address)
181+
const receipt = await voting.newTokenAndVote(holder2, 1, CONTEXT)
182+
voteId = getEventArgument(receipt, 'StartVote', 'voteId')
183+
184+
const { snapshotBlock, votingPower } = await getVoteState(voting, voteId)
185+
186+
assertBn(snapshotBlock, await web3.eth.getBlockNumber() - 1, 'snapshot block should be correct')
187+
assertBn(votingPower, await token.totalSupplyAt(snapshotBlock), 'voting power should match snapshot supply')
188+
assertBn(votingPower, 2, 'voting power should be correct')
189+
})
190+
})
191+
})
192+
193+
context('when there is no supply', () => {
194+
it('reverts', async () => {
195+
await assertRevert(createVote({ voting, script: false }), VOTING_ERRORS.VOTING_NO_VOTING_POWER)
196+
})
197+
})
198+
})
199+
})
200+
})

0 commit comments

Comments
 (0)