Skip to content

feat(KlerosCore): staking implementation #34

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

Merged
merged 2 commits into from
Jan 31, 2022
Merged
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
173 changes: 120 additions & 53 deletions contracts/src/arbitration/KlerosCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,23 +52,21 @@ contract KlerosCore is IArbitrator {
IDisputeKit disputeKit; // ID of the dispute kit that this dispute was assigned to.
Period period; // The current period of the dispute.
bool ruled; // True if the ruling has been executed, false otherwise.
uint256 currentRound; // The index of the current appeal round. Note that 0 represents the default dispute round. Former votes.length - 1.
uint256 lastPeriodChange; // The last time the period was changed.
uint256 nbVotes; // The total number of votes the dispute can possibly have in the current round. Former votes[_appeal].length.
mapping(uint256 => address[]) drawnJurors; // Addresses of the drawn jurors in the form `drawnJurors[_appeal]`.
Round[] rounds;
}

struct Round {
uint256 tokensAtStakePerJuror; // The amount of tokens at stake for each juror in the form `tokensAtStakePerJuror[appeal]`.
uint256 totalFeesForJurors; // The total juror fees paid in the form `totalFeesForJurors[appeal]`.
uint256 repartitions; // A counter of vote reward repartitions made in each round in the form `repartitionsInEachRound[appeal]`.
uint256 penalties; // The amount of tokens collected from penalties in each round in the form `penaltiesInEachRound[appeal]`.
uint256 tokensAtStakePerJuror; // The amount of tokens at stake for each juror in this round.
uint256 totalFeesForJurors; // The total juror fees paid in this round.
uint256 repartitions; // A counter of reward repartitions made in this round.
uint256 penalties; // The amount of tokens collected from penalties in this round.
address[] drawnJurors; // Addresses of the jurors that were drawn in this round.
}

struct Juror {
uint96[] subcourtIDs; // The IDs of subcourts where the juror's stake path ends. A stake path is a path from the forking court to a court the juror directly staked in using `_setStake`.
// TODO: Relations of staked and locked tokens (currently unfinished).
mapping(uint96 => uint256) stakedTokens; // The number of tokens the juror has staked in the subcourt in the form `stakedTokens[subcourtID]`.
mapping(uint96 => uint256) lockedTokens; // The number of tokens the juror has locked in the subcourt in the form `lockedTokens[subcourtID]`.
}
Expand All @@ -90,7 +88,7 @@ contract KlerosCore is IArbitrator {
Court[] public courts; // The subcourts.

//TODO: disputeKits forest.
IDisputeKit[] public disputeKits; // All supported dispute kits.
mapping(uint256 => IDisputeKit) public disputeKits; // All supported dispute kits.

Dispute[] public disputes; // The disputes.
mapping(address => Juror) internal jurors; // The jurors.
Expand Down Expand Up @@ -150,7 +148,7 @@ contract KlerosCore is IArbitrator {
governor = _governor;
pinakion = _pinakion;
jurorProsecutionModule = _jurorProsecutionModule;
disputeKits.push(_disputeKit);
disputeKits[0] = _disputeKit;

// Create the Forking court.
courts.push(
Expand Down Expand Up @@ -210,12 +208,12 @@ contract KlerosCore is IArbitrator {

/** @dev Add a new supported dispute kit module to the court.
* @param _disputeKitAddress The address of the dispute kit contract.
* @param _disputeKitID The ID assigned to the added dispute kit.
*/
function addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByGovernor {
// TODO: the dispute kit data structure. For now keep it a simple array.
function addNewDisputeKit(IDisputeKit _disputeKitAddress, uint8 _disputeKitID) external onlyByGovernor {
// TODO: the dispute kit data structure. For now keep it a simple mapping.
// Also note that in current state this function doesn't take into account that the added address is actually new.
require(disputeKits.length <= 256); // Can't be more than 256 because the IDs are used in a bitfield.
disputeKits.push(_disputeKitAddress);
disputeKits[_disputeKitID] = _disputeKitAddress;
}

/** @dev Creates a subcourt under a specified parent court.
Expand All @@ -227,6 +225,7 @@ contract KlerosCore is IArbitrator {
* @param _jurorsForCourtJump The `jurorsForCourtJump` property value of the subcourt.
* @param _timesPerPeriod The `timesPerPeriod` property value of the subcourt.
* @param _sortitionSumTreeK The number of children per node of the subcourt's sortition sum tree.
* @param _supportedDisputeKits Bitfield that contains the IDs of the dispute kits that this court will support.
*/
function createSubcourt(
uint96 _parent,
Expand All @@ -236,7 +235,8 @@ contract KlerosCore is IArbitrator {
uint256 _feeForJuror,
uint256 _jurorsForCourtJump,
uint256[4] memory _timesPerPeriod,
uint256 _sortitionSumTreeK
uint256 _sortitionSumTreeK,
uint256 _supportedDisputeKits
) external onlyByGovernor {
require(
courts[_parent].minStake <= _minStake,
Expand All @@ -255,7 +255,7 @@ contract KlerosCore is IArbitrator {
feeForJuror: _feeForJuror,
jurorsForCourtJump: _jurorsForCourtJump,
timesPerPeriod: _timesPerPeriod,
supportedDisputeKits: 1
supportedDisputeKits: _supportedDisputeKits
})
);

Expand Down Expand Up @@ -317,7 +317,7 @@ contract KlerosCore is IArbitrator {

/** @dev Adds/removes particular dispute kits to a subcourt's bitfield of supported dispute kits.
* @param _subcourtID The ID of the subcourt.
* @param _disputeKitIDs IDs of dispute kits in the disputeKits array, which support should be added/removed.
* @param _disputeKitIDs IDs of dispute kits which support should be added/removed.
* @param _enable Whether add or remove the dispute kits from the subcourt.
*/
function setDisputeKits(
Expand Down Expand Up @@ -346,7 +346,7 @@ contract KlerosCore is IArbitrator {
* @param _stake The new stake.
*/
function setStake(uint96 _subcourtID, uint256 _stake) external {
setStakeForAccount(msg.sender, _subcourtID, _stake);
require(setStakeForAccount(msg.sender, _subcourtID, _stake, 0), "Staking failed");
}

/** @dev Creates a dispute. Must be called by the arbitrable contract.
Expand Down Expand Up @@ -396,17 +396,17 @@ contract KlerosCore is IArbitrator {
*/
function passPeriod(uint256 _disputeID) external {
Dispute storage dispute = disputes[_disputeID];

uint256 currentRound = dispute.rounds.length - 1;
Round storage round = dispute.rounds[currentRound];
if (dispute.period == Period.evidence) {
require(
dispute.currentRound > 0 ||
currentRound > 0 ||
block.timestamp - dispute.lastPeriodChange >=
courts[dispute.subcourtID].timesPerPeriod[uint256(dispute.period)],
"The evidence period time has not passed yet and it is not an appeal."
);
require(
dispute.drawnJurors[dispute.currentRound].length == dispute.nbVotes,
"The dispute has not finished drawing yet."
);
require(round.drawnJurors.length == dispute.nbVotes, "The dispute has not finished drawing yet.");
dispute.period = courts[dispute.subcourtID].hiddenVotes ? Period.commit : Period.vote;
} else if (dispute.period == Period.commit) {
// In case the jurors finished casting commits beforehand the dispute kit should call passPeriod() by itself.
Expand Down Expand Up @@ -448,20 +448,26 @@ contract KlerosCore is IArbitrator {
*/
function draw(uint256 _disputeID, uint256 _iterations) external {
Dispute storage dispute = disputes[_disputeID];
uint256 currentRound = dispute.rounds.length - 1;
Round storage round = dispute.rounds[currentRound];
require(dispute.period == Period.evidence, "Should be evidence period.");

IDisputeKit disputeKit = dispute.disputeKit;
uint256 startIndex = dispute.drawnJurors[dispute.currentRound].length;
uint256 startIndex = round.drawnJurors.length;
uint256 endIndex = startIndex + _iterations <= dispute.nbVotes ? startIndex + _iterations : dispute.nbVotes;

for (uint256 i = startIndex; i < endIndex; i++) {
address drawnAddress = disputeKit.draw(_disputeID);
if (drawnAddress != address(0)) {
// In case no one has staked at the court yet.
jurors[drawnAddress].lockedTokens[dispute.subcourtID] += dispute
.rounds[dispute.currentRound]
.tokensAtStakePerJuror;
dispute.drawnJurors[dispute.currentRound].push(drawnAddress);
emit Draw(drawnAddress, _disputeID, dispute.currentRound, i);
jurors[drawnAddress].lockedTokens[dispute.subcourtID] += round.tokensAtStakePerJuror;
require(
jurors[drawnAddress].stakedTokens[dispute.subcourtID] >=
jurors[drawnAddress].lockedTokens[dispute.subcourtID],
"Locked amount shouldn't exceed staked amount."
);
round.drawnJurors.push(drawnAddress);
emit Draw(drawnAddress, _disputeID, currentRound, i);
}
}
}
Expand Down Expand Up @@ -493,8 +499,6 @@ contract KlerosCore is IArbitrator {
ALPHA_DIVISOR;
extraRound.totalFeesForJurors = msg.value;

dispute.currentRound++;

emit AppealDecision(_disputeID, dispute.arbitrated);
emit NewPeriod(_disputeID, Period.evidence);
}
Expand All @@ -515,7 +519,7 @@ contract KlerosCore is IArbitrator {
uint256 end = dispute.rounds[_appeal].repartitions + _iterations;
uint256 penaltiesInRoundCache = dispute.rounds[_appeal].penalties; // For saving gas.

uint256 numberOfVotesInRound = dispute.drawnJurors[_appeal].length;
uint256 numberOfVotesInRound = dispute.rounds[_appeal].drawnJurors.length;
uint256 coherentCount = dispute.disputeKit.getCoherentCount(_disputeID, _appeal); // Total number of jurors that are eligible to a reward in this round.

address account; // Address of the juror.
Expand All @@ -539,14 +543,25 @@ contract KlerosCore is IArbitrator {
(ALPHA_DIVISOR - degreeOfCoherence)) / ALPHA_DIVISOR; // Fully coherent jurors won't be penalized.
penaltiesInRoundCache += penalty;

account = dispute.drawnJurors[_appeal][i];
account = dispute.rounds[_appeal].drawnJurors[i];
jurors[account].lockedTokens[dispute.subcourtID] -= penalty; // Release this part of locked tokens.
// TODO: properly update staked tokens in case of penalty.

// Can only update the stake if it is able to cover the minStake and penalty, otherwise unstake from the court.
if (jurors[account].stakedTokens[dispute.subcourtID] >= courts[dispute.subcourtID].minStake + penalty) {
setStakeForAccount(
account,
dispute.subcourtID,
jurors[account].stakedTokens[dispute.subcourtID] - penalty,
penalty
);
} else if (jurors[account].stakedTokens[dispute.subcourtID] != 0) {
setStakeForAccount(account, dispute.subcourtID, 0, penalty);
}

// Unstake the juror if he lost due to inactivity.
if (!dispute.disputeKit.isVoteActive(_disputeID, _appeal, i)) {
for (uint256 j = 0; j < jurors[account].subcourtIDs.length; j++)
setStakeForAccount(account, jurors[account].subcourtIDs[j], 0);
setStakeForAccount(account, jurors[account].subcourtIDs[j], 0, 0);
}
emit TokenAndETHShift(account, _disputeID, -int256(penalty), 0);

Expand All @@ -565,12 +580,19 @@ contract KlerosCore is IArbitrator {
i % numberOfVotesInRound
);
if (degreeOfCoherence > ALPHA_DIVISOR) degreeOfCoherence = ALPHA_DIVISOR;
account = dispute.drawnJurors[_appeal][i % 2];
account = dispute.rounds[_appeal].drawnJurors[i % numberOfVotesInRound];
// Release the rest of the tokens of the juror for this round.
jurors[account].lockedTokens[dispute.subcourtID] -=
(dispute.rounds[_appeal].tokensAtStakePerJuror * degreeOfCoherence) /
ALPHA_DIVISOR;
// TODO: properly update staked tokens in case of reward.

if (jurors[account].stakedTokens[dispute.subcourtID] == 0) {
// Give back the locked tokens in case the juror fully unstaked earlier.
pinakion.transfer(
account,
(dispute.rounds[_appeal].tokensAtStakePerJuror * degreeOfCoherence) / ALPHA_DIVISOR
);
}

uint256 tokenReward = ((penaltiesInRoundCache / coherentCount) * degreeOfCoherence) / ALPHA_DIVISOR;
uint256 ETHReward = ((dispute.rounds[_appeal].totalFeesForJurors / coherentCount) * degreeOfCoherence) /
Expand Down Expand Up @@ -656,8 +678,41 @@ contract KlerosCore is IArbitrator {
return disputeKit.currentRuling(_disputeID);
}

function getDispute(uint256 _disputeID) external view returns (Round[] memory) {
return disputes[_disputeID].rounds;
function getRoundInfo(uint256 _disputeID, uint256 _round)
external
view
returns (
uint256 tokensAtStakePerJuror,
uint256 totalFeesForJurors,
uint256 repartitions,
uint256 penalties,
address[] memory drawnJurors
)
{
Dispute storage dispute = disputes[_disputeID];
Round storage round = dispute.rounds[_round];
return (
round.tokensAtStakePerJuror,
round.totalFeesForJurors,
round.repartitions,
round.penalties,
round.drawnJurors
);
}

function getNumberOfRounds(uint256 _disputeID) external view returns (uint256) {
Dispute storage dispute = disputes[_disputeID];
return dispute.rounds.length;
}

function getJurorBalance(address _juror, uint96 _subcourtID)
external
view
returns (uint256 staked, uint256 locked)
{
Juror storage juror = jurors[_juror];
staked = juror.stakedTokens[_subcourtID];
locked = juror.lockedTokens[_subcourtID];
}

// ************************************* //
Expand Down Expand Up @@ -712,20 +767,24 @@ contract KlerosCore is IArbitrator {
* @param _account The address of the juror.
* @param _subcourtID The ID of the subcourt.
* @param _stake The new stake.
* @param _penalty Penalized amount won't be transferred back to juror when the stake is lowered.
* @return succeeded True if the call succeeded, false otherwise.
*/
function setStakeForAccount(
address _account,
uint96 _subcourtID,
uint256 _stake
) internal {
uint256 _stake,
uint256 _penalty
) internal returns (bool succeeded) {
Juror storage juror = jurors[_account];
bytes32 stakePathID = accountAndSubcourtIDToStakePathID(_account, _subcourtID);
uint256 currentStake = sortitionSumTrees.stakeOf(bytes32(uint256(_subcourtID)), stakePathID);

if (_stake != 0) {
require(_stake >= courts[_subcourtID].minStake, "Should stake at least minimal amount");
// Check against locked tokens in case the min stake was lowered.
if (_stake < courts[_subcourtID].minStake || _stake < juror.lockedTokens[_subcourtID]) return false;
if (currentStake == 0) {
require(juror.subcourtIDs.length < MAX_STAKE_PATHS, "Max stake paths reached");
if (juror.subcourtIDs.length >= MAX_STAKE_PATHS) return false;
juror.subcourtIDs.push(_subcourtID);
}
} else {
Expand All @@ -751,20 +810,28 @@ contract KlerosCore is IArbitrator {
else currentSubcourtID = courts[currentSubcourtID].parent;
}

emit StakeSet(_account, _subcourtID, _stake, newTotalStake);

uint256 transferredAmount;
if (_stake >= currentStake) {
require(
pinakion.transferFrom(_account, address(this), _stake - currentStake),
"Sender doesn't have enough tokens"
);
transferredAmount = _stake - currentStake;
if (transferredAmount > 0) {
if (!pinakion.transferFrom(_account, address(this), transferredAmount)) return false;
}
} else if (_stake == 0) {
// Keep locked tokens in the contract and release them after dispute is executed.
transferredAmount = currentStake - juror.lockedTokens[_subcourtID] - _penalty;
if (transferredAmount > 0) {
if (!pinakion.transfer(_account, transferredAmount)) return false;
}
} else {
// Keep locked tokens in the contract and release them after dispute is executed. Note that the current flow of staking is unfinished.
require(
pinakion.transfer(_account, currentStake - _stake - juror.lockedTokens[_subcourtID]),
"The transfer failed"
);
transferredAmount = currentStake - _stake - _penalty;
if (transferredAmount > 0) {
if (!pinakion.transfer(_account, transferredAmount)) return false;
}
}

emit StakeSet(_account, _subcourtID, _stake, newTotalStake);
return true;
}

/** @dev Gets a subcourt ID, the minimum number of jurors and an ID of a dispute kit from a specified extra data bytes array.
Expand Down Expand Up @@ -793,7 +860,7 @@ contract KlerosCore is IArbitrator {
}
if (subcourtID >= courts.length) subcourtID = 0;
if (minJurors == 0) minJurors = MIN_JURORS;
if (disputeKitID >= disputeKits.length) disputeKitID = 0;
if (disputeKits[disputeKitID] == IDisputeKit(address(0))) disputeKitID = 0;
} else {
subcourtID = 0;
minJurors = MIN_JURORS;
Expand Down