diff --git a/contracts/Court.sol b/contracts/Court.sol index 7d896a24..4843416e 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -1235,6 +1235,7 @@ contract Court is IJurorsRegistryOwner, ICRVotingOwner, ISubscriptionsOwner, Tim // Otherwise, return the times the active balance of the juror fits in the min active balance, multiplying // it by a round factor to ensure a better precision rounding. + // TODO: review, we are not using the final round discount here return (FINAL_ROUND_WEIGHT_PRECISION.mul(activeBalance) / minJurorsActiveBalance).toUint64(); } diff --git a/test/court/court-appeal.js b/test/court/court-appeal.js index e5b087eb..f2be589c 100644 --- a/test/court/court-appeal.js +++ b/test/court/court-appeal.js @@ -2,8 +2,8 @@ const { bigExp } = require('../helpers/numbers')(web3) const { filterJurors } = require('../helpers/jurors') const { assertRevert } = require('@aragon/os/test/helpers/assertThrow') const { assertAmountOfEvents, assertEvent } = require('@aragon/os/test/helpers/assertEvent')(web3) -const { buildHelper, ROUND_STATES, DISPUTE_STATES } = require('../helpers/court')(web3, artifacts) -const { OUTCOMES, getVoteId, oppositeOutcome, outcomeFor } = require('../helpers/crvoting')(web3) +const { getVoteId, oppositeOutcome, outcomeFor, OUTCOMES } = require('../helpers/crvoting')(web3) +const { buildHelper, DEFAULTS, ROUND_STATES, DISPUTE_STATES } = require('../helpers/court')(web3, artifacts) const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' @@ -109,8 +109,8 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju context('when the appeal maker has enough balance', () => { beforeEach('mint fee tokens for appeal maker', async () => { - await courtHelper.feeToken.generateTokens(appealMaker, bigExp(1e6, 18)) - await courtHelper.feeToken.approve(court.address, bigExp(1e6, 18), { from: appealMaker }) + const { appealDeposit } = await courtHelper.getAppealFees(disputeId, roundId) + await courtHelper.mintAndApproveFeeTokens(appealMaker, court.address, appealDeposit) }) it('emits an event', async () => { @@ -124,7 +124,7 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju await court.appeal(disputeId, roundId, appealMakerRuling, { from: appealMaker }) const { appealer, appealedRuling, taker, opposedRuling } = await courtHelper.getAppeal(disputeId, roundId) - assert.equal(appealer, appealMaker, 'appealer does not match') + assert.equal(appealer, appealMaker, 'appeal maker does not match') assert.equal(appealedRuling.toString(), appealMakerRuling, 'appealed ruling does not match') assert.equal(taker.toString(), ZERO_ADDRESS, 'appeal taker does not match') assert.equal(opposedRuling.toString(), 0, 'opposed ruling does not match') @@ -132,7 +132,7 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju it('transfers the appeal deposit to the court', async () => { const { accounting, feeToken } = courtHelper - const expectedAppealDeposit = await courtHelper.getAppealDeposit(disputeId, roundId) + const { appealDeposit } = await courtHelper.getAppealFees(disputeId, roundId) const previousCourtBalance = await feeToken.balanceOf(court.address) const previousAccountingBalance = await feeToken.balanceOf(accounting.address) @@ -144,10 +144,10 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju assert.equal(previousCourtBalance.toString(), currentCourtBalance.toString(), 'court balances do not match') const currentAccountingBalance = await feeToken.balanceOf(accounting.address) - assert.equal(previousAccountingBalance.plus(expectedAppealDeposit).toString(), currentAccountingBalance.toString(), 'court accounting balances do not match') + assert.equal(previousAccountingBalance.plus(appealDeposit).toString(), currentAccountingBalance.toString(), 'court accounting balances do not match') const currentAppealerBalance = await feeToken.balanceOf(appealMaker) - assert.equal(previousAppealerBalance.minus(expectedAppealDeposit).toString(), currentAppealerBalance.toString(), 'sender balances do not match') + assert.equal(previousAppealerBalance.minus(appealDeposit).toString(), currentAppealerBalance.toString(), 'sender balances do not match') }) it('does not create a new round for the dispute', async () => { @@ -279,7 +279,7 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju }) context('for a final round', () => { - const roundId = 3 + const roundId = DEFAULTS.maxRegularAppealRounds.toNumber() beforeEach('move to final round', async () => { await courtHelper.moveToFinalRound({ disputeId }) diff --git a/test/court/court-confirm-appeal.js b/test/court/court-confirm-appeal.js index fa854871..0611ebd6 100644 --- a/test/court/court-confirm-appeal.js +++ b/test/court/court-confirm-appeal.js @@ -2,8 +2,8 @@ const { bigExp } = require('../helpers/numbers')(web3) const { filterJurors } = require('../helpers/jurors') const { assertRevert } = require('@aragon/os/test/helpers/assertThrow') const { assertAmountOfEvents, assertEvent } = require('@aragon/os/test/helpers/assertEvent')(web3) -const { buildHelper, ROUND_STATES, DISPUTE_STATES } = require('../helpers/court')(web3, artifacts) -const { OUTCOMES, getVoteId, oppositeOutcome, outcomeFor } = require('../helpers/crvoting')(web3) +const { getVoteId, oppositeOutcome, outcomeFor, OUTCOMES } = require('../helpers/crvoting')(web3) +const { buildHelper, DEFAULTS, ROUND_STATES, DISPUTE_STATES } = require('../helpers/court')(web3, artifacts) contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, juror1000, juror1500, juror2000, juror2500, juror3000, juror3500, juror4000]) => { let courtHelper, court, voting @@ -129,80 +129,132 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju context('when the appeal taker has enough balance', () => { beforeEach('mint fee tokens for appeal taker', async () => { - await courtHelper.feeToken.generateTokens(appealTaker, bigExp(1e6, 18)) - await courtHelper.feeToken.approve(court.address, bigExp(1e6, 18), { from: appealTaker }) + const { confirmAppealDeposit } = await courtHelper.getAppealFees(disputeId, roundId) + await courtHelper.mintAndApproveFeeTokens(appealTaker, court.address, confirmAppealDeposit) }) - it('emits an event', async () => { - const receipt = await court.confirmAppeal(disputeId, roundId, appealTakerRuling, { from: appealTaker }) + const itCreatesNewRoundSuccessfully = roundId => { + it('computes next round details successfully', async () => { + const [nextRoundStartTerm, nextRoundJurorsNumber, newDisputeState, feeToken, totalFees, jurorFees, appealDeposit, confirmAppealDeposit] = await court.getNextRoundDetails(disputeId, roundId) - assertAmountOfEvents(receipt, 'RulingAppealConfirmed') + const expectedStartTerm = await courtHelper.getNextRoundStartTerm(disputeId, roundId) + assert.equal(nextRoundStartTerm.toString(), expectedStartTerm.toString(), 'next round start term does not match') - const newRoundDraftTerm = (await court.getLastEnsuredTermId()).plus(courtHelper.appealConfirmTerms) - const nextRoundJurorsNumber = await courtHelper.getNextRoundJurorsNumber(disputeId, roundId) - assertEvent(receipt, 'RulingAppealConfirmed', { - disputeId, - roundId: roundId + 1, - draftTermId: newRoundDraftTerm, - jurorsNumber: nextRoundJurorsNumber + const expectedJurorsNumber = await courtHelper.getNextRoundJurorsNumber(disputeId, roundId) + assert.equal(nextRoundJurorsNumber.toString(), expectedJurorsNumber.toString(), 'next round jurors number does not match') + + const expectedDisputeState = (roundId < courtHelper.maxRegularAppealRounds.toNumber() - 1) ? DISPUTE_STATES.PRE_DRAFT : DISPUTE_STATES.ADJUDICATING + assert.equal(newDisputeState.toString(), expectedDisputeState.toString(), 'next round jurors number does not match') + + const expectedJurorFees = await courtHelper.getNextRoundJurorFees(disputeId, roundId) + assert.equal(jurorFees.toString(), expectedJurorFees.toString(), 'juror fees does not match') + + const { appealFees, appealDeposit: expectedAppealDeposit, confirmAppealDeposit: expectedConfirmAppealDeposit } = await courtHelper.getAppealFees(disputeId, roundId) + assert.equal(feeToken, courtHelper.feeToken.address, 'fee token does not match') + assert.equal(totalFees.toString(), appealFees.toString(), 'appeal fees does not match') + assert.equal(appealDeposit.toString(), expectedAppealDeposit.toString(), 'appeal deposit does not match') + assert.equal(confirmAppealDeposit.toString(), expectedConfirmAppealDeposit.toString(), 'confirm appeal deposit does not match') }) - }) - it('confirms the given appealed round', async () => { - await court.confirmAppeal(disputeId, roundId, appealTakerRuling, { from: appealTaker }) + it('emits an event', async () => { + const receipt = await court.confirmAppeal(disputeId, roundId, appealTakerRuling, { from: appealTaker }) - const { appealer, appealedRuling, taker, opposedRuling } = await courtHelper.getAppeal(disputeId, roundId) - assert.equal(appealer, appealMaker, 'appealer does not match') - assert.equal(appealedRuling.toString(), appealMakerRuling, 'appealed ruling does not match') - assert.equal(taker.toString(), appealTaker, 'appeal taker does not match') - assert.equal(opposedRuling.toString(), appealTakerRuling, 'opposed ruling does not match') - }) + assertAmountOfEvents(receipt, 'RulingAppealConfirmed') + + const nextRoundStartTerm = await courtHelper.getNextRoundStartTerm(disputeId, roundId) + const nextRoundJurorsNumber = await courtHelper.getNextRoundJurorsNumber(disputeId, roundId) + assertEvent(receipt, 'RulingAppealConfirmed', { + disputeId, + roundId: roundId + 1, + draftTermId: nextRoundStartTerm, + jurorsNumber: nextRoundJurorsNumber + }) + }) - it('creates a new round for the given dispute', async () => { - await court.confirmAppeal(disputeId, roundId, appealTakerRuling, { from: appealTaker }) + it('confirms the given appealed round', async () => { + await court.confirmAppeal(disputeId, roundId, appealTakerRuling, { from: appealTaker }) - const newRoundId = roundId + 1 - const { draftTerm, delayedTerms, roundJurorsNumber, selectedJurors, triggeredBy, settledPenalties, collectedTokens } = await courtHelper.getRound(disputeId, newRoundId) + const { appealer, appealedRuling, taker, opposedRuling } = await courtHelper.getAppeal(disputeId, roundId) + assert.equal(appealer, appealMaker, 'appeal maker does not match') + assert.equal(appealedRuling.toString(), appealMakerRuling, 'appealed ruling does not match') + assert.equal(taker.toString(), appealTaker, 'appeal taker does not match') + assert.equal(opposedRuling.toString(), appealTakerRuling, 'opposed ruling does not match') + }) - const newRoundDraftTerm = (await court.getLastEnsuredTermId()).plus(courtHelper.appealConfirmTerms) - assert.equal(draftTerm.toString(), newRoundDraftTerm.toString(), 'new round draft term does not match') - assert.equal(delayedTerms.toString(), 0, 'new round delay term does not match') + it('creates a new round for the given dispute', async () => { + await court.confirmAppeal(disputeId, roundId, appealTakerRuling, { from: appealTaker }) - const nextRoundJurorsNumber = await courtHelper.getNextRoundJurorsNumber(disputeId, roundId) - assert.equal(roundJurorsNumber.toString(), nextRoundJurorsNumber.toString(), 'new round jurors number does not match') - assert.equal(selectedJurors.toString(), 0, 'new round selected jurors number does not match') - assert.equal(triggeredBy, appealTaker, 'new round trigger does not match') - assert.equal(settledPenalties, false, 'new round penalties should not be settled') - assert.equal(collectedTokens.toString(), 0, 'new round collected tokens should be zero') - }) + const { draftTerm, delayedTerms, roundJurorsNumber, selectedJurors, triggeredBy, settledPenalties, collectedTokens } = await courtHelper.getRound(disputeId, roundId + 1) - it('does not modify the current round of the dispute', async () => { - await court.confirmAppeal(disputeId, roundId, appealTakerRuling, { from: appealTaker }) - - const { draftTerm, delayedTerms, roundJurorsNumber, selectedJurors, triggeredBy, settledPenalties, collectedTokens } = await courtHelper.getRound(disputeId, roundId) - assert.equal(draftTerm.toString(), draftTermId, 'current round draft term does not match') - assert.equal(delayedTerms.toString(), 0, 'current round delay term does not match') - assert.equal(roundJurorsNumber.toString(), jurorsNumber, 'current round jurors number does not match') - assert.equal(selectedJurors.toString(), jurorsNumber, 'current round selected jurors number does not match') - assert.equal(triggeredBy, disputer, 'current round trigger does not match') - assert.equal(settledPenalties, false, 'current round penalties should not be settled') - assert.equal(collectedTokens.toString(), 0, 'current round collected tokens should be zero') - }) + const nextRoundStartTerm = await courtHelper.getNextRoundStartTerm(disputeId, roundId) + assert.equal(draftTerm.toString(), nextRoundStartTerm.toString(), 'new round draft term does not match') + assert.equal(delayedTerms.toString(), 0, 'new round delay term does not match') + + const nextRoundJurorsNumber = await courtHelper.getNextRoundJurorsNumber(disputeId, roundId) + assert.equal(roundJurorsNumber.toString(), nextRoundJurorsNumber.toString(), 'new round jurors number does not match') + assert.equal(selectedJurors.toString(), 0, 'new round selected jurors number does not match') + assert.equal(triggeredBy, appealTaker, 'new round trigger does not match') + assert.equal(settledPenalties, false, 'new round penalties should not be settled') + assert.equal(collectedTokens.toString(), 0, 'new round collected tokens should be zero') + }) - it('updates the dispute state', async () => { - await court.confirmAppeal(disputeId, roundId, appealTakerRuling, { from: appealTaker }) + it('does not modify the current round of the dispute', async () => { + const { draftTerm: previousDraftTerm, delayedTerms: previousDelayedTerms, roundJurorsNumber: previousJurorsNumber, triggeredBy: previousTriggeredBy } = await courtHelper.getRound(disputeId, roundId) - const { possibleRulings, state, finalRuling } = await courtHelper.getDispute(disputeId) - assert.equal(state, DISPUTE_STATES.PRE_DRAFT, 'dispute state does not match') + await court.confirmAppeal(disputeId, roundId, appealTakerRuling, { from: appealTaker }) - assert.equal(possibleRulings.toString(), 2, 'dispute possible rulings do not match') - assert.equal(finalRuling.toString(), 0, 'dispute final ruling does not match') + const { draftTerm, delayedTerms, roundJurorsNumber, selectedJurors, triggeredBy, settledPenalties, collectedTokens } = await courtHelper.getRound(disputeId, roundId) + assert.equal(draftTerm.toString(), previousDraftTerm.toString(), 'current round draft term does not match') + assert.equal(delayedTerms.toString(), previousDelayedTerms.toString(), 'current round delay term does not match') + assert.equal(roundJurorsNumber.toString(), previousJurorsNumber.toString(), 'current round jurors number does not match') + assert.equal(selectedJurors.toString(), previousJurorsNumber.toString(), 'current round selected jurors number does not match') + assert.equal(triggeredBy, previousTriggeredBy, 'current round trigger does not match') + assert.equal(settledPenalties, false, 'current round penalties should not be settled') + assert.equal(collectedTokens.toString(), 0, 'current round collected tokens should be zero') + }) + + it('updates the dispute state', async () => { + await court.confirmAppeal(disputeId, roundId, appealTakerRuling, { from: appealTaker }) + + const { possibleRulings, state, finalRuling } = await courtHelper.getDispute(disputeId) + + const expectedDisputeState = (roundId < courtHelper.maxRegularAppealRounds.toNumber() - 1) ? DISPUTE_STATES.PRE_DRAFT : DISPUTE_STATES.ADJUDICATING + assert.equal(state.toString(), expectedDisputeState.toString(), 'dispute state does not match') + assert.equal(possibleRulings.toString(), 2, 'dispute possible rulings do not match') + assert.equal(finalRuling.toString(), 0, 'dispute final ruling does not match') + }) + + it('cannot be confirmed twice', async () => { + await court.confirmAppeal(disputeId, roundId, appealTakerRuling, { from: appealTaker }) + + await assertRevert(court.confirmAppeal(disputeId, roundId, appealTakerRuling, { from: appealTaker }), 'CT_INVALID_ADJUDICATION_STATE') + }) + } + + context('when the next round is a regular round', () => { + itCreatesNewRoundSuccessfully(roundId) }) - it('cannot be confirmed twice', async () => { - await court.confirmAppeal(disputeId, roundId, appealTakerRuling, { from: appealTaker }) + context('when the next round is a final round', () => { + const finalRoundId = DEFAULTS.maxRegularAppealRounds.toNumber() + + beforeEach('move to final round', async () => { + // appeal until we reach the final round, always flipping the previous round winning ruling + for (let nextRoundId = roundId + 1; nextRoundId < finalRoundId; nextRoundId++) { + await courtHelper.confirmAppeal({ disputeId, roundId: nextRoundId - 1, appealTaker, ruling: appealTakerRuling }) + const roundVoters = await courtHelper.draft({ disputeId }) + roundVoters.forEach(voter => voter.outcome = appealTakerRuling) + await courtHelper.commit({ disputeId, roundId: nextRoundId, voters: roundVoters }) + await courtHelper.reveal({ disputeId, roundId: nextRoundId, voters: roundVoters }) + await courtHelper.appeal({ disputeId, roundId: nextRoundId, appealMaker, ruling: appealMakerRuling }) + } + + // mint fee tokens for last appeal taker + const { confirmAppealDeposit } = await courtHelper.getAppealFees(disputeId, finalRoundId - 1) + await courtHelper.mintAndApproveFeeTokens(appealTaker, court.address, confirmAppealDeposit) + }) - await assertRevert(court.confirmAppeal(disputeId, roundId, appealTakerRuling, { from: appealTaker }), 'CT_INVALID_ADJUDICATION_STATE') + itCreatesNewRoundSuccessfully(finalRoundId - 1) }) }) @@ -276,7 +328,7 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju }) context('for a final round', () => { - const roundId = 3 + const roundId = DEFAULTS.maxRegularAppealRounds.toNumber() beforeEach('move to final round', async () => { await courtHelper.moveToFinalRound({ disputeId }) @@ -293,9 +345,16 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju nonVoters = filterJurors(jurors, voters) }) + const itCannotComputeNextRoundDetails = () => { + it('cannot compute next round details', async () => { + await assertRevert(court.getNextRoundDetails(disputeId, roundId), 'CT_ROUND_IS_FINAL') + }) + } + context('during commit period', () => { itIsAtState(roundId, ROUND_STATES.COMMITTING) itFailsToConfirmAppeal(roundId) + itCannotComputeNextRoundDetails() }) context('during reveal period', () => { @@ -305,6 +364,7 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju itIsAtState(roundId, ROUND_STATES.REVEALING) itFailsToConfirmAppeal(roundId) + itCannotComputeNextRoundDetails() }) context('during appeal period', () => { @@ -315,6 +375,7 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju itIsAtState(roundId, ROUND_STATES.ENDED) itFailsToConfirmAppeal(roundId) + itCannotComputeNextRoundDetails() }) context('during the appeal confirmation period', () => { @@ -326,6 +387,7 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju itIsAtState(roundId, ROUND_STATES.ENDED) itFailsToConfirmAppeal(roundId) + itCannotComputeNextRoundDetails() }) context('after the appeal confirmation period', () => { @@ -337,6 +399,7 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju itIsAtState(roundId, ROUND_STATES.ENDED) itFailsToConfirmAppeal(roundId) + itCannotComputeNextRoundDetails() }) }) }) diff --git a/test/court/court-disputes.js b/test/court/court-disputes.js index 69991571..d300947f 100644 --- a/test/court/court-disputes.js +++ b/test/court/court-disputes.js @@ -40,11 +40,10 @@ contract('Court', ([_, sender]) => { context('when the creator approves enough fee tokens', () => { const jurorFees = jurorFee.mul(jurorsNumber) const jurorRewards = (draftFee.plus(settleFee)).mul(jurorsNumber) - const requiredCollateral = jurorFees.plus(heartbeatFee).plus(jurorRewards) + const disputeFees = jurorFees.plus(heartbeatFee).plus(jurorRewards) beforeEach('approve fee amount', async () => { - await feeToken.generateTokens(sender, requiredCollateral) - await feeToken.approve(court.address, requiredCollateral, { from: sender }) + await courtHelper.mintAndApproveFeeTokens(sender, court.address, disputeFees) }) it('creates a new dispute', async () => { @@ -198,8 +197,8 @@ contract('Court', ([_, sender]) => { beforeEach('create dispute', async () => { await courtHelper.setTimestamp(firstTermStartTime) - await feeToken.generateTokens(sender, bigExp(1000, 18)) - await feeToken.approve(court.address, bigExp(1000, 18), { from: sender }) + const disputeFees = courtHelper.getDisputeFees(jurorsNumber) + await courtHelper.mintAndApproveFeeTokens(sender, court.address, disputeFees) await court.createDispute(arbitrable.address, possibleRulings, jurorsNumber, draftTermId, { from: sender }) }) @@ -229,8 +228,8 @@ contract('Court', ([_, sender]) => { beforeEach('create dispute', async () => { await courtHelper.setTimestamp(firstTermStartTime) - await feeToken.generateTokens(sender, bigExp(1000, 18)) - await feeToken.approve(court.address, bigExp(1000, 18), { from: sender }) + const disputeFees = courtHelper.getDisputeFees(jurorsNumber) + await courtHelper.mintAndApproveFeeTokens(sender, court.address, disputeFees) await court.createDispute(arbitrable.address, possibleRulings, jurorsNumber, draftTermId, { from: sender }) }) diff --git a/test/court/court-settle.js b/test/court/court-settle.js index 796c133e..402ad845 100644 --- a/test/court/court-settle.js +++ b/test/court/court-settle.js @@ -3,7 +3,7 @@ const { assertRevert } = require('@aragon/os/test/helpers/assertThrow') const { decodeEventsOfType } = require('../helpers/decodeEvent') const { filterJurors, filterWinningJurors } = require('../helpers/jurors') const { assertAmountOfEvents, assertEvent } = require('@aragon/os/test/helpers/assertEvent')(web3) -const { OUTCOMES, getVoteId, oppositeOutcome } = require('../helpers/crvoting')(web3) +const { getVoteId, oppositeOutcome, OUTCOMES } = require('../helpers/crvoting')(web3) const { buildHelper, DEFAULTS, ROUND_STATES, DISPUTE_STATES } = require('../helpers/court')(web3, artifacts) const Arbitrable = artifacts.require('Arbitrable') @@ -147,7 +147,7 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju expectedCollectedTokens = expectedCollectedTokens.plus(roundLockedBalance) } - // for final rounds all voter's tokens are collected before hand + // for final rounds all voter's tokens are collected before hand, then, add the balances of the winning jurors as well if (roundId >= courtHelper.maxRegularAppealRounds) { for (const { address } of expectedWinningJurors) { const roundLockedBalance = await courtHelper.getRoundLockBalance(disputeId, roundId, address) @@ -201,32 +201,10 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju }) it('updates the given round', async () => { - let expectedDraftTermId = draftTermId - for (let round = 0; round < roundId; round++) { - const { commitTerms, revealTerms, appealTerms, appealConfirmTerms } = courtHelper - expectedDraftTermId = commitTerms.plus(revealTerms).plus(appealTerms).plus(appealConfirmTerms).plus(expectedDraftTermId) - } - - let expectedJurorsNumber - if (roundId < courtHelper.maxRegularAppealRounds) { - expectedJurorsNumber = jurorsNumber - for (let round = 0; round < roundId; round++) { - expectedJurorsNumber = courtHelper.getNextRoundJurorsNumberFor(expectedJurorsNumber) - } - } else { - const totalActiveBalance = await courtHelper.jurorsRegistry.totalActiveBalanceAt(expectedDraftTermId) - expectedJurorsNumber = totalActiveBalance.mul(1000).div(courtHelper.jurorsMinActiveBalance).toNumber() - } - - const { draftTerm, delayedTerms, roundJurorsNumber, selectedJurors, triggeredBy, settledPenalties, collectedTokens, coherentJurors } = await courtHelper.getRound(disputeId, roundId) + const { settledPenalties, collectedTokens, coherentJurors } = await courtHelper.getRound(disputeId, roundId) assert.equal(settledPenalties, true, 'current round penalties should be settled') assert.equal(collectedTokens.toString(), expectedCollectedTokens.toString(), 'current round collected tokens does not match') assert.equal(coherentJurors.toString(), expectedCoherentJurors, 'current round coherent jurors does not match') - assert.equal(delayedTerms.toString(), 0, 'current round delay term does not match') - assert.equal(draftTerm.toString(), expectedDraftTermId, 'current round draft term does not match') - assert.equal(roundJurorsNumber.toString(), expectedJurorsNumber, 'current round jurors number does not match') - assert.equal(selectedJurors.toString(), roundId < courtHelper.maxRegularAppealRounds ? expectedJurorsNumber : 0, 'current round selected jurors number does not match') - assert.equal(triggeredBy, roundId === 0 ? disputer : appealTaker, 'current round trigger does not match') }) it('cannot be settled twice', async () => { @@ -243,7 +221,7 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju }) context('when settling in multiple batches', () => { - if (roundId < DEFAULTS.maxRegularAppealRounds) { + if (roundId < DEFAULTS.maxRegularAppealRounds.toNumber()) { beforeEach('settle penalties', async () => { const batches = expectedWinningJurors.length + expectedLosingJurors.length for (let batch = 0; batch < batches; batch++) { @@ -259,7 +237,7 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju it('reverts', async () => { await court.settlePenalties(disputeId, roundId, 1) - await assertRevert(court.settlePenalties(disputeId, roundId, 1), '') + await assertRevert(court.settlePenalties(disputeId, roundId, 1), 'CT_ROUND_ALREADY_SETTLED') }) } }) @@ -481,14 +459,14 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju itSettlesAppealDeposits(roundId, () => { it('returns the deposit to the appeal maker', async () => { const { accounting, feeToken } = courtHelper - const expectedAppealDeposit = await courtHelper.getAppealDeposit(disputeId, roundId) + const { appealDeposit } = await courtHelper.getAppealFees(disputeId, roundId) const previousBalance = await accounting.balanceOf(feeToken.address, appealMaker) await court.settleAppealDeposit(disputeId, roundId) const currentBalance = await accounting.balanceOf(feeToken.address, appealMaker) - assert.equal(previousBalance.plus(expectedAppealDeposit).toString(), currentBalance.toString(), 'appealer balances do not match') + assert.equal(previousBalance.plus(appealDeposit).toString(), currentBalance.toString(), 'appeal maker balances do not match') }) }) } @@ -497,17 +475,15 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju itSettlesAppealDeposits(roundId, () => { it('settles the total deposit to the appeal taker', async () => { const { accounting, feeToken } = courtHelper - const appealDeposit = await courtHelper.getAppealDeposit(disputeId, roundId) - const appealConfirmDeposit = await courtHelper.getConfirmAppealDeposit(disputeId, roundId) - const appealFees = await courtHelper.getAppealFees(disputeId, roundId) - const expectedAppealReward = appealDeposit.plus(appealConfirmDeposit).minus(appealFees) + const { appealFees, appealDeposit, confirmAppealDeposit } = await courtHelper.getAppealFees(disputeId, roundId) + const expectedAppealReward = appealDeposit.plus(confirmAppealDeposit).minus(appealFees) const previousAppealTakerBalance = await accounting.balanceOf(feeToken.address, appealTaker) await court.settleAppealDeposit(disputeId, roundId) const currentAppealTakerBalance = await accounting.balanceOf(feeToken.address, appealTaker) - assert.equal(previousAppealTakerBalance.plus(expectedAppealReward).toString(), currentAppealTakerBalance.toString(), 'appealer balances do not match') + assert.equal(previousAppealTakerBalance.plus(expectedAppealReward).toString(), currentAppealTakerBalance.toString(), 'appeal maker balances do not match') }) }) } @@ -516,16 +492,15 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju itSettlesAppealDeposits(roundId, () => { it('settles the total deposit to the appeal maker', async () => { const { accounting, feeToken } = courtHelper - const appealDeposit = await courtHelper.getAppealDeposit(disputeId, roundId) - const appealConfirmDeposit = await courtHelper.getConfirmAppealDeposit(disputeId, roundId) - const appealFees = await courtHelper.getAppealFees(disputeId, roundId) - const expectedAppealReward = appealDeposit.plus(appealConfirmDeposit).minus(appealFees) + const { appealFees, appealDeposit, confirmAppealDeposit } = await courtHelper.getAppealFees(disputeId, roundId) + + const expectedAppealReward = appealDeposit.plus(confirmAppealDeposit).minus(appealFees) const previousAppealMakerBalance = await accounting.balanceOf(feeToken.address, appealMaker) await court.settleAppealDeposit(disputeId, roundId) const currentAppealMakerBalance = await accounting.balanceOf(feeToken.address, appealMaker) - assert.equal(previousAppealMakerBalance.plus(expectedAppealReward).toString(), currentAppealMakerBalance.toString(), 'appealer balances do not match') + assert.equal(previousAppealMakerBalance.plus(expectedAppealReward).toString(), currentAppealMakerBalance.toString(), 'appeal maker balances do not match') }) }) } @@ -534,23 +509,21 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju itSettlesAppealDeposits(roundId, () => { it('splits the appeal deposit', async () => { const { accounting, feeToken } = courtHelper - const appealDeposit = await courtHelper.getAppealDeposit(disputeId, roundId) - const appealConfirmDeposit = await courtHelper.getConfirmAppealDeposit(disputeId, roundId) - const appealFees = await courtHelper.getAppealFees(disputeId, roundId) + const { appealFees, appealDeposit, confirmAppealDeposit } = await courtHelper.getAppealFees(disputeId, roundId) const expectedAppealMakerReward = appealDeposit.minus(appealFees.divToInt(2)) const previousAppealMakerBalance = await accounting.balanceOf(feeToken.address, appealMaker) - const expectedAppealTakerReward = appealConfirmDeposit.minus(appealFees.divToInt(2)) + const expectedAppealTakerReward = confirmAppealDeposit.minus(appealFees.divToInt(2)) const previousAppealTakerBalance = await accounting.balanceOf(feeToken.address, appealTaker) await court.settleAppealDeposit(disputeId, roundId) const currentAppealMakerBalance = await accounting.balanceOf(feeToken.address, appealMaker) - assert.equal(previousAppealMakerBalance.plus(expectedAppealMakerReward).toString(), currentAppealMakerBalance.toString(), 'appealer balances do not match') + assert.equal(previousAppealMakerBalance.plus(expectedAppealMakerReward).toString(), currentAppealMakerBalance.toString(), 'appeal maker balances do not match') const currentAppealTakerBalance = await accounting.balanceOf(feeToken.address, appealTaker) - assert.equal(previousAppealTakerBalance.plus(expectedAppealTakerReward).toString(), currentAppealTakerBalance.toString(), 'appealer balances do not match') + assert.equal(previousAppealTakerBalance.plus(expectedAppealTakerReward).toString(), currentAppealTakerBalance.toString(), 'appeal taker balances do not match') }) }) } @@ -681,7 +654,7 @@ contract('Court', ([_, disputer, drafter, appealMaker, appealTaker, juror500, ju }) context('when the next round is a final round', () => { - const finalRoundId = 3 + const finalRoundId = DEFAULTS.maxRegularAppealRounds.toNumber() const itHandlesRoundsSettlesProperly = (finalRoundVoters, expectedFinalRuling) => { const previousRoundsVoters = { [roundId]: voters } diff --git a/test/court/court-voting.js b/test/court/court-voting.js index 70bb9ea9..005b6a3b 100644 --- a/test/court/court-voting.js +++ b/test/court/court-voting.js @@ -1,9 +1,9 @@ const { bigExp } = require('../helpers/numbers')(web3) const { filterJurors } = require('../helpers/jurors') const { assertRevert } = require('@aragon/os/test/helpers/assertThrow') -const { buildHelper, ROUND_STATES } = require('../helpers/court')(web3, artifacts) +const { buildHelper, DEFAULTS, ROUND_STATES } = require('../helpers/court')(web3, artifacts) const { assertAmountOfEvents, assertEvent } = require('@aragon/os/test/helpers/assertEvent')(web3) -const { SALT, OUTCOMES, getVoteId, encryptVote, outcomeFor } = require('../helpers/crvoting')(web3) +const { getVoteId, encryptVote, outcomeFor, SALT, OUTCOMES } = require('../helpers/crvoting')(web3) contract('Court', ([_, disputer, drafter, juror500, juror1000, juror1500, juror2000, juror2500, juror3000, juror3500, juror4000]) => { let courtHelper, court, voting @@ -245,7 +245,7 @@ contract('Court', ([_, disputer, drafter, juror500, juror1000, juror1500, juror2 }) context('for a final round', () => { - const roundId = 3, poorJuror = juror500 + const roundId = DEFAULTS.maxRegularAppealRounds.toNumber(), poorJuror = juror500 beforeEach('simulate juror without enough balance to vote on a final round', async () => { const { initialActiveBalance } = jurors.find(({ address }) => address === poorJuror) diff --git a/test/helpers/court.js b/test/helpers/court.js index 3c650063..0ba6e66c 100644 --- a/test/helpers/court.js +++ b/test/helpers/court.js @@ -4,6 +4,7 @@ const { getEvents, getEventArgument } = require('@aragon/os/test/helpers/events' const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' +const PCT_BASE = 10000 const APPEAL_COLLATERAL_FACTOR = 3 const APPEAL_CONFIRMATION_COLLATERAL_FACTOR = 2 @@ -49,6 +50,7 @@ module.exports = (web3, artifacts) => { subscriptionPrePaymentPeriods: bn(0), // none subscription pre payment period subscriptionLatePaymentPenaltyPct: bn(0), // none subscription late payment penalties subscriptionGovernorSharePct: bn(0), // none subscription governor shares + finalRoundWeightPrecision: bn(1000), // use to improve division rounding for final round maths } class CourtHelper { @@ -57,6 +59,13 @@ module.exports = (web3, artifacts) => { this.artifacts = artifacts } + getDisputeFees(jurorsNumber) { + const jurorFees = this.jurorFee.mul(jurorsNumber) + const draftFees = this.draftFee.mul(jurorsNumber) + const settleFees = this.settleFee.mul(jurorsNumber) + return this.heartbeatFee.plus(jurorFees).plus(draftFees).plus(settleFees) + } + async getDispute(disputeId) { const [subject, possibleRulings, state, finalRuling, lastRoundId] = await this.court.getDispute(disputeId) return { subject, possibleRulings, state, finalRuling, lastRoundId } @@ -72,6 +81,49 @@ module.exports = (web3, artifacts) => { return { appealer, appealedRuling, taker, opposedRuling } } + async getNextRoundJurorsNumber(disputeId, roundId) { + if (roundId < this.maxRegularAppealRounds.toNumber() - 1) { + const { roundJurorsNumber } = await this.getRound(disputeId, roundId) + let nextRoundJurorsNumber = this.appealStepFactor.mul(roundJurorsNumber).toNumber() + if (nextRoundJurorsNumber % 2 === 0) nextRoundJurorsNumber++ + return nextRoundJurorsNumber + } else { + const finalRoundStartTerm = await this.getNextRoundStartTerm(disputeId, roundId) + const totalActiveBalance = await this.jurorsRegistry.totalActiveBalanceAt(finalRoundStartTerm) + return totalActiveBalance.mul(this.finalRoundWeightPrecision).div(this.jurorsMinActiveBalance).toNumber() + } + } + + async getNextRoundJurorFees(disputeId, roundId) { + const jurorsNumber = await this.getNextRoundJurorsNumber(disputeId, roundId) + let jurorFees = this.jurorFee.mul(jurorsNumber) + if (roundId >= this.maxRegularAppealRounds.toNumber() - 1) { + jurorFees = jurorFees.div(this.finalRoundWeightPrecision).mul(this.finalRoundReduction).div(PCT_BASE) + } + return jurorFees + } + + async getAppealFees(disputeId, roundId) { + const nextRoundJurorsNumber = await this.getNextRoundJurorsNumber(disputeId, roundId) + const jurorFees = await this.getNextRoundJurorFees(disputeId, roundId) + let appealFees = this.heartbeatFee.plus(jurorFees) + + if (roundId < this.maxRegularAppealRounds.toNumber() - 1) { + const draftFees = this.draftFee.mul(nextRoundJurorsNumber) + const settleFees = this.settleFee.mul(nextRoundJurorsNumber) + appealFees = appealFees.add(draftFees).add(settleFees) + } + + const appealDeposit = appealFees.mul(APPEAL_COLLATERAL_FACTOR) + const confirmAppealDeposit = appealFees.mul(APPEAL_CONFIRMATION_COLLATERAL_FACTOR) + return { appealFees , appealDeposit, confirmAppealDeposit } + } + + async getNextRoundStartTerm(disputeId, roundId) { + const { draftTerm } = await this.getRound(disputeId, roundId) + return draftTerm.plus(this.commitTerms).plus(this.revealTerms).plus(this.appealTerms).plus(this.appealConfirmTerms) + } + async getRoundJuror(disputeId, roundId, juror) { const [weight, rewarded] = await this.court.getJuror(disputeId, roundId, juror) return { weight, rewarded } @@ -79,14 +131,14 @@ module.exports = (web3, artifacts) => { async getRoundLockBalance(disputeId, roundId, juror) { if (roundId < this.maxRegularAppealRounds) { - const lockPerDraft = this.jurorsMinActiveBalance.mul(this.penaltyPct).div(10000) + const lockPerDraft = this.jurorsMinActiveBalance.mul(this.penaltyPct).div(PCT_BASE) const { weight } = await this.getRoundJuror(disputeId, roundId, juror) return lockPerDraft.mul(weight) } else { const { draftTerm } = await this.getRound(disputeId, roundId) const draftActiveBalance = await this.jurorsRegistry.activeBalanceOfAt(juror, draftTerm) if (draftActiveBalance.lt(this.jurorsMinActiveBalance)) return bn(0) - return draftActiveBalance.mul(this.penaltyPct).div(10000) + return draftActiveBalance.mul(this.penaltyPct).div(PCT_BASE) } } @@ -94,40 +146,7 @@ module.exports = (web3, artifacts) => { const { draftTerm } = await this.getRound(disputeId, roundId) const draftActiveBalance = await this.jurorsRegistry.activeBalanceOfAt(juror, draftTerm) if (draftActiveBalance.lt(this.jurorsMinActiveBalance)) return bn(0) - return draftActiveBalance.mul(1000).divToInt(this.jurorsMinActiveBalance) - } - - async getAppealFees(disputeId, roundId) { - const nextRoundJurorsNumber = await this.getNextRoundJurorsNumber(disputeId, roundId) - return this.getDraftDepositFor(nextRoundJurorsNumber) - } - - async getAppealDeposit(disputeId, roundId) { - const totalFees = await this.getAppealFees(disputeId, roundId) - return totalFees.mul(APPEAL_COLLATERAL_FACTOR) - } - - async getConfirmAppealDeposit(disputeId, roundId) { - const totalFees = await this.getAppealFees(disputeId, roundId) - return totalFees.mul(APPEAL_CONFIRMATION_COLLATERAL_FACTOR) - } - - async getNextRoundJurorsNumber(disputeId, roundId) { - const { roundJurorsNumber } = await this.getRound(disputeId, roundId) - return this.getNextRoundJurorsNumberFor(roundJurorsNumber) - } - - getNextRoundJurorsNumberFor(jurorsNumber) { - let nextRoundJurorsNumber = this.appealStepFactor.mul(jurorsNumber).toNumber() - if (nextRoundJurorsNumber % 2 === 0) nextRoundJurorsNumber++ - return nextRoundJurorsNumber - } - - getDraftDepositFor(jurorsNumber) { - const jurorFees = this.jurorFee.mul(jurorsNumber) - const draftFees = this.draftFee.mul(jurorsNumber) - const settleFees = this.settleFee.mul(jurorsNumber) - return this.heartbeatFee.plus(jurorFees).plus(draftFees).plus(settleFees) + return draftActiveBalance.mul(this.finalRoundWeightPrecision).divToInt(this.jurorsMinActiveBalance) } async setTimestamp(timestamp) { @@ -171,12 +190,14 @@ module.exports = (web3, artifacts) => { await advanceBlocks(2) } - async mintFeeTokens(address, amount = bigExp(1e6, 18)) { - const allowance = await this.feeToken.allowance(address, this.court.address) - if (allowance.gt(0)) await this.feeToken.approve(this.court.address, 0, { from: address }) + async mintAndApproveFeeTokens(from, to, amount) { + // reset allowance in case allowed address has already been approved some balance + const allowance = await this.feeToken.allowance(from, to) + if (allowance.gt(0)) await this.feeToken.approve(to, 0, { from }) - await this.feeToken.generateTokens(address, amount) - await this.feeToken.approve(this.court.address, amount, { from: address }) + // mint and approve tokens + await this.feeToken.generateTokens(from, amount) + await this.feeToken.approve(to, amount, { from }) } async activate(jurors) { @@ -191,7 +212,8 @@ module.exports = (web3, artifacts) => { async dispute({ jurorsNumber, draftTermId, possibleRulings = 2, arbitrable = undefined, disputer = undefined }) { // mint enough fee tokens for the disputer, if no disputer was given pick the second account if (!disputer) disputer = web3.eth.accounts[1] - await this.mintFeeTokens(disputer) + const disputeFees = this.getDisputeFees(jurorsNumber) + await this.mintAndApproveFeeTokens(disputer, this.court.address, disputeFees) // create an arbitrable if no one was given, and mock subscriptions if (!arbitrable) arbitrable = await this.artifacts.require('ArbitrableMock').new() @@ -264,7 +286,8 @@ module.exports = (web3, artifacts) => { async appeal({ disputeId, roundId, appealMaker = undefined, ruling = undefined }) { // mint fee tokens for the appealer, if no appealer was given pick the fourth account if (!appealMaker) appealMaker = web3.eth.accounts[3] - await this.mintFeeTokens(appealMaker) + const { appealDeposit } = await this.getAppealFees(disputeId, roundId) + await this.mintAndApproveFeeTokens(appealMaker, this.court.address, appealDeposit) // use the opposite to the round winning ruling for the appeal if no one was given if (!ruling) { @@ -281,7 +304,8 @@ module.exports = (web3, artifacts) => { async confirmAppeal({ disputeId, roundId, appealTaker = undefined, ruling = undefined }) { // mint fee tokens for the appeal taker, if no taker was given pick the fifth account if (!appealTaker) appealTaker = web3.eth.accounts[4] - await this.mintFeeTokens(appealTaker) + const { confirmAppealDeposit } = await this.getAppealFees(disputeId, roundId) + await this.mintAndApproveFeeTokens(appealTaker, this.court.address, confirmAppealDeposit) // use the opposite ruling the one appealed if no one was given if (!ruling) {