diff --git a/contracts/Court.sol b/contracts/Court.sol index 6c8c1e8c..48485bf3 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -21,7 +21,6 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner, ISubscriptions using SafeMath for uint256; uint256 internal constant MAX_JURORS_PER_BATCH = 10; // to cap gas used on draft - uint256 internal constant MAX_REGULAR_APPEAL_ROUNDS = 4; // before the final appeal uint256 internal constant FINAL_ROUND_WEIGHT_PRECISION = 1000; // to improve roundings uint32 internal constant APPEAL_STEP_FACTOR = 3; // TODO: move all other constants up here @@ -96,6 +95,7 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner, ISubscriptions IArbitrable subject; uint8 possibleRulings; // number of possible rulings the court can decide on uint8 winningRuling; + uint32 maxRegularAppealRounds; // before the final appeal DisputeState state; AdjudicationRound[] rounds; } @@ -115,6 +115,7 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner, ISubscriptions // Court state uint64 public termId; uint64 public configChangeTermId; + uint32 public maxRegularAppealRounds; // before the final appeal mapping (address => Account) internal accounts; mapping (uint256 => address) public jurorsByTreeId; mapping (uint64 => Term) public terms; @@ -124,6 +125,7 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner, ISubscriptions string internal constant ERROR_DEPOSIT_FAILED = "COURT_DEPOSIT_FAILED"; string internal constant ERROR_ZERO_TRANSFER = "COURT_ZERO_TRANSFER"; string internal constant ERROR_TOO_MANY_TRANSITIONS = "COURT_TOO_MANY_TRANSITIONS"; + string internal constant ERROR_ZERO_MAX_ROUNDS = "COURT_ZERO_MAX_ROUNDS"; string internal constant ERROR_UNFINISHED_TERM = "COURT_UNFINISHED_TERM"; string internal constant ERROR_PAST_TERM_FEE_CHANGE = "COURT_PAST_TERM_FEE_CHANGE"; string internal constant ERROR_INVALID_ACCOUNT_STATE = "COURT_INVALID_ACCOUNT_STATE"; @@ -229,6 +231,7 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner, ISubscriptions uint64[3] _roundStateDurations, uint16 _penaltyPct, uint16 _finalRoundReduction, + uint32 _maxRegularAppealRounds, uint256[5] _subscriptionParams // _periodDuration, _feeAmount, _prePaymentPeriods, _latePaymentPenaltyPct, _governorSharePct ) public { termDuration = _termDuration; @@ -238,6 +241,7 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner, ISubscriptions subscriptions = _subscriptions; jurorMinStake = _jurorMinStake; governor = _governor; + setMaxRegularAppealRounds(_maxRegularAppealRounds); voting.setOwner(ICRVotingOwner(this)); sumTree.init(address(this)); @@ -412,6 +416,7 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner, ISubscriptions Dispute storage dispute = disputes[disputeId]; dispute.subject = _subject; dispute.possibleRulings = _possibleRulings; + dispute.maxRegularAppealRounds = maxRegularAppealRounds; // _newAdjudicationRound charges fees for starting the round _newAdjudicationRound(disputeId, _jurorNumber, _draftTermId); @@ -525,7 +530,7 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner, ISubscriptions uint64 appealDraftTermId = termId + 1; // Appeals are drafted in the next term uint256 roundId; - if (_roundId == MAX_REGULAR_APPEAL_ROUNDS - 1) { // final round, roundId starts at 0 + if (_roundId == dispute.maxRegularAppealRounds - 1) { // final round, roundId starts at 0 // number of jurors will be the number of times the minimum stake is hold in the tree, multiplied by a precision factor for division roundings (roundId, appealJurorNumber) = _newFinalAdjudicationRound(_disputeId, appealDraftTermId); } else { // no need for more checks, as final appeal won't ever be in Appealable state, so it would never reach here (first check would fail) @@ -588,7 +593,7 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner, ISubscriptions round.coherentJurors = uint64(coherentJurors); uint256 collectedTokens; - if (_roundId < MAX_REGULAR_APPEAL_ROUNDS) { + if (_roundId < dispute.maxRegularAppealRounds) { collectedTokens = _settleRegularRoundSlashing(round, voteId, config.penaltyPct, winningRuling); round.collectedTokens = collectedTokens; _assignTokens(config.feeToken, msg.sender, config.settleFee * round.jurorNumber); @@ -604,7 +609,7 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner, ISubscriptions if (coherentJurors == 0) { // refund fees and burn ANJ uint256 jurorFee = config.jurorFee * round.jurorNumber; - if (_roundId == MAX_REGULAR_APPEAL_ROUNDS) { + if (_roundId == dispute.maxRegularAppealRounds) { // number of jurors will in the final round is multiplied by a precision factor for division roundings, so we need to undo it here // besides, we account for the final round discount in fees jurorFee = _pct4(jurorFee / FINAL_ROUND_WEIGHT_PRECISION, config.finalRoundReduction); @@ -681,7 +686,7 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner, ISubscriptions CourtConfig storage config = courtConfigs[terms[round.draftTermId].courtConfigId]; // safe to use directly as it is a past term uint256 jurorFee = config.jurorFee * jurorState.weight * round.jurorNumber / coherentJurors; - if (_roundId == MAX_REGULAR_APPEAL_ROUNDS) { + if (_roundId == dispute.maxRegularAppealRounds) { // number of jurors will in the final round is multiplied by a precision factor for division roundings, so we need to undo it here // besides, we account for the final round discount in fees jurorFee = _pct4(jurorFee / FINAL_ROUND_WEIGHT_PRECISION, config.finalRoundReduction); @@ -725,6 +730,11 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner, ISubscriptions feeAmount = fees.heartbeatFee + _jurorNumber * (fees.jurorFee + fees.draftFee + fees.settleFee); } + function setMaxRegularAppealRounds(uint32 _maxRegularAppealRounds) public { + require(_maxRegularAppealRounds > 0, ERROR_ZERO_MAX_ROUNDS); + maxRegularAppealRounds = _maxRegularAppealRounds; + } + /** * @dev Callback of approveAndCall, allows staking directly with a transaction to the token contract. * @param _from The address making the transfer. @@ -779,7 +789,7 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner, ISubscriptions (uint256 disputeId, uint256 roundId) = _decodeVoteId(_voteId); // for the final round - if (roundId == MAX_REGULAR_APPEAL_ROUNDS) { + if (roundId == disputes[disputeId].maxRegularAppealRounds) { return _canCommitFinalRound(disputeId, roundId, _voter); } @@ -896,6 +906,10 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner, ISubscriptions return governor; } + function getMaxRegularAppealRounds(uint256 _disputeId) public view returns (uint32) { + return disputes[_disputeId].maxRegularAppealRounds; + } + function _newAdjudicationRound( uint256 _disputeId, uint64 _jurorNumber, @@ -974,7 +988,8 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner, ISubscriptions } function _adjudicationStateAtTerm(uint256 _disputeId, uint256 _roundId, uint64 _termId) internal view returns (AdjudicationState) { - AdjudicationRound storage round = disputes[_disputeId].rounds[_roundId]; + Dispute storage dispute = disputes[_disputeId]; + AdjudicationRound storage round = dispute.rounds[_roundId]; // we use the config for the original draft term and only use the delay for the timing of the rounds uint64 draftTermId = round.draftTermId; @@ -992,7 +1007,7 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner, ISubscriptions return AdjudicationState.Commit; } else if (_termId < appealStart) { return AdjudicationState.Reveal; - } else if (_termId < appealEnd && _roundId < MAX_REGULAR_APPEAL_ROUNDS) { + } else if (_termId < appealEnd && _roundId < dispute.maxRegularAppealRounds) { return AdjudicationState.Appealable; } else { return AdjudicationState.Ended; diff --git a/contracts/test/CourtMock.sol b/contracts/test/CourtMock.sol index b83b84be..8e30b5a6 100644 --- a/contracts/test/CourtMock.sol +++ b/contracts/test/CourtMock.sol @@ -22,6 +22,7 @@ contract CourtMock is Court { uint64[3] _roundStateDurations, uint16 _penaltyPct, uint16 _finalRoundReduction, + uint32 _maxRegularAppealRounds, uint256[5] _subscriptionParams // _periodDuration, _feeAmount, _prePaymentPeriods, _latePaymentPenaltyPct, _governorSharePct ) Court( _termDuration, @@ -37,6 +38,7 @@ contract CourtMock is Court { _roundStateDurations, _penaltyPct, _finalRoundReduction, + _maxRegularAppealRounds, _subscriptionParams ) public {} @@ -113,10 +115,6 @@ contract CourtMock is Court { return MAX_JURORS_PER_BATCH; } - function getMaxRegularAppealRounds() public pure returns (uint256) { - return MAX_REGULAR_APPEAL_ROUNDS; - } - function getAppealStepFactor() public pure returns (uint32) { return APPEAL_STEP_FACTOR; } diff --git a/test/court-batches.js b/test/court-batches.js index ef4d6d97..d88d5230 100644 --- a/test/court-batches.js +++ b/test/court-batches.js @@ -86,6 +86,7 @@ contract('Court: Batches', ([ rich, governor, arbitrable, juror1, juror2, juror3 [ commitTerms, appealTerms, revealTerms ], penaltyPct, finalRoundReduction, + 4, [ 0, 0, 0, 0, 0 ] ) diff --git a/test/court-disputes.js b/test/court-disputes.js index 99c4e849..ab698c2d 100644 --- a/test/court-disputes.js +++ b/test/court-disputes.js @@ -102,6 +102,7 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arb [ commitTerms, appealTerms, revealTerms ], penaltyPct, finalRoundReduction, + 4, [ 0, 0, 0, 0, 0 ] ) diff --git a/test/court-final-appeal-non-exact.js b/test/court-final-appeal-non-exact.js index 8b231735..64fdcb14 100644 --- a/test/court-final-appeal-non-exact.js +++ b/test/court-final-appeal-non-exact.js @@ -40,7 +40,6 @@ contract('Court: final appeal (non-exact)', ([ poor, rich, governor, juror1, jur const jurors = [juror1, juror2, juror3] const NO_DATA = '' const ZERO_ADDRESS = '0x' + '00'.repeat(20) - let MAX_REGULAR_APPEAL_ROUNDS let APPEAL_STEP_FACTOR const DECIMALS = 1e18 @@ -106,10 +105,10 @@ contract('Court: final appeal (non-exact)', ([ poor, rich, governor, juror1, jur [ commitTerms, appealTerms, revealTerms ], penaltyPct, finalRoundReduction, + 4, [ 0, 0, 0, 0, 0 ] ) - MAX_REGULAR_APPEAL_ROUNDS = (await this.court.getMaxRegularAppealRounds.call()).toNumber() APPEAL_STEP_FACTOR = (await this.court.getAppealStepFactor.call()).toNumber() await this.court.mock_hijackTreeSearch() @@ -207,7 +206,9 @@ contract('Court: final appeal (non-exact)', ([ poor, rich, governor, juror1, jur const initialJurorNumber = 3 - for (let roundId = 0; roundId < MAX_REGULAR_APPEAL_ROUNDS; roundId++) { + const maxRegularAppealRounds = (await this.court.getMaxRegularAppealRounds.call(disputeId)).toNumber() + + for (let roundId = 0; roundId < maxRegularAppealRounds; roundId++) { let roundJurors = initialJurorNumber * (APPEAL_STEP_FACTOR ** roundId) if (roundJurors % 2 == 0) { roundJurors++ @@ -225,6 +226,8 @@ contract('Court: final appeal (non-exact)', ([ poor, rich, governor, juror1, jur await passTerms(appealTerms) await this.court.mock_blockTravel(1) } + + return maxRegularAppealRounds } context('Rewards and slashes', () => { @@ -232,13 +235,13 @@ contract('Court: final appeal (non-exact)', ([ poor, rich, governor, juror1, jur const weight = jurorGenericStake / jurorMinStake const testFinalRound = async (_winningJurors) => { - await moveForwardToFinalRound() + const maxRegularAppealRounds = await moveForwardToFinalRound() // final round await vote(voteId, _winningJurors) // settle - for (let roundId = 0; roundId <= MAX_REGULAR_APPEAL_ROUNDS; roundId++) { + for (let roundId = 0; roundId <= maxRegularAppealRounds; roundId++) { const receiptPromise = this.court.settleRoundSlashing(disputeId, roundId) await assertLogs(receiptPromise, ROUND_SLASHING_SETTLED_EVENT) } @@ -247,7 +250,7 @@ contract('Court: final appeal (non-exact)', ([ poor, rich, governor, juror1, jur for (let i = 0; i < _winningJurors; i++) { const tokenBalance = (await this.anj.balanceOf(jurors[i])).toNumber() const courtBalance = (await this.court.totalStakedFor(jurors[i])).toNumber() - const receiptPromise = this.court.settleReward(disputeId, MAX_REGULAR_APPEAL_ROUNDS, jurors[i]) + const receiptPromise = this.court.settleReward(disputeId, maxRegularAppealRounds, jurors[i]) await assertLogs(receiptPromise, REWARD_SETTLED_EVENT) // as jurors are not withdrawing here, real token balance shouldn't change diff --git a/test/court-final-appeal.js b/test/court-final-appeal.js index 303580c2..2eff0679 100644 --- a/test/court-final-appeal.js +++ b/test/court-final-appeal.js @@ -41,8 +41,8 @@ contract('Court: final appeal', ([ poor, rich, governor, juror1, juror2, juror3, const NO_DATA = '' const ZERO_ADDRESS = '0x' + '00'.repeat(20) let MAX_JURORS_PER_BATCH - let MAX_REGULAR_APPEAL_ROUNDS let APPEAL_STEP_FACTOR + const MAX_REGULAR_APPEAL_ROUNDS = 4 const termDuration = 10 const firstTermStart = 1 @@ -69,6 +69,7 @@ contract('Court: final appeal', ([ poor, rich, governor, juror1, juror2, juror3, const ROUND_SLASHING_SETTLED_EVENT = 'RoundSlashingSettled' const REWARD_SETTLED_EVENT = 'RewardSettled' + const ERROR_ZERO_MAX_ROUNDS = 'COURT_ZERO_MAX_ROUNDS' const ERROR_INVALID_ADJUDICATION_STATE = 'COURT_INVALID_ADJUDICATION_STATE' const ERROR_INVALID_ADJUDICATION_ROUND = 'COURT_INVALID_ADJUDICATION_ROUND' const ERROR_FINAL_ROUNDS_PENDING = 'COURT_FINAL_ROUNDS_PENDING' @@ -112,11 +113,11 @@ contract('Court: final appeal', ([ poor, rich, governor, juror1, juror2, juror3, [ commitTerms, appealTerms, revealTerms ], penaltyPct, finalRoundReduction, + MAX_REGULAR_APPEAL_ROUNDS, [ 0, 0, 0, 0, 0 ] ) MAX_JURORS_PER_BATCH = (await this.court.getMaxJurorsPerBatch.call()).toNumber() - MAX_REGULAR_APPEAL_ROUNDS = (await this.court.getMaxRegularAppealRounds.call()).toNumber() APPEAL_STEP_FACTOR = (await this.court.getAppealStepFactor.call()).toNumber() await this.court.mock_setBlockNumber(startBlock) @@ -145,6 +146,30 @@ contract('Court: final appeal', ([ poor, rich, governor, juror1, juror2, juror3, assert.isFalse(await this.court.canTransitionTerm(), 'all terms transitioned') } + context('Max number of regular appeals', () => { + it('Can change number of regular appeals', async () => { + const newMaxAppeals = MAX_REGULAR_APPEAL_ROUNDS + 1 + // set new max + await this.court.setMaxRegularAppealRounds(newMaxAppeals) + + // create dispute + const arbitrable = poor // it doesn't matter, just an address + const jurorNumber = 3 + const term = 3 + const rulings = 2 + const receipt = await this.court.createDispute(arbitrable, rulings, jurorNumber, term) + await assertLogs(receipt, NEW_DISPUTE_EVENT) + const disputeId = getLog(receipt, NEW_DISPUTE_EVENT, 'disputeId') + + assertEqualBN(this.court.getMaxRegularAppealRounds(disputeId), newMaxAppeals, 'Max appeals number should macth') + }) + + it('Fails trying to change number of regular appeals to zero', async () => { + // set new max + await assertRevert(this.court.setMaxRegularAppealRounds(0), ERROR_ZERO_MAX_ROUNDS) + }) + }) + context('Final appeal', () => { const jurorNumber = 3 @@ -185,7 +210,9 @@ contract('Court: final appeal', ([ poor, rich, governor, juror1, juror2, juror3, const initialJurorNumber = 3 - for (let roundId = 0; roundId < MAX_REGULAR_APPEAL_ROUNDS; roundId++) { + const maxRegularAppealRounds = (await this.court.getMaxRegularAppealRounds.call(disputeId)).toNumber() + + for (let roundId = 0; roundId < maxRegularAppealRounds; roundId++) { let roundJurors = initialJurorNumber * (APPEAL_STEP_FACTOR ** roundId) if (roundJurors % 2 == 0) { roundJurors++ @@ -207,6 +234,8 @@ contract('Court: final appeal', ([ poor, rich, governor, juror1, juror2, juror3, await passTerms(appealTerms) await this.court.mock_blockTravel(1) } + + return maxRegularAppealRounds } it('reaches final appeal, all jurors can vote', async () => { @@ -219,7 +248,7 @@ contract('Court: final appeal', ([ poor, rich, governor, juror1, juror2, juror3, }) it('fails appealing after final appeal', async () => { - await moveForwardToFinalRound() + const maxRegularAppealRounds = await moveForwardToFinalRound() const roundJurors = (await this.sumTree.getNextKey()).toNumber() - 1 // no need to draft (as it's all jurors) @@ -231,7 +260,7 @@ contract('Court: final appeal', ([ poor, rich, governor, juror1, juror2, juror3, await passTerms(revealTerms) // appeal - await assertRevert(this.court.appealRuling(disputeId, MAX_REGULAR_APPEAL_ROUNDS), ERROR_INVALID_ADJUDICATION_STATE) + await assertRevert(this.court.appealRuling(disputeId, maxRegularAppealRounds), ERROR_INVALID_ADJUDICATION_STATE) }) context('Rewards and slashes', () => { @@ -241,8 +270,10 @@ contract('Court: final appeal', ([ poor, rich, governor, juror1, juror2, juror3, // more than half of the jurors voting first option const winningJurors = Math.floor(jurors.length / 2) + 1 + let maxRegularAppealRounds + beforeEach(async () => { - await moveForwardToFinalRound() + maxRegularAppealRounds = await moveForwardToFinalRound() // vote const winningVote = 2 const losingVote = 3 @@ -272,7 +303,7 @@ contract('Court: final appeal', ([ poor, rich, governor, juror1, juror2, juror3, await passTerms(revealTerms) // settle - for (let roundId = 0; roundId <= MAX_REGULAR_APPEAL_ROUNDS; roundId++) { + for (let roundId = 0; roundId <= maxRegularAppealRounds; roundId++) { const receiptPromise = this.court.settleRoundSlashing(disputeId, roundId) await assertLogs(receiptPromise, ROUND_SLASHING_SETTLED_EVENT) } @@ -282,7 +313,7 @@ contract('Court: final appeal', ([ poor, rich, governor, juror1, juror2, juror3, for (let i = 0; i < winningJurors; i++) { const tokenBalance = (await this.anj.balanceOf(jurors[i])).toNumber() const courtBalance = (await this.court.totalStakedFor(jurors[i])).toNumber() - const receiptPromise = this.court.settleReward(disputeId, MAX_REGULAR_APPEAL_ROUNDS, jurors[i], { from: jurors[i] }) + const receiptPromise = this.court.settleReward(disputeId, maxRegularAppealRounds, jurors[i], { from: jurors[i] }) await assertLogs(receiptPromise, REWARD_SETTLED_EVENT) // as jurors are not withdrawing here, real token balance shouldn't change diff --git a/test/court-lifecycle.js b/test/court-lifecycle.js index d5691143..6c960a0a 100644 --- a/test/court-lifecycle.js +++ b/test/court-lifecycle.js @@ -83,6 +83,7 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { [ commitTerms, appealTerms, revealTerms ], penaltyPct, finalRoundReduction, + 4, [ 0, 0, 0, 0, 0 ] ) diff --git a/test/court-staking.js b/test/court-staking.js index cd1e1fce..16c0cee6 100644 --- a/test/court-staking.js +++ b/test/court-staking.js @@ -60,6 +60,7 @@ contract('Court: Staking', ([ pleb, rich ]) => { [ 1, 1, 1 ], 1, finalRoundReduction, + 4, [ 0, 0, 0, 0, 0 ] ) })