Skip to content

Commit

Permalink
Court: Make max number of appeal rounds configurable
Browse files Browse the repository at this point in the history
Closes #55.
  • Loading branch information
ßingen committed Jul 8, 2019
1 parent e72a03a commit fb2154f
Show file tree
Hide file tree
Showing 8 changed files with 71 additions and 26 deletions.
31 changes: 23 additions & 8 deletions contracts/Court.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand All @@ -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";
Expand Down Expand Up @@ -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;
Expand All @@ -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));
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
6 changes: 2 additions & 4 deletions contracts/test/CourtMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -37,6 +38,7 @@ contract CourtMock is Court {
_roundStateDurations,
_penaltyPct,
_finalRoundReduction,
_maxRegularAppealRounds,
_subscriptionParams
) public {}

Expand Down Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions test/court-batches.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ contract('Court: Batches', ([ rich, governor, arbitrable, juror1, juror2, juror3
[ commitTerms, appealTerms, revealTerms ],
penaltyPct,
finalRoundReduction,
4,
[ 0, 0, 0, 0, 0 ]
)

Expand Down
1 change: 1 addition & 0 deletions test/court-disputes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
)

Expand Down
15 changes: 9 additions & 6 deletions test/court-final-appeal-non-exact.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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++
Expand All @@ -225,20 +226,22 @@ 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', () => {
const penalty = jurorMinStake * penaltyPct / 10000
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)
}
Expand All @@ -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
Expand Down
41 changes: 33 additions & 8 deletions test/court-final-appeal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -112,11 +112,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)
Expand Down Expand Up @@ -145,6 +145,25 @@ 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')
})
})

context('Final appeal', () => {

const jurorNumber = 3
Expand Down Expand Up @@ -185,7 +204,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++
Expand All @@ -207,6 +228,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 () => {
Expand All @@ -219,7 +242,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)
Expand All @@ -231,7 +254,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', () => {
Expand All @@ -241,8 +264,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
Expand Down Expand Up @@ -272,7 +297,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)
}
Expand All @@ -282,7 +307,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
Expand Down
1 change: 1 addition & 0 deletions test/court-lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => {
[ commitTerms, appealTerms, revealTerms ],
penaltyPct,
finalRoundReduction,
4,
[ 0, 0, 0, 0, 0 ]
)

Expand Down
1 change: 1 addition & 0 deletions test/court-staking.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ contract('Court: Staking', ([ pleb, rich ]) => {
[ 1, 1, 1 ],
1,
finalRoundReduction,
4,
[ 0, 0, 0, 0, 0 ]
)
})
Expand Down

0 comments on commit fb2154f

Please sign in to comment.