Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Final appeal round #45

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
266 changes: 216 additions & 50 deletions contracts/Court.sol

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions contracts/HexSumTreeWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ contract HexSumTreeWrapper {
return tree.getItem(_key);
}

function getItemPast(uint256 _key, uint64 _checkpointTime) external view returns (uint256) {
return tree.getItemPast(_key, _checkpointTime);
}

function totalSumPresent(uint64 _checkpointTime) external view returns (uint256) {
return tree.totalSumPresent(_checkpointTime);
}
Expand Down
1 change: 1 addition & 0 deletions contracts/standards/sumtree/ISumTree.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface ISumTree {
function update(uint256 _key, uint64 _checkpointTime, uint256 _delta, bool _positive) external;

function getItem(uint256 _key) external view returns (uint256);
function getItemPast(uint256 _key, uint64 _checkpointTime) external view returns (uint256);

function totalSumPresent(uint64 _checkpointTime) external view returns (uint256);

Expand Down
22 changes: 22 additions & 0 deletions contracts/test/CourtMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ contract CourtMock is Court {
uint256 _jurorMinStake,
uint64[3] _roundStateDurations,
uint16 _penaltyPct
// TODO: stack too deep:
//uint16 _finalRoundReduction
) Court(
_termDuration,
_jurorToken,
Expand All @@ -42,6 +44,8 @@ contract CourtMock is Court {
_jurorMinStake,
_roundStateDurations,
_penaltyPct
// TODO: stack too deep:
//_finalRoundReduction
) public {}

function mock_setTime(uint64 time) external {
Expand All @@ -64,6 +68,20 @@ contract CourtMock is Court {
treeSearchHijacked = true;
}

function executeRuling(uint256 _disputeId, uint256 _roundId) external ensureTerm {
// checks that dispute is in adjudication state
_checkAdjudicationState(_disputeId, _roundId, AdjudicationState.Ended);

Dispute storage dispute = disputes[_disputeId];
dispute.state = DisputeState.Executed;

uint8 winningRuling = dispute.rounds[_roundId].winningRuling;

//dispute.subject.rule(_disputeId, uint256(winningRuling));

emit RulingExecuted(_disputeId, winningRuling);
}

function _treeSearch(
bytes32 _termRandomness,
uint256 _disputeId,
Expand Down Expand Up @@ -102,6 +120,10 @@ contract CourtMock is Court {
return MAX_JURORS_PER_BATCH;
}

function getMaxDraftRounds() public pure returns (uint256) {
return MAX_DRAFT_ROUNDS;
}

function getAdjudicationState(uint256 _disputeId, uint256 _roundId, uint64 _termId) public view returns (AdjudicationState) {
return _adjudicationStateAtTerm(_disputeId, _roundId, _termId);
}
Expand Down
10 changes: 10 additions & 0 deletions test/court-disputes.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arb
const VOTE_COMMITTED_EVENT = 'VoteCommitted'
const VOTE_REVEALED_EVENT = 'VoteRevealed'
const RULING_APPEALED_EVENT = 'RulingAppealed'
const RULING_EXECUTED_EVENT = 'RulingExecuted'
const ROUND_SLASHING_SETTLED_EVENT = 'RoundSlashingSettled'

const SALT = soliditySha3('passw0rd')
Expand Down Expand Up @@ -269,6 +270,15 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arb
await assertRevert(this.court.appealRuling(disputeId, firstRoundId + 1), 'COURT_INVALID_ADJUDICATION_ROUND')
})

it('can settle if executed', async () => {
await passTerms(2) // term = 6
// execute
const executeReceiptPromise = await this.court.executeRuling(disputeId, firstRoundId)
await assertLogs(executeReceiptPromise, RULING_EXECUTED_EVENT)
// settle
await assertLogs(this.court.settleRoundSlashing(disputeId, firstRoundId), ROUND_SLASHING_SETTLED_EVENT)
})

context('settling round', () => {
const slashed = pct4(jurorMinStake, penaltyPct)

Expand Down
285 changes: 285 additions & 0 deletions test/court-final-appeal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
const { assertRevert } = require('@aragon/os/test/helpers/assertThrow')
const { soliditySha3 } = require('web3-utils')

const TokenFactory = artifacts.require('TokenFactory')
const CourtMock = artifacts.require('CourtMock')
const CRVoting = artifacts.require('CRVoting')
const SumTree = artifacts.require('HexSumTreeWrapper')

const MINIME = 'MiniMeToken'

const getLog = (receipt, logName, argName) => {
const log = receipt.logs.find(({ event }) => event == logName)
return log ? log.args[argName] : null
}

const getLogCount = (receipt, logName) => {
const logs = receipt.logs.filter(l => l.event == logName)
return logs.length
}

const deployedContract = async (receiptPromise, name) =>
artifacts.require(name).at(getLog(await receiptPromise, 'Deployed', 'addr'))

const assertEqualBN = async (actualPromise, expected, message) =>
assert.equal((await actualPromise).toNumber(), expected, message)

const assertLogs = async (receiptPromise, ...logNames) => {
const receipt = await receiptPromise
for (const logName of logNames) {
assert.isNotNull(getLog(receipt, logName), `Expected ${logName} in receipt`)
}
}

contract('Court: final appeal', ([ poor, rich, governor, juror1, juror2, juror3, juror4, juror5, juror6, juror7 ]) => {
const jurors = [juror1, juror2, juror3, juror4, juror5, juror6, juror7]
const NO_DATA = ''
const ZERO_ADDRESS = '0x' + '00'.repeat(20)
let MAX_JURORS_PER_BATCH
let MAX_DRAFT_ROUNDS

const termDuration = 10
const firstTermStart = 1
const jurorMinStake = 200
const startBlock = 1000
const commitTerms = 1
const revealTerms = 1
const appealTerms = 1
const penaltyPct = 100 // 100‱ = 1%

const initialBalance = 1e6
const richStake = 1000
const jurorGenericStake = 600

const NEW_DISPUTE_EVENT = 'NewDispute'
const JUROR_DRAFTED_EVENT = 'JurorDrafted'
const DISPUTE_STATE_CHANGED_EVENT = 'DisputeStateChanged'
const VOTE_COMMITTED_EVENT = 'VoteCommitted'
const VOTE_REVEALED_EVENT = 'VoteRevealed'
const RULING_APPEALED_EVENT = 'RulingAppealed'
const ROUND_SLASHING_SETTLED_EVENT = 'RoundSlashingSettled'
const REWARD_SETTLED_EVENT = 'RewardSettled'

const ERROR_INVALID_ADJUDICATION_STATE = 'COURT_INVALID_ADJUDICATION_STATE'

const SALT = soliditySha3('passw0rd')

const encryptVote = (ruling, salt = SALT) =>
soliditySha3(
{ t: 'uint8', v: ruling },
{ t: 'bytes32', v: salt }
)

const pct4 = (n, p) => n * p / 1e4

before(async () => {
this.tokenFactory = await TokenFactory.new()
})

beforeEach(async () => {
// Mints 1,000,000 tokens for sender
this.anj = await deployedContract(this.tokenFactory.newToken('ANJ', initialBalance, { from: rich }), MINIME)
await assertEqualBN(this.anj.balanceOf(rich), initialBalance, 'rich balance')
await assertEqualBN(this.anj.balanceOf(poor), 0, 'poor balance')

const initPwd = SALT
const preOwner = '0x' + soliditySha3(initPwd).slice(-40)
this.voting = await CRVoting.new(preOwner)
this.sumTree = await SumTree.new(preOwner)

this.court = await CourtMock.new(
termDuration,
this.anj.address,
ZERO_ADDRESS, // no fees
this.voting.address,
this.sumTree.address,
initPwd,
0,
0,
0,
0,
0,
governor,
firstTermStart,
jurorMinStake,
[ commitTerms, appealTerms, revealTerms ],
penaltyPct
)

MAX_JURORS_PER_BATCH = (await this.court.getMaxJurorsPerBatch.call()).toNumber()
MAX_DRAFT_ROUNDS = (await this.court.getMaxDraftRounds.call()).toNumber()

await this.court.mock_setBlockNumber(startBlock)

assert.equal(await this.court.token(), this.anj.address, 'court token')
//assert.equal(await this.court.jurorToken(), this.anj.address, 'court juror token')
await assertEqualBN(this.court.mock_treeTotalSum(), 0, 'empty sum tree')

await this.anj.approveAndCall(this.court.address, richStake, NO_DATA, { from: rich })

for (let juror of jurors) {
await this.anj.approve(this.court.address, jurorGenericStake, { from: rich })
await this.court.stakeFor(juror, jurorGenericStake, NO_DATA, { from: rich })
}

await assertEqualBN(this.court.totalStakedFor(rich), richStake, 'rich stake')
for (let juror of jurors) {
await assertEqualBN(this.court.totalStakedFor(juror), jurorGenericStake, 'juror stake')
}
})

const passTerms = async terms => {
await this.court.mock_timeTravel(terms * termDuration)
await this.court.heartbeat(terms)
await this.court.mock_blockTravel(1)
assert.isFalse(await this.court.canTransitionTerm(), 'all terms transitioned')
}

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

const jurorNumber = 3
const term = 3
const rulings = 2

let disputeId = 0
const firstRoundId = 0
let voteId

beforeEach(async () => {
for (const juror of jurors) {
await this.court.activate({ from: juror })
}
await passTerms(1) // term = 1

const arbitrable = poor // it doesn't matter, just an address
const receipt = await this.court.createDispute(arbitrable, rulings, jurorNumber, term)
await assertLogs(receipt, NEW_DISPUTE_EVENT)
disputeId = getLog(receipt, NEW_DISPUTE_EVENT, 'disputeId')
voteId = getLog(receipt, NEW_DISPUTE_EVENT, 'voteId')
})

const draftAdjudicationRound = async (roundJurors) => {
let roundJurorsDrafted = 0
let draftReceipt
while (roundJurorsDrafted < roundJurors) {
draftReceipt = await this.court.draftAdjudicationRound(disputeId)
const callJurorsDrafted = getLogCount(draftReceipt, JUROR_DRAFTED_EVENT)
roundJurorsDrafted += callJurorsDrafted
}
await assertLogs(draftReceipt, DISPUTE_STATE_CHANGED_EVENT)
}

const moveForwardToFinalRound = async () => {
await passTerms(2) // term = 3, dispute init

for (let roundId = 0; roundId < MAX_DRAFT_ROUNDS; roundId++) {
const roundJurors = (2**roundId) * jurorNumber + 2**roundId - 1
// draft
await draftAdjudicationRound(roundJurors)

// commit
await passTerms(commitTerms)

// reveal
await passTerms(revealTerms)

// appeal
const appealReceipt = await this.court.appealRuling(disputeId, roundId)
assertLogs(appealReceipt, RULING_APPEALED_EVENT)
voteId = getLog(appealReceipt, RULING_APPEALED_EVENT, 'voteId')
await passTerms(appealTerms)
}
}

it('reaches final appeal, all jurors can vote', async () => {
await moveForwardToFinalRound()
const vote = 1
for (const juror of jurors) {
const receiptPromise = this.voting.commitVote(voteId, encryptVote(vote), { from: juror })
await assertLogs(receiptPromise, VOTE_COMMITTED_EVENT)
}
})

it('fails appealing after final appeal', async () => {
await moveForwardToFinalRound()

const roundJurors = (await this.sumTree.getNextKey()).toNumber() - 1
// no need to draft (as it's all jurors)

// commit
await passTerms(commitTerms)

// reveal
await passTerms(revealTerms)

// appeal
await assertRevert(this.court.appealRuling(disputeId, MAX_DRAFT_ROUNDS), ERROR_INVALID_ADJUDICATION_STATE)
})

context('Rewards and slashes', () => {
const penalty = jurorMinStake * penaltyPct / 10000
const weight = jurorGenericStake / jurorMinStake

// more than half of the jurors voting first option
const winningJurors = Math.floor(jurors.length / 2) + 1

beforeEach(async () => {
await moveForwardToFinalRound()
// vote
const vote = 2

// commit
for (let i = 0; i < winningJurors; i++) {
const receiptPromise = this.voting.commitVote(voteId, encryptVote(vote), { from: jurors[i] })
await assertLogs(receiptPromise, VOTE_COMMITTED_EVENT)
}

await passTerms(commitTerms)

// reveal
for (let i = 0; i < winningJurors; i++) {
const receiptPromise = this.voting.revealVote(voteId, vote, SALT, { from: jurors[i] })
await assertLogs(receiptPromise, VOTE_REVEALED_EVENT)
}

await passTerms(revealTerms)

// settle
for (let roundId = 0; roundId <= MAX_DRAFT_ROUNDS; roundId++) {
const receiptPromise = this.court.settleRoundSlashing(disputeId, roundId)
await assertLogs(receiptPromise, ROUND_SLASHING_SETTLED_EVENT)
}
})

it('winning jurors get reward', async () => {
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.settleFinalRounds(jurors[i], 2)
await assertLogs(receiptPromise, REWARD_SETTLED_EVENT)

// as jurors are not withdrawing here, real token balance shouldn't change
assert.equal(tokenBalance, (await this.anj.balanceOf(jurors[i])).toNumber(), `token balance doesn't match for juror ${i}`)

const reward = Math.floor(penalty * (jurors.length - winningJurors) * weight / winningJurors)
assert.equal(courtBalance + reward, (await this.court.totalStakedFor(jurors[i])).toNumber(), `balance in court doesn't match for juror ${i}`)
}
})

it('losers jurors have penalty', async () => {
for (let i = winningJurors; i < jurorNumber; i++) {
const tokenBalance = (await this.anj.balanceOf(jurors[i])).toNumber()
const courtBalance = (await this.court.totalStakedFor(jurors[i])).toNumber()
const receiptPromise = this.court.settleFinalRounds(jurors[i], 2)
await assertLogs(receiptPromise, REWARD_SETTLED_EVENT)

// as jurors are not withdrawing here, real token balance shouldn't change
assert.equal(tokenBalance, (await this.anj.balanceOf(jurors[i])).toNumber(), `token balance doesn't match for juror ${i}`)

const weightedPenalty = Math.floor(penalty * (jurors.length - winningJurors) * weight / winningJurors)
assert.equal(courtBalance + weightedPenalty, (await this.court.totalStakedFor(jurors[i])).toNumber(), `balance in court doesn't match for juror ${i}`)
}
})
})
})
})