Skip to content

Commit

Permalink
court: test disputes creation
Browse files Browse the repository at this point in the history
  • Loading branch information
facuspagnuolo committed Aug 26, 2019
1 parent da99eeb commit 5e0b779
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 19 deletions.
55 changes: 37 additions & 18 deletions contracts/Court.sol
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ contract Court is IJurorsRegistryOwner, ICRVotingOwner, ISubscriptionsOwner, Tim
string internal constant ERROR_INVALID_DISPUTE_STATE = "CTBAD_DISPUTE_STATE";
string internal constant ERROR_INVALID_ADJUDICATION_ROUND = "CTBAD_ADJ_ROUND";
string internal constant ERROR_INVALID_ADJUDICATION_STATE = "CTBAD_ADJ_STATE";
string internal constant ERROR_DISPUTE_DOES_NOT_EXIST = "CT_DISPUTE_DOES_NOT_EXIST";
string internal constant ERROR_CANNOT_CREATE_DISPUTE = "CT_CANNOT_CREATE_DISPUTE";
string internal constant ERROR_ROUND_DOES_NOT_EXIST = "CT_ROUND_DOES_NOT_EXIST";
string internal constant ERROR_ROUND_ALREADY_APPEALED = "CTROUND_ALRDY_APPEALED";
string internal constant ERROR_ROUND_NOT_APPEALED = "CTROUND_NOT_APPEALED";
string internal constant ERROR_ROUND_APPEAL_ALREADY_SETTLED = "CTAPPEAL_ALRDY_SETTLED";
Expand Down Expand Up @@ -172,7 +175,7 @@ contract Court is IJurorsRegistryOwner, ICRVotingOwner, ISubscriptionsOwner, Tim
event NewTerm(uint64 termId, address indexed heartbeatSender);
event NewCourtConfig(uint64 fromTermId, uint64 courtConfigId);
event DisputeStateChanged(uint256 indexed disputeId, DisputeState indexed state);
event NewDispute(uint256 indexed disputeId, address indexed subject, uint64 indexed draftTermId, uint64 jurorNumber);
event NewDispute(uint256 indexed disputeId, address indexed subject, uint64 indexed draftTermId, uint64 jurorsNumber);
event RulingAppealed(uint256 indexed disputeId, uint256 indexed roundId, uint8 ruling);
event RulingAppealConfirmed(uint256 indexed disputeId, uint256 indexed roundId, uint64 indexed draftTermId, uint256 jurorNumber);
event RulingExecuted(uint256 indexed disputeId, uint8 indexed ruling);
Expand All @@ -189,6 +192,17 @@ contract Court is IJurorsRegistryOwner, ICRVotingOwner, ISubscriptionsOwner, Tim
_;
}

modifier disputeExists(uint256 _id) {
require(_id < disputes.length, ERROR_DISPUTE_DOES_NOT_EXIST);
_;
}

modifier roundExists(uint256 _disputeId, uint256 _roundId) {
require(_disputeId < disputes.length, ERROR_DISPUTE_DOES_NOT_EXIST);
require(_roundId < disputes[_disputeId].rounds.length, ERROR_ROUND_DOES_NOT_EXIST);
_;
}

/**
* @param _termDuration Duration in seconds per term (recommended 1 hour)
* @param _tokens Array containing:
Expand Down Expand Up @@ -264,34 +278,39 @@ contract Court is IJurorsRegistryOwner, ICRVotingOwner, ISubscriptionsOwner, Tim
}

/**
* @notice Create a dispute over `_subject` with `_possibleRulings` possible rulings, drafting `_jurorNumber` jurors in term `_draftTermId`
* @notice Create a dispute over `_subject` with `_possibleRulings` possible rulings, drafting `_jurorsNumber` jurors in term `_draftTermId`
* @dev Create a dispute to be drafted in a future term
* @param _subject Arbitrable subject being disputed
* @param _possibleRulings Number of possible rulings allowed for the drafted jurors to vote on the dispute
* @param _jurorsNumber Requested number of jurors to be drafted for the dispute
* @param _draftTermId Term in which the jurors for the dispute will be drafted
* @return Dispute identification number
*/
function createDispute(IArbitrable _subject, uint8 _possibleRulings, uint64 _jurorNumber, uint64 _draftTermId) external ensureTerm
function createDispute(IArbitrable _subject, uint8 _possibleRulings, uint64 _jurorsNumber, uint64 _draftTermId) external ensureTerm
returns (uint256)
{
// TODO: Limit the min amount of terms before drafting (to allow for evidence submission)
// TODO: Limit the max amount of terms into the future that a dispute can be drafted
// TODO: Limit the max number of initial jurors
// TODO: ERC165 check that _subject conforms to the Arbitrable interface

// TODO: require(address(_subject) == msg.sender, ERROR_INVALID_DISPUTE_CREATOR);
require(termId > ZERO_TERM_ID, ERROR_CANNOT_CREATE_DISPUTE);
require(subscriptions.isUpToDate(address(_subject)), ERROR_SUBSCRIPTION_NOT_PAID);
require(_possibleRulings >= MIN_RULING_OPTIONS && _possibleRulings <= MAX_RULING_OPTIONS, ERROR_INVALID_RULING_OPTIONS);

uint256 disputeId = disputes.length;
disputes.length = disputeId + 1;

// Create the dispute
uint256 disputeId = disputes.length++;
Dispute storage dispute = disputes[disputeId];
dispute.subject = _subject;
dispute.possibleRulings = _possibleRulings;
emit NewDispute(disputeId, _subject, _draftTermId, _jurorsNumber);

(ERC20 feeToken, uint256 feeAmount, uint256 jurorFees) = _getFeesForRegularRound(_draftTermId, _jurorNumber);
// pay round fees
_payGeneric(feeToken, feeAmount);
_createRound(disputeId, DisputeState.PreDraft, _draftTermId, _jurorNumber, jurorFees);

emit NewDispute(disputeId, _subject, _draftTermId, _jurorNumber);
// Create first adjudication round of the dispute
(ERC20 feeToken, uint256 feeAmount, uint256 jurorFees) = _getFeesForRegularRound(_draftTermId, _jurorsNumber);
_createRound(disputeId, DisputeState.PreDraft, _draftTermId, _jurorsNumber, jurorFees);

// Pay round fees and return dispute id
_payGeneric(feeToken, feeAmount);
return disputeId;
}

Expand Down Expand Up @@ -648,14 +667,14 @@ contract Court is IJurorsRegistryOwner, ICRVotingOwner, ISubscriptionsOwner, Tim
return governor;
}

function getDispute(uint256 _disputeId) external view
function getDispute(uint256 _disputeId) external disputeExists(_disputeId) view
returns (address subject, uint8 possibleRulings, DisputeState state, uint8 finalRuling)
{
Dispute storage dispute = disputes[_disputeId];
return (dispute.subject, dispute.possibleRulings, dispute.state, dispute.finalRuling);
}

function getAdjudicationRound(uint256 _disputeId, uint256 _roundId) external view
function getAdjudicationRound(uint256 _disputeId, uint256 _roundId) external roundExists(_disputeId, _roundId) view
returns (uint64 draftTerm, uint64 jurorNumber, address triggeredBy, bool settledPenalties, uint256 slashedTokens)
{
AdjudicationRound storage round = disputes[_disputeId].rounds[_roundId];
Expand Down Expand Up @@ -990,9 +1009,9 @@ contract Court is IJurorsRegistryOwner, ICRVotingOwner, ISubscriptionsOwner, Tim
);
}

function _payGeneric(ERC20 paymentToken, uint256 amount) internal {
if (amount > 0) {
require(paymentToken.safeTransferFrom(msg.sender, address(accounting), amount), ERROR_DEPOSIT_FAILED);
function _payGeneric(ERC20 _paymentToken, uint256 _amount) internal {
if (_amount > uint256(0)) {
require(_paymentToken.safeTransferFrom(msg.sender, address(accounting), _amount), ERROR_DEPOSIT_FAILED);
}
}

Expand Down
258 changes: 258 additions & 0 deletions test/court/court-disputes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
const { bigExp } = require('../helpers/numbers')(web3)
const { assertRevert } = require('@aragon/os/test/helpers/assertThrow')
const { TOMORROW, ONE_DAY } = require('../helpers/time')
const { buildHelper, DISPUTE_STATES } = require('../helpers/court')(web3, artifacts)
const { assertAmountOfEvents, assertEvent } = require('@aragon/os/test/helpers/assertEvent')(web3)

const MiniMeToken = artifacts.require('MiniMeToken')
const Arbitrable = artifacts.require('ArbitrableMock')

const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'

contract('Court', ([_, sender]) => {
let courtHelper, court, feeToken, arbitrable

const termDuration = ONE_DAY
const firstTermStartTime = TOMORROW
const jurorFee = bigExp(10, 18)
const heartbeatFee = bigExp(20, 18)
const draftFee = bigExp(30, 18)
const settleFee = bigExp(40, 18)

beforeEach('create court', async () => {
courtHelper = buildHelper()
feeToken = await MiniMeToken.new(ZERO_ADDRESS, ZERO_ADDRESS, 0, 'Court Fee Token', 18, 'CFT', true)
court = await courtHelper.deploy({ firstTermStartTime, termDuration, feeToken, jurorFee, heartbeatFee, draftFee, settleFee })
})

beforeEach('mock subscriptions and arbitrable instance', async () => {
arbitrable = await Arbitrable.new()
await courtHelper.subscriptions.setUpToDate(true)
})

describe('createDispute', () => {
context('when the given input is valid', () => {
const draftTermId = 2
const jurorsNumber = 10
const possibleRulings = 2

const itHandlesDisputesCreationProperly = expectedTermTransitions => {
context('when the creator deposits enough collateral', () => {
const jurorFees = jurorFee.mul(jurorsNumber)
const jurorRewards = (draftFee.plus(settleFee)).mul(jurorsNumber)
const requiredCollateral = jurorFees.plus(heartbeatFee).plus(jurorRewards)

beforeEach('deposit collateral', async () => {
await feeToken.generateTokens(sender, requiredCollateral)
await feeToken.approve(court.address, requiredCollateral, { from: sender })
})

it('creates a new dispute', async () => {
const receipt = await court.createDispute(arbitrable.address, possibleRulings, jurorsNumber, draftTermId, { from: sender })

assertAmountOfEvents(receipt, 'NewDispute')
assertEvent(receipt, 'NewDispute', { disputeId: 0, subject: arbitrable.address, draftTermId, jurorsNumber })

const [subject, rulings, state, finalRuling] = await court.getDispute(0)
assert.equal(subject, arbitrable.address, 'dispute subject does not match')
assert.equal(state, DISPUTE_STATES.PRE_DRAFT, 'dispute state does not match')
assert.equal(rulings.toString(), possibleRulings, 'dispute possible rulings do not match')
assert.equal(finalRuling.toString(), 0, 'dispute final ruling does not match')
})

it('creates a new adjudication round', async () => {
await court.createDispute(arbitrable.address, possibleRulings, jurorsNumber, draftTermId, { from: sender })

const [draftTerm, jurorNumber, triggeredBy, settledPenalties, slashedTokens] = await court.getAdjudicationRound(0, 0)
assert.equal(draftTerm.toString(), draftTermId, 'round draft term does not match')
assert.equal(jurorNumber.toString(), jurorsNumber, 'round jurors number does not match')
assert.equal(triggeredBy, sender, 'round trigger does not match')
assert.equal(settledPenalties, false, 'round penalties should not be settled')
assert.equal(slashedTokens.toString(), 0, 'round slashed tokens should be zero')
})

it('transfers the collateral to the court', async () => {
const previousCourtBalance = await feeToken.balanceOf(court.address)
const previousAccountingBalance = await feeToken.balanceOf(courtHelper.accounting.address)
const previousSenderBalance = await feeToken.balanceOf(sender)

await court.createDispute(arbitrable.address, possibleRulings, jurorsNumber, draftTermId, { from: sender })

const jurorFees = jurorFee.mul(jurorsNumber)
const jurorRewards = (draftFee.plus(settleFee)).mul(jurorsNumber)
const expectedDelta = jurorFees.plus(heartbeatFee).plus(jurorRewards)

const currentCourtBalance = await feeToken.balanceOf(court.address)
assert.equal(previousCourtBalance.toString(), currentCourtBalance.toString(), 'court balances do not match')

const currentAccountingBalance = await feeToken.balanceOf(courtHelper.accounting.address)
assert.equal(previousAccountingBalance.plus(expectedDelta).toString(), currentAccountingBalance.toString(), 'court accounting balances do not match')

const currentSenderBalance = await feeToken.balanceOf(sender)
assert.equal(previousSenderBalance.minus(expectedDelta).toString(), currentSenderBalance.toString(), 'sender balances do not match')
})

it(`transitions ${expectedTermTransitions} terms`, async () => {
const previousTermId = await court.getLastEnsuredTermId()

const receipt = await court.createDispute(arbitrable.address, possibleRulings, jurorsNumber, draftTermId, { from: sender })

assertAmountOfEvents(receipt, 'NewTerm', expectedTermTransitions)

const currentTermId = await court.getLastEnsuredTermId()
assert.equal(previousTermId.plus(expectedTermTransitions).toString(), currentTermId.toString(), 'term id does not match')
})
})

context('when the creator does not deposit enough collateral', () => {
it('reverts', async () => {
await assertRevert(court.createDispute(arbitrable.address, possibleRulings, jurorsNumber, draftTermId), 'CTDEPOSIT_FAIL')
})
})
}

context('when the court is at term zero', () => {
it('reverts', async () => {
await assertRevert(court.createDispute(arbitrable.address, possibleRulings, jurorsNumber, draftTermId), 'CT_CANNOT_CREATE_DISPUTE')
})
})

context('when the court is after term zero', () => {
beforeEach('set timestamp at the beginning of the first term', async () => {
await courtHelper.setTimestamp(firstTermStartTime)
})

context('when the term is up-to-date', () => {
const expectedTermTransitions = 0

beforeEach('update term', async () => {
await court.heartbeat(1)
})

itHandlesDisputesCreationProperly(expectedTermTransitions)
})

context('when the term is outdated by one term', () => {
const expectedTermTransitions = 1

itHandlesDisputesCreationProperly(expectedTermTransitions)
})

context('when the term is outdated by more than one term', () => {
beforeEach('set timestamp two terms after the first term', async () => {
await courtHelper.setTimestamp(firstTermStartTime + termDuration * 2)
})

it('reverts', async () => {
await assertRevert(court.createDispute(arbitrable.address, possibleRulings, jurorsNumber, draftTermId), 'CTTOO_MANY_TRANSITIONS')
})
})
})
})

context('when the given input is not valid', () => {
beforeEach('set timestamp at the beginning of the first term', async () => {
await courtHelper.setTimestamp(firstTermStartTime)
})

context('when the possible rulings are invalid', () => {
it('reverts', async () => {
await assertRevert(court.createDispute(arbitrable.address, 0, 10, 20), 'CTBAD_RULING_OPTS')
await assertRevert(court.createDispute(arbitrable.address, 1, 10, 20), 'CTBAD_RULING_OPTS')
await assertRevert(court.createDispute(arbitrable.address, 3, 10, 20), 'CTBAD_RULING_OPTS')
})
})

context('when the subscription is outdated', () => {
it('reverts', async () => {
await courtHelper.subscriptions.setUpToDate(false)

await assertRevert(court.createDispute(arbitrable.address, 2, 10, 20), 'CTSUBSC_UNPAID')
})
})

context('when the number of jurors is invalid', () => {
// TODO: implement
})

context('when the given term id is invalid', () => {
// TODO: implement
})

context('when the arbitrable is not valid', () => {
// TODO: implement
})
})
})

describe('getDispute', () => {
context('when the dispute exists', async () => {
const draftTermId = 2
const jurorsNumber = 10
const possibleRulings = 2

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 })

await court.createDispute(arbitrable.address, possibleRulings, jurorsNumber, draftTermId, { from: sender })
})

it('returns the requested dispute', async () => {
const [subject, rulings, state, finalRuling] = await court.getDispute(0)

assert.equal(subject, arbitrable.address, 'dispute subject does not match')
assert.equal(state, DISPUTE_STATES.PRE_DRAFT, 'dispute state does not match')
assert.equal(rulings.toString(), possibleRulings, 'dispute possible rulings do not match')
assert.equal(finalRuling.toString(), 0, 'dispute final ruling does not match')
})
})

context('when the dispute does not exist', async () => {
it('reverts', async () => {
await assertRevert(court.getDispute(0), 'CT_DISPUTE_DOES_NOT_EXIST')
})
})
})

describe('getRound', () => {
context('when the dispute exists', async () => {
const draftTermId = 2
const jurorsNumber = 10
const possibleRulings = 2

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 })

await court.createDispute(arbitrable.address, possibleRulings, jurorsNumber, draftTermId, { from: sender })
})

context('when the round exists', async () => {
it('returns the requested round', async () => {
const [draftTerm, jurorNumber, triggeredBy, settledPenalties, slashedTokens] = await court.getAdjudicationRound(0, 0)

assert.equal(draftTerm.toString(), draftTermId, 'round draft term does not match')
assert.equal(jurorNumber.toString(), jurorsNumber, 'round jurors number does not match')
assert.equal(triggeredBy, sender, 'round trigger does not match')
assert.equal(settledPenalties, false, 'round penalties should not be settled')
assert.equal(slashedTokens.toString(), 0, 'round slashed tokens should be zero')
})
})

context('when the round does not exist', async () => {
it('reverts', async () => {
await assertRevert(court.getAdjudicationRound(0, 1), 'CT_ROUND_DOES_NOT_EXIST')
})
})
})

context('when the dispute does not exist', () => {
it('reverts', async () => {
await assertRevert(court.getAdjudicationRound(0, 0), 'CT_DISPUTE_DOES_NOT_EXIST')
})
})
})
})
2 changes: 1 addition & 1 deletion test/court/court-terms.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ contract('Court', ([_, sender]) => {
itRevertsTryingToTransitionOneTerm()
})

context('when current timestamp is right at the beginning of the first term ', () => {
context('when current timestamp is right at the beginning of the first term', () => {
beforeEach('set current timestamp', async () => {
await courtHelper.setTimestamp(firstTermStartTime)
})
Expand Down
Loading

0 comments on commit 5e0b779

Please sign in to comment.