Skip to content

Commit

Permalink
tmp: Final round fees
Browse files Browse the repository at this point in the history
  • Loading branch information
ßingen committed May 29, 2019
1 parent 5b01168 commit 083bfff
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 39 deletions.
155 changes: 116 additions & 39 deletions contracts/Court.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,25 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner {
mapping (address => uint256) balances; // token addr -> balance
// when deactivating, balance becomes available on next term:
uint64 deactivationTermId;
uint64 nextFinalRoundDisputeToSettle; // id in finalRoundDisputeIds array of the next pending final appeal round to settle
uint256 atStakeTokens; // maximum amount of juror tokens that the juror could be slashed given their drafts
uint256 sumTreeId; // key in the sum tree used for sortition
}

struct CourtConfig {
// Fee structure
ERC20 feeToken;
uint16 governanceFeeShare; // ‱ of fees going to the governor (1/10,000)
uint256 jurorFee; // per juror, total round juror fee = jurorFee * jurors drawn
uint256 heartbeatFee; // per dispute, total heartbeat fee = heartbeatFee * disputes/appeals in term
uint256 draftFee; // per juror, total round draft fee = draftFee * jurors drawn
uint256 settleFee; // per juror, total round draft fee = settleFee * jurors drawn
uint16 governanceFeeShare; // ‱ of fees going to the governor (1/10,000)
uint256 jurorFee; // per juror, total round juror fee = jurorFee * jurors drawn
uint256 heartbeatFee; // per dispute, total heartbeat fee = heartbeatFee * disputes/appeals in term
uint256 draftFee; // per juror, total round draft fee = draftFee * jurors drawn
uint256 settleFee; // per juror, total round draft fee = settleFee * jurors drawn
// Dispute config
uint64 commitTerms;
uint64 revealTerms;
uint64 appealTerms;
uint16 penaltyPct;
uint16 finalRoundReduction; // ‱ of reduction applied for final appeal round (1/10,000)
}

struct Term {
Expand Down Expand Up @@ -75,6 +77,8 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner {
uint64 draftTermId;
uint64 delayTerms;
uint32 jurorNumber;
uint8 winningRuling;
uint32 coherentJurors;
address triggeredBy;
bool settledPenalties;
uint256 slashedTokens;
Expand Down Expand Up @@ -119,6 +123,7 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner {
ISumTree internal sumTree;
Dispute[] public disputes;
mapping (uint32 => Vote) votes; // to map from voteId to disputeId and roundId
uint256[] finalRoundDisputeIds; // Disputes that made it to the final appeal round

string internal constant ERROR_INVALID_ADDR = "COURT_INVALID_ADDR";
string internal constant ERROR_DEPOSIT_FAILED = "COURT_DEPOSIT_FAILED";
Expand Down Expand Up @@ -227,6 +232,8 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner {
uint256 _jurorMinStake,
uint64[3] _roundStateDurations,
uint16 _penaltyPct
// TODO: stack too deep
//uint16 _finalRoundReduction
) public {
termDuration = _termDuration;
jurorToken = _jurorToken;
Expand All @@ -248,7 +255,10 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner {
_settleFee,
_governanceFeeShare,
_roundStateDurations,
_penaltyPct
_penaltyPct,
// TODO: stack too deep
//_finalRoundReduction
5000
);
terms[ZERO_TERM_ID].startTime = _firstTermStartTime - _termDuration;
}
Expand Down Expand Up @@ -567,7 +577,7 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner {
Dispute storage dispute = disputes[_disputeId];
dispute.state = DisputeState.Executed;

uint8 winningRuling = _getWinningRuling(dispute);
uint8 winningRuling = dispute.rounds[_roundId].winningRuling;

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

Expand All @@ -581,6 +591,7 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner {
function settleRoundSlashing(uint256 _disputeId, uint256 _roundId) external ensureTerm {
Dispute storage dispute = disputes[_disputeId];
AdjudicationRound storage round = dispute.rounds[_roundId];
CourtConfig storage config = courtConfigs[terms[round.draftTermId].courtConfigId]; // safe to use directly as it is the current term

// Enforce that rounds are settled in order to avoid one round without incentive to settle
// even if there is a settleFee, it may not be big enough and all jurors in the round are going to be slashed
Expand All @@ -594,24 +605,50 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner {
}

uint8 winningRuling = _getWinningRuling(dispute);
CourtConfig storage config = courtConfigs[terms[round.draftTermId].courtConfigId]; // safe to use directly as it is the current term
// uint256 penalty = _pct4(jurorMinStake, config.penaltyPct); // TODO: stack too deep
round.winningRuling = winningRuling;
uint256 coherentJurors = voting.getRulingVotes(round.voteId, winningRuling);
round.coherentJurors = uint32(coherentJurors);

uint256 slashedTokens;
if (_roundId < MAX_DRAFT_ROUNDS) {
slashedTokens = _settleRegularRoundSlashing(dispute, round, config.penaltyPct, winningRuling);
} else {
slashedTokens = _settleFinalRoundSlashing(_disputeId, round, config.penaltyPct);
}

round.slashedTokens = slashedTokens;
round.settledPenalties = true;

// No juror was coherent in the round
if (coherentJurors == 0) {
// refund fees and burn ANJ
_payFees(config.feeToken, round.triggeredBy, config.jurorFee * round.jurorNumber, config.governanceFeeShare);
_assignTokens(jurorToken, BURN_ACCOUNT, slashedTokens);
}

uint256 slashedTokens = 0;
uint256 votesLength = round.jurors.length;
_payFees(config.feeToken, msg.sender, config.settleFee * round.jurorNumber, config.governanceFeeShare);


emit RoundSlashingSettled(_disputeId, _roundId, slashedTokens);
}

function _settleRegularRoundSlashing(Dispute storage _dispute, AdjudicationRound storage _round, uint16 _penaltyPct, uint8 _winningRuling) internal returns (uint256 slashedTokens) {
// uint256 penalty = _pct4(jurorMinStake, _penaltyPct); // TODO: stack too deep

uint256 votesLength = _round.jurors.length;
uint64 slashingUpdateTermId = termId + 1;

// should we batch this too?? OOG?
for (uint256 i = 0; i < votesLength; i++) {
address juror = round.jurors[i];
//uint256 weightedPenalty = penalty * round.jurorSlotStates[juror].weight; // TODO: stack too deep
uint256 weightedPenalty = _pct4(jurorMinStake, config.penaltyPct) * round.jurorSlotStates[juror].weight;
address juror = _round.jurors[i];
//uint256 weightedPenalty = penalty * _round.jurorSlotStates[juror].weight; // TODO: stack too deep
uint256 weightedPenalty = _pct4(jurorMinStake, _penaltyPct) * _round.jurorSlotStates[juror].weight;
Account storage account = accounts[juror];
account.atStakeTokens -= weightedPenalty;

uint8 jurorRuling = voting.getCastVote(round.voteId, juror);
uint8 jurorRuling = voting.getCastVote(_round.voteId, juror);
// If the juror didn't vote for the final winning ruling
if (jurorRuling != winningRuling) {
if (jurorRuling != _winningRuling) {
slashedTokens += weightedPenalty;

if (account.deactivationTermId <= slashingUpdateTermId) {
Expand All @@ -623,20 +660,14 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner {
}
}
}
}

round.slashedTokens = slashedTokens;
round.settledPenalties = true;

// No juror was coherent in the round
if (voting.getRulingVotes(round.voteId, winningRuling) == 0) {
// refund fees and burn ANJ
_payFees(config.feeToken, round.triggeredBy, config.jurorFee * round.jurorNumber, config.governanceFeeShare);
_assignTokens(jurorToken, BURN_ACCOUNT, slashedTokens);
}

_payFees(config.feeToken, msg.sender, config.settleFee * round.jurorNumber, config.governanceFeeShare);
function _settleFinalRoundSlashing(uint256 _disputeId, AdjudicationRound storage _round, uint16 _penaltyPct) internal returns (uint256 slashedTokens) {
slashedTokens = _pct4(jurorMinStake, _penaltyPct) * (_round.jurorNumber - _round.coherentJurors);

emit RoundSlashingSettled(_disputeId, _roundId, slashedTokens);
uint256 finalRoundDisputeIdsLength = finalRoundDisputeIds.length;
finalRoundDisputeIds.length = finalRoundDisputeIdsLength + 1;
finalRoundDisputeIds[finalRoundDisputeIdsLength]= _disputeId;
}

/**
Expand All @@ -653,12 +684,10 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner {

jurorState.rewarded = true;

uint256 voteId = round.voteId;
uint8 winningRuling = _getWinningRuling(dispute);
uint256 coherentJurors = voting.getRulingVotes(voteId, winningRuling);
uint8 jurorRuling = voting.getCastVote(voteId, _juror);
uint256 coherentJurors = round.coherentJurors;
uint8 jurorRuling = voting.getCastVote(round.voteId, _juror);

require(jurorRuling == winningRuling, ERROR_JUROR_NOT_COHERENT);
require(jurorRuling == round.winningRuling, ERROR_JUROR_NOT_COHERENT);

uint256 slashedTokens = round.slashedTokens;

Expand All @@ -672,6 +701,47 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner {
emit RewardSettled(_disputeId, _roundId, _juror);
}

function settleFinalRounds(address _juror) external {
uint256 finalRoundDisputeIdsLength = finalRoundDisputeIds.length;
for (uint256 i = account.nextFinalRoundDisputeToSettle; i < finalRoundDisputeIdsLength; i++) {
_settleFinalRound(_juror, account, finalRoundDisputeIds[i]);
}

Account storage account = accounts[_juror];
account.nextFinalRoundDisputeToSettle = uint64(finalRoundDisputeIdsLength);
}

function _settleFinalRound(address _juror, Account storage _account, uint256 _disputeId) internal {
Dispute storage dispute = disputes[_disputeId];
uint256 roundId = MAX_DRAFT_ROUNDS;
AdjudicationRound storage round = dispute.rounds[roundId];
CourtConfig storage config = courtConfigs[terms[round.draftTermId].courtConfigId]; // safe to use directly as it is the current term

require(round.settledPenalties, ERROR_ROUND_NOT_SETTLED);

uint256 coherentJurors = round.coherentJurors;
uint8 jurorRuling = voting.getCastVote(round.voteId, _juror);

uint256 slashedTokens = round.slashedTokens;

if (jurorRuling == round.winningRuling) {
_assignTokens(jurorToken, _juror, slashedTokens / coherentJurors);
_payFees(config.feeToken, _juror, config.jurorFee * round.jurorNumber / coherentJurors, config.governanceFeeShare);

emit RewardSettled(_disputeId, roundId, _juror);
} else {
uint256 penalty = _pct4(jurorMinStake, config.penaltyPct);
uint64 slashingUpdateTermId = termId + 1;
if (_account.deactivationTermId <= slashingUpdateTermId) {
// Slash from balance if the account already deactivated
_removeTokens(jurorToken, _juror, penalty);
} else {
// account.sumTreeId always > 0: as the juror has activated (and gots its sumTreeId)
sumTree.update(_account.sumTreeId, slashingUpdateTermId, penalty, false);
}
}
}

function canTransitionTerm() public view returns (bool) {
return neededTermTransitions() >= 1;
}
Expand Down Expand Up @@ -807,8 +877,6 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner {
}

(roundId, voteId) = _createRound(_disputeId, DisputeState.PreDraft, _draftTermId, _jurorNumber, 0);

terms[_draftTermId].dependingDrafts += 1;
}

function _finalAdjudicationRound(
Expand All @@ -818,10 +886,15 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner {
internal
returns (uint256 roundId, uint32 voteId)
{
// TODO: fees

uint32 jurorNumber = uint32(sumTree.getNextKey() - 1); // TODO: check overflow?

CourtConfig storage config = _courtConfigForTerm(_draftTermId);
// apply final round discount
uint256 feeAmount = config.heartbeatFee + _pct4(jurorNumber * config.jurorFee, config.finalRoundReduction);
if (feeAmount > 0) {
require(config.feeToken.safeTransferFrom(msg.sender, this, feeAmount), ERROR_DEPOSIT_FAILED);
}

(roundId, voteId) = _createRound(_disputeId, DisputeState.Adjudicating, _draftTermId, jurorNumber, jurorNumber);
}

Expand Down Expand Up @@ -851,6 +924,8 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner {
round.triggeredBy = msg.sender;

votes[voteId] = Vote(uint128(_disputeId), uint128(roundId));

terms[_draftTermId].dependingDrafts += 1;
}

function _getWinningRuling(Dispute storage dispute) internal view returns (uint8) {
Expand Down Expand Up @@ -980,7 +1055,8 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner {
uint256 _settleFee,
uint16 _governanceFeeShare,
uint64[3] _roundStateDurations,
uint16 _penaltyPct
uint16 _penaltyPct,
uint16 _finalRoundReduction
)
internal
{
Expand Down Expand Up @@ -1008,7 +1084,8 @@ contract Court is ERC900, ApproveAndCallFallBack, ICRVotingOwner {
commitTerms: _roundStateDurations[0],
revealTerms: _roundStateDurations[1],
appealTerms: _roundStateDurations[2],
penaltyPct: _penaltyPct
penaltyPct: _penaltyPct,
finalRoundReduction: _finalRoundReduction
});

uint64 courtConfigId = uint64(courtConfigs.push(courtConfig) - 1);
Expand Down
4 changes: 4 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 Down
12 changes: 12 additions & 0 deletions test/court-final-appeal.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,5 +217,17 @@ contract('Court: final appeal', ([ poor, rich, governor, juror1, juror2, juror3,
await assertRevert(this.court.appealRuling(disputeId, MAX_DRAFT_ROUNDS), ERROR_INVALID_ADJUDICATION_STATE)
})

context('Rewards and slashes', () => {
beforeEach(async () => {
await moveForwardToFinalRound()
// vote
const vote = 1
for (const juror of jurors) {
const receiptPromise = await this.voting.commitVote(voteId, encryptVote(vote), { from: juror })
await assertLogs(receiptPromise, VOTE_COMMITTED_EVENT)
}
})
})

})
})

0 comments on commit 083bfff

Please sign in to comment.