From c26f6e24901d9b2c19d328ed9adaecb534bea80e Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Tue, 19 Feb 2019 16:45:16 +0100 Subject: [PATCH 01/27] Copy contracts --- contracts/Court.sol | 842 ++++++++++++++++++ contracts/{ => lib}/HexSumTree.sol | 0 .../standards/arbitration/Arbitrable.sol | 51 ++ .../standards/arbitration/Arbitrator.sol | 94 ++ .../standards/arbitration/IArbitrable.sol | 54 ++ contracts/standards/rng/RNG.sol | 42 + contracts/test/HexSumTreePublic.sol | 2 +- package.json | 6 +- 8 files changed, 1089 insertions(+), 2 deletions(-) create mode 100644 contracts/Court.sol rename contracts/{ => lib}/HexSumTree.sol (100%) create mode 100644 contracts/standards/arbitration/Arbitrable.sol create mode 100644 contracts/standards/arbitration/Arbitrator.sol create mode 100644 contracts/standards/arbitration/IArbitrable.sol create mode 100644 contracts/standards/rng/RNG.sol diff --git a/contracts/Court.sol b/contracts/Court.sol new file mode 100644 index 00000000..554ebc0f --- /dev/null +++ b/contracts/Court.sol @@ -0,0 +1,842 @@ +pragma solidity ^0.4.24; // TODO: pin solc + +// Forked from: Kleros.sol https://github.com/kleros/kleros @ 7281e69 + +import "./standards/rng/RNG.sol"; +import "./standards/arbitration/Arbitrator.sol"; +import "./standards/arbitration/Arbitrable.sol"; +import { ApproveAndCallFallBack } from "@aragon/apps-shared-minime/contracts/MiniMeToken.sol"; +import "@aragon/os/contracts/lib/token/ERC20.sol"; + +// AUDIT(@izqui): Use of implicitely sized `uint` +// AUDIT(@izqui): Code format should be optimized for readability not reducing the amount of LOCs +// AUDIT(@izqui): Not using SafeMath should be reviewed in a case by case basis +// AUDIT(@izqui): Arbitration fees should be payable in an ERC20, no just ETH (can have native ETH support) +// AUDIT(@izqui): Incorrect function order +// AUDIT(@izqui): Use emit for events +// AUDIT(@izqui): Magic strings in revert reasons + + +contract Court is Arbitrator, ApproveAndCallFallBack { + + // **************************** // + // * Contract variables * // + // **************************** // + + // Variables which should not change after initialization. + ERC20 public jurorToken; + uint256 public constant NON_PAYABLE_AMOUNT = (2**256 - 2) / 2; // An astronomic amount, practically can't be paid. + + // Variables which will subject to the governance mechanism. + // Note they will only be able to be changed during the activation period (because a session assumes they don't change after it). + RNG public rng; // Random Number Generator used to draw jurors. + uint256 public arbitrationFeePerJuror = 0.05 ether; // The fee which will be paid to each juror. + uint16 public defaultNumberJuror = 3; // Number of drawn jurors unless specified otherwise. + uint256 public minActivatedToken = 0.1 * 1e18; // Minimum of tokens to be activated (in basic units). + uint[5] public timePerPeriod; // The minimum time each period lasts (seconds). + uint256 public alpha = 2000; // alpha in ‱ (1 / 10 000). + uint256 constant ALPHA_DIVISOR = 1e4; // Amount we need to divided alpha in ‱ to get the float value of alpha. + uint256 public maxAppeals = 5; // Number of times a dispute can be appealed. When exceeded appeal cost becomes NON_PAYABLE_AMOUNT. + // Initially, the governor will be an address controlled by the Kleros team. At a later stage, + // the governor will be switched to a governance contract with liquid voting. + address public governor; // Address of the governor contract. + + // Variables changing during day to day interaction. + uint256 public session = 1; // Current session of the court. + uint256 public lastPeriodChange; // The last time we changed of period (seconds). + uint256 public segmentSize; // Size of the segment of activated tokens. + uint256 public rnBlock; // The block linked with the RN which is requested. + uint256 public randomNumber; // Random number of the session. + + enum Period { + Activation, // When juror can deposit their tokens and parties give evidences. + Draw, // When jurors are drawn at random, note that this period is fast. + Vote, // Where jurors can vote on disputes. + Appeal, // When parties can appeal the rulings. + Execution // When where token redistribution occurs and Kleros call the arbitrated contracts. + } + + Period public period; // AUDIT(@izqui): It should be possible to many periods running in parallel + + struct Juror { + uint256 balance; // The amount of tokens the contract holds for this juror. + // Total number of tokens the jurors can loose in disputes they are drawn in. Those tokens are locked. Note that we can have atStake > balance but it should be statistically unlikely and does not pose issues. + uint256 atStake; + uint256 lastSession; // Last session the tokens were activated. + uint256 segmentStart; // Start of the segment of activated tokens. + uint256 segmentEnd; // End of the segment of activated tokens. + } + + mapping (address => Juror) public jurors; + + struct Vote { + address account; // The juror who casted the vote. + uint256 ruling; // The ruling which was given. + } + + struct VoteCounter { + uint256 winningChoice; // The choice which currently has the highest amount of votes. Is 0 in case of a tie. + uint256 winningCount; // The number of votes for winningChoice. Or for the choices which are tied. AUDIT(@izqui): Is this redundant? + mapping (uint256 => uint) voteCount; // voteCount[choice] is the number of votes for choice. + } + + enum DisputeState { // Not to be confused this with DisputeStatus in Arbitrator contract. + Open, // The dispute is opened but the outcome is not available yet (this include when jurors voted but appeal is still possible). + Resolving, // The token repartition has started. Note that if it's done in just one call, this state is skipped. + Executable, // The arbitrated contract can be called to enforce the decision. + Executed // Everything has been done and the dispute can't be interacted with anymore. + } + + struct Dispute { + Arbitrable arbitrated; // Contract to be arbitrated. + uint256 session; // First session the dispute was schedule. + uint256 appeals; // Number of appeals. + uint256 choices; // The number of choices available to the jurors. + uint16 initialNumberJurors; // The initial number of jurors. + uint256 arbitrationFeePerJuror; // The fee which will be paid to each juror. + DisputeState state; // The state of the dispute. + Vote[][] votes; // The votes in the form vote[appeals][voteID]. + VoteCounter[] voteCounter; // The vote counters in the form voteCounter[appeals]. + mapping (address => uint) lastSessionVote; // Last session a juror has voted on this dispute. Is 0 if he never did. + uint256 currentAppealToRepartition; // The current appeal we are repartitioning. + AppealsRepartitioned[] appealsRepartitioned; // Track a partially repartitioned appeal in the form AppealsRepartitioned[appeal]. + } + + enum RepartitionStage { // State of the token repartition if oneShotTokenRepartition would throw because there are too many votes. + Incoherent, + Coherent, + AtStake, + Complete + } + + struct AppealsRepartitioned { + uint256 totalToRedistribute; // Total amount of tokens we have to redistribute. + uint256 nbCoherent; // Number of coherent jurors for session. + uint256 currentIncoherentVote; // Current vote for the incoherent loop. + uint256 currentCoherentVote; // Current vote we need to count. + uint256 currentAtStakeVote; // Current vote we need to count. + RepartitionStage stage; // Use with multipleShotTokenRepartition if oneShotTokenRepartition would throw. + } + + Dispute[] public disputes; + + // **************************** // + // * Events * // + // **************************** // + + /** @dev Emitted when we pass to a new period. + * @param _period The new period. + * @param _session The current session. + */ + event NewPeriod(Period _period, uint256 indexed _session); + + /** @dev Emitted when a juror wins or loses tokens. + * @param _account The juror affected. + * @param _disputeID The ID of the dispute. + * @param _amount The amount of parts of token which was won. Can be negative for lost amounts. + */ + event TokenShift(address indexed _account, uint256 _disputeID, int _amount); + + /** @dev Emited when a juror wins arbitration fees. + * @param _account The account affected. + * @param _disputeID The ID of the dispute. + * @param _amount The amount of weis which was won. + */ + event ArbitrationReward(address indexed _account, uint256 _disputeID, uint256 _amount); + + // **************************** // + // * Modifiers * // + // **************************** // + + // AUDIT(@izqui): Code formatting + modifier onlyBy(address _account) {require(msg.sender == _account, "Wrong caller."); _;} + // AUDIT(@izqui): Currently not checking if the period should have been transitioned, so some periods can last longer if no one bothers to call `passPeriod()` + modifier onlyDuring(Period _period) {require(period == _period, "Wrong period."); _;} + modifier onlyGovernor() {require(msg.sender == governor, "Only callable by the governor."); _;} + + + /** @dev Constructor. + * @param _jurorToken The address of the jurorToken contract. + * @param _rng The random number generator which will be used. + * @param _timePerPeriod The minimal time for each period (seconds). + * @param _governor Address of the governor contract. + */ + constructor(ERC20 _jurorToken, RNG _rng, uint[5] _timePerPeriod, address _governor) public { + jurorToken = _jurorToken; + rng = _rng; + // solium-disable-next-line security/no-block-members + lastPeriodChange = block.timestamp; + timePerPeriod = _timePerPeriod; // AUDIT(@izqui): Verify the bytecode that solc produces here + governor = _governor; + } + + // **************************** // + // * Functions interacting * // + // * with ANJ contract * // + // **************************** // + + /** @dev Callback of approveAndCall - transfer jurorTokens of a juror in the contract. Should be called by the jurorToken contract. TRUSTED. + * @param _from The address making the transfer. + * @param _amount Amount of tokens to transfer to Kleros (in basic units). + */ + function receiveApproval(address _from, uint256 _amount, address token, bytes) public onlyBy(jurorToken) onlyBy(token) { + require(jurorToken.transferFrom(_from, this, _amount), "Transfer failed."); + + jurors[_from].balance += _amount; + } + + /** @dev Withdraw tokens. Note that we can't withdraw the tokens which are still atStake. + * Jurors can't withdraw their tokens if they have deposited some during this session. + * This is to prevent jurors from withdrawing tokens they could lose. + * @param _value The amount to withdraw. + */ + function withdraw(uint256 _value) public { + Juror storage juror = jurors[msg.sender]; + // Make sure that there is no more at stake than owned to avoid overflow. + require(juror.atStake <= juror.balance, "Balance is less than stake."); + require(_value <= juror.balance-juror.atStake, "Value is more than free balance."); // AUDIT(@izqui): Simpler to just safe math here + require(juror.lastSession != session, "You have deposited in this session."); + + juror.balance -= _value; + require(jurorToken.transfer(msg.sender,_value), "Transfer failed."); + } + + // **************************** // + // * Court functions * // + // * Modifying the state * // + // **************************** // + + // AUDIT(@izqui): This could automatically be triggered by any other court function that requires a period transition. + // AUDIT(@izqui): No incentive for anyone to call this, delaying to call the function can result in periods lasting longer. + + /** @dev To call to go to a new period. TRUSTED. + */ + function passPeriod() public { + // solium-disable-next-line security/no-block-members + uint256 time = block.timestamp; + require(time - lastPeriodChange >= timePerPeriod[uint8(period)], "Not enough time has passed."); + + if (period == Period.Activation) { + rnBlock = block.number + 1; + rng.requestRN(rnBlock); + period = Period.Draw; + } else if (period == Period.Draw) { + randomNumber = rng.getUncorrelatedRN(rnBlock); // AUDIT(@izqui): For the block number RNG the next period transition must be done within 256 blocks + require(randomNumber != 0, "Random number not ready yet."); + period = Period.Vote; + } else if (period == Period.Vote) { + period = Period.Appeal; + } else if (period == Period.Appeal) { + period = Period.Execution; + } else if (period == Period.Execution) { + period = Period.Activation; + ++session; + segmentSize = 0; + rnBlock = 0; + randomNumber = 0; + } + + lastPeriodChange = time; + emit NewPeriod(period, session); + } + + // AUDIT(@izqui): Really impractical to require jurors to send a transaction to activate every period. It costs ~50k gas per juror to activate per period (issue #2) + // AUDIT(@izqui): Jurors should provide either the period number or a TTL in case the transaction takes longer to mine resulting in a later activation + + /** @dev Deposit tokens in order to have chances of being drawn. Note that once tokens are deposited, + * there is no possibility of depositing more. + * @param _value Amount of tokens (in basic units) to deposit. + */ + function activateTokens(uint256 _value) public onlyDuring(Period.Activation) { + Juror storage juror = jurors[msg.sender]; + require(_value <= juror.balance, "Not enough balance."); + require(_value >= minActivatedToken, "Value is less than the minimum stake."); + // Verify that tokens were not already activated for this session. + require(juror.lastSession != session, "You have already activated in this session."); + + juror.lastSession = session; + juror.segmentStart = segmentSize; + segmentSize += _value; + juror.segmentEnd = segmentSize; + + } + + // AUDIT(@izqui): Lacking commit-reveal juror votes + // AUDIT(@izqui): Being drawn multiple times can lead to arbitration fees being kept by the contract and never distributed. + + /** @dev Vote a ruling. Juror must input the draw ID he was drawn. + * Note that the complexity is O(d), where d is amount of times the juror was drawn. + * Since being drawn multiple time is a rare occurrence and that a juror can always vote with less weight than it has, it is not a problem. + * But note that it can lead to arbitration fees being kept by the contract and never distributed. + * @param _disputeID The ID of the dispute the juror was drawn. + * @param _ruling The ruling given. + * @param _draws The list of draws the juror was drawn. Draw numbering starts at 1 and the numbers should be increasing. + */ + function voteRuling(uint256 _disputeID, uint256 _ruling, uint[] _draws) public onlyDuring(Period.Vote) { + Dispute storage dispute = disputes[_disputeID]; + Juror storage juror = jurors[msg.sender]; + VoteCounter storage voteCounter = dispute.voteCounter[dispute.appeals]; + require(dispute.lastSessionVote[msg.sender] != session, "You have already voted."); // Make sure juror hasn't voted yet. + require(_ruling <= dispute.choices, "Invalid ruling."); + // Note that it throws if the draws are incorrect. + require(validDraws(msg.sender, _disputeID, _draws), "Invalid draws."); + + dispute.lastSessionVote[msg.sender] = session; + voteCounter.voteCount[_ruling] += _draws.length; + if (voteCounter.winningCount < voteCounter.voteCount[_ruling]) { + voteCounter.winningCount = voteCounter.voteCount[_ruling]; + voteCounter.winningChoice = _ruling; + } else if (voteCounter.winningCount==voteCounter.voteCount[_ruling] && _draws.length!=0) { // Verify draw length to be non-zero to avoid the possibility of setting tie by casting 0 votes. + voteCounter.winningChoice = 0; // It's currently a tie. + } + for (uint256 i = 0; i < _draws.length; ++i) { + dispute.votes[dispute.appeals].push(Vote({ + account: msg.sender, + ruling: _ruling + })); + } + + juror.atStake += _draws.length * getStakePerDraw(); + uint256 feeToPay = _draws.length * dispute.arbitrationFeePerJuror; + msg.sender.transfer(feeToPay); + emit ArbitrationReward(msg.sender, _disputeID, feeToPay); + } + + /** @dev Steal part of the tokens and the arbitration fee of a juror who failed to vote. + * Note that a juror who voted but without all his weight can't be penalized. + * It is possible to not penalize with the maximum weight. + * But note that it can lead to arbitration fees being kept by the contract and never distributed. + * @param _jurorAddress Address of the juror to steal tokens from. + * @param _disputeID The ID of the dispute the juror was drawn. + * @param _draws The list of draws the juror was drawn. Numbering starts at 1 and the numbers should be increasing. + */ + function penalizeInactiveJuror(address _jurorAddress, uint256 _disputeID, uint[] _draws) public { + Dispute storage dispute = disputes[_disputeID]; + Juror storage inactiveJuror = jurors[_jurorAddress]; + require(period > Period.Vote, "Must be called after the vote period."); + require(dispute.lastSessionVote[_jurorAddress] != session, "Juror did vote."); // Verify the juror hasn't voted. + dispute.lastSessionVote[_jurorAddress] = session; // Update last session to avoid penalizing multiple times. + require(validDraws(_jurorAddress, _disputeID, _draws), "Invalid draws."); + uint256 penality = _draws.length * minActivatedToken * 2 * alpha / ALPHA_DIVISOR; + // Make sure the penality is not higher than the balance. + penality = (penality < inactiveJuror.balance) ? penality : inactiveJuror.balance; + inactiveJuror.balance -= penality; + emit TokenShift(_jurorAddress, _disputeID, -int(penality)); + jurors[msg.sender].balance += penality / 2; // Give half of the penalty to the caller. + emit TokenShift(msg.sender, _disputeID, int(penality / 2)); + jurors[governor].balance += penality / 2; // The other half to the governor. + emit TokenShift(governor, _disputeID, int(penality / 2)); + msg.sender.transfer(_draws.length*dispute.arbitrationFeePerJuror); // Give the arbitration fees to the caller. + } + + // AUDIT(@izqui): these two repartition functions could be simplified if the juror has to pull their own tokens. Total refactor required here. + // AUDIT(@izqui): once a dispute appeal's period passes this should be executable at any time, not only during execution periods + + /** @dev Execute all the token repartition. + * Note that this function could consume to much gas if there is too much votes. + * It is O(v), where v is the number of votes for this dispute. + * In the next version, there will also be a function to execute it in multiple calls + * (but note that one shot execution, if possible, is less expensive). + * @param _disputeID ID of the dispute. + */ + function oneShotTokenRepartition(uint256 _disputeID) public onlyDuring(Period.Execution) { + Dispute storage dispute = disputes[_disputeID]; + require(dispute.state == DisputeState.Open, "Dispute is not open."); + require(dispute.session + dispute.appeals <= session, "Dispute is still active."); + + uint256 winningChoice = dispute.voteCounter[dispute.appeals].winningChoice; + uint256 amountShift = getStakePerDraw(); + for (uint256 i = 0; i <= dispute.appeals; ++i) { + // If the result is not a tie, some parties are incoherent. Note that 0 (refuse to arbitrate) winning is not a tie. + // Result is a tie if the winningChoice is 0 (refuse to arbitrate) and the choice 0 is not the most voted choice. + // Note that in case of a "tie" among some choices including 0, parties who did not vote 0 are considered incoherent. + if (winningChoice!=0 || (dispute.voteCounter[dispute.appeals].voteCount[0] == dispute.voteCounter[dispute.appeals].winningCount)) { + uint256 totalToRedistribute = 0; + uint256 nbCoherent = 0; + // First loop to penalize the incoherent votes. + for (uint256 j = 0; j < dispute.votes[i].length; ++j) { + Vote storage vote = dispute.votes[i][j]; + if (vote.ruling != winningChoice) { + Juror storage juror = jurors[vote.account]; + uint256 penalty = amountShift= _maxIterations) { + return; + } + Vote storage vote = dispute.votes[i][j]; + if (vote.ruling != winningChoice) { + Juror storage juror = jurors[vote.account]; + uint256 penalty = amountShift= _maxIterations) { + return; + } + vote = dispute.votes[i][j]; + if (vote.ruling == winningChoice) { + juror = jurors[vote.account]; + juror.balance += toRedistribute; + emit TokenShift(vote.account, _disputeID, int(toRedistribute)); + } + + ++currentIterations; + ++dispute.appealsRepartitioned[i].currentCoherentVote; + } + + dispute.appealsRepartitioned[i].stage = RepartitionStage.AtStake; + } + } + + if (dispute.appealsRepartitioned[i].stage == RepartitionStage.AtStake) { + // Third loop to lower the atStake in order to unlock tokens. + for (j = dispute.appealsRepartitioned[i].currentAtStakeVote; j < dispute.votes[i].length; ++j) { + if (currentIterations >= _maxIterations) { + return; + } + vote = dispute.votes[i][j]; + juror = jurors[vote.account]; + juror.atStake -= amountShift; // Note that it can't underflow due to amountShift not changing between vote and redistribution. + + ++currentIterations; + ++dispute.appealsRepartitioned[i].currentAtStakeVote; + } + + dispute.appealsRepartitioned[i].stage = RepartitionStage.Complete; + } + + if (dispute.appealsRepartitioned[i].stage == RepartitionStage.Complete) { + ++dispute.currentAppealToRepartition; + } + } + + // AUDIT(@izqui): Since this requires the juror tokens to be distributed, rulings aren't executed until someone pays to distribute tokens + // As there is no incentive to do so, the party interested in the outcome will end up paying the gas for paying jurors + + dispute.state = DisputeState.Executable; + } + + // **************************** // + // * Court functions * // + // * Constant and Pure * // + // **************************** // + + /** @dev Return the amount of jurors which are or will be drawn in the dispute. + * The number of jurors is doubled and 1 is added at each appeal. We have proven the formula by recurrence. + * This avoid having a variable number of jurors which would be updated in order to save gas. + * @param _disputeID The ID of the dispute we compute the amount of jurors. + * @return nbJurors The number of jurors which are drawn. + */ + function amountJurors(uint256 _disputeID) public view returns (uint256 nbJurors) { + Dispute storage dispute = disputes[_disputeID]; + return (dispute.initialNumberJurors + 1) * 2 ** dispute.appeals - 1; + } + + /** @dev Must be used to verify that a juror has been draw at least _draws.length times. + * We have to require the user to specify the draws that lead the juror to be drawn. + * Because doing otherwise (looping through all draws) could consume too much gas. + * @param _jurorAddress Address of the juror we want to verify draws. + * @param _disputeID The ID of the dispute the juror was drawn. + * @param _draws The list of draws the juror was drawn. It draw numbering starts at 1 and the numbers should be increasing. + * Note that in most cases this list will just contain 1 number. + * @param valid true if the draws are valid. + */ + function validDraws(address _jurorAddress, uint256 _disputeID, uint[] _draws) public view returns (bool valid) { + uint256 draw = 0; + Juror storage juror = jurors[_jurorAddress]; + Dispute storage dispute = disputes[_disputeID]; + uint256 nbJurors = amountJurors(_disputeID); + + // AUDIT(@izqui): Code formatting + + if (juror.lastSession != session) return false; // Make sure that the tokens were deposited for this session. + if (dispute.session+dispute.appeals != session) return false; // Make sure there is currently a dispute. + if (period <= Period.Draw) return false; // Make sure that jurors are already drawn. + for (uint256 i = 0; i < _draws.length; ++i) { + if (_draws[i] <= draw) return false; // Make sure that draws are always increasing to avoid someone inputing the same multiple times. + draw = _draws[i]; + if (draw > nbJurors) return false; + uint256 position = uint(keccak256(randomNumber, _disputeID, draw)) % segmentSize; // Random position on the segment for draw. + require(position >= juror.segmentStart, "Invalid draw."); + require(position < juror.segmentEnd, "Invalid draw."); + } + + return true; + } + + // **************************** // + // * Arbitrator functions * // + // * Modifying the state * // + // **************************** // + + /** @dev Create a dispute. Must be called by the arbitrable contract. + * Must be paid at least arbitrationCost(). + * @param _choices Amount of choices the arbitrator can make in this dispute. + * @param _extraData Null for the default number. Otherwise, first 16 bytes will be used to return the number of jurors. + * @return disputeID ID of the dispute created. + */ + function createDispute(uint256 _choices, bytes _extraData) public payable returns (uint256 disputeID) { + uint16 nbJurors = extraDataToNbJurors(_extraData); + require(msg.value >= arbitrationCost(_extraData), "Not enough ETH to pay arbitration fees."); + + disputeID = disputes.length++; + Dispute storage dispute = disputes[disputeID]; + dispute.arbitrated = Arbitrable(msg.sender); + + if (period < Period.Draw) { // If drawing did not start schedule it for the current session. + dispute.session = session; + } else { // Otherwise schedule it for the next one. + dispute.session = session+1; + } + + dispute.choices = _choices; + dispute.initialNumberJurors = nbJurors; + // We store it as the general fee can be changed through the governance mechanism. + dispute.arbitrationFeePerJuror = arbitrationFeePerJuror; + dispute.votes.length++; // AUDIT(@izqui): Why it cannot be zero indexed? + dispute.voteCounter.length++; + + DisputeCreation(disputeID, Arbitrable(msg.sender)); + return disputeID; + } + + /** @dev Appeal a ruling. Note that it has to be called before the arbitrator contract calls rule. + * @param _disputeID ID of the dispute to be appealed. + */ + function appeal(uint256 _disputeID, bytes) public payable onlyDuring(Period.Appeal) { + bytes memory noAppealExtraData = new bytes(0); + super.appeal(_disputeID, noAppealExtraData); // AUDIT(@izqui): super.appeal just emits an event, inline it in this contract + Dispute storage dispute = disputes[_disputeID]; + require(msg.value >= appealCost(_disputeID, noAppealExtraData), "Not enough ETH to pay appeal fees."); + require(dispute.session+dispute.appeals == session, "Dispute is no longer active."); // Dispute of the current session. + require(dispute.arbitrated == msg.sender, "Caller is not the arbitrated contract."); + + dispute.appeals++; + dispute.votes.length++; + dispute.voteCounter.length++; + } + + // AUDIT(@izqui): Rulings can be executed infinite times, arbitrable contract should only be called once per ruling + + /** @dev Execute the ruling of a dispute which is in the state executable. UNTRUSTED. + * @param disputeID ID of the dispute to execute the ruling. + */ + function executeRuling(uint256 disputeID) public { + Dispute storage dispute = disputes[disputeID]; + require(dispute.state == DisputeState.Executable, "Dispute is not executable."); + + dispute.state = DisputeState.Executed; + dispute.arbitrated.rule(disputeID, dispute.voteCounter[dispute.appeals].winningChoice); + } + + // **************************** // + // * Arbitrator functions * // + // * Constant and pure * // + // **************************** // + + /** @dev Compute the cost of arbitration. It is recommended not to increase it often, + * as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. + * @param _extraData Null for the default number. Other first 16 bits will be used to return the number of jurors. + * @return fee Amount to be paid. + */ + function arbitrationCost(bytes _extraData) public view returns (uint256 fee) { + return extraDataToNbJurors(_extraData) * arbitrationFeePerJuror; + } + + /** @dev Compute the cost of appeal. It is recommended not to increase it often, + * as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. + * @param _disputeID ID of the dispute to be appealed. + * @return fee Amount to be paid. + */ + function appealCost(uint256 _disputeID, bytes) public view returns (uint256 fee) { + Dispute storage dispute = disputes[_disputeID]; + + if(dispute.appeals >= maxAppeals) return NON_PAYABLE_AMOUNT; + + return (2*amountJurors(_disputeID) + 1) * dispute.arbitrationFeePerJuror; + } + + /** @dev Compute the amount of jurors to be drawn. + * @param _extraData Null for the default number. Other first 16 bits will be used to return the number of jurors. + * Note that it does not check that the number of jurors is odd, but users are advised to choose a odd number of jurors. + */ + function extraDataToNbJurors(bytes _extraData) internal view returns (uint16 nbJurors) { + if (_extraData.length < 2) + return defaultNumberJuror; + else + return (uint16(_extraData[0]) << 8) + uint16(_extraData[1]); + } + + /** @dev Compute the minimum activated jurorTokens in alpha. + * Note there may be multiple draws for a single user on a single dispute. + */ + function getStakePerDraw() public view returns (uint256 minActivatedTokenInAlpha) { + return (alpha * minActivatedToken) / ALPHA_DIVISOR; + } + + + // **************************** // + // * Constant getters * // + // **************************** // + + /** @dev Getter for account in Vote. + * @param _disputeID ID of the dispute. + * @param _appeals Which appeal (or 0 for the initial session). + * @param _voteID The ID of the vote for this appeal (or initial session). + * @return account The address of the voter. + */ + function getVoteAccount(uint256 _disputeID, uint256 _appeals, uint256 _voteID) public view returns (address account) { + return disputes[_disputeID].votes[_appeals][_voteID].account; + } + + /** @dev Getter for ruling in Vote. + * @param _disputeID ID of the dispute. + * @param _appeals Which appeal (or 0 for the initial session). + * @param _voteID The ID of the vote for this appeal (or initial session). + * @return ruling The ruling given by the voter. + */ + function getVoteRuling(uint256 _disputeID, uint256 _appeals, uint256 _voteID) public view returns (uint256 ruling) { + return disputes[_disputeID].votes[_appeals][_voteID].ruling; + } + + /** @dev Getter for winningChoice in VoteCounter. + * @param _disputeID ID of the dispute. + * @param _appeals Which appeal (or 0 for the initial session). + * @return winningChoice The currently winning choice (or 0 if it's tied). Note that 0 can also be return if the majority refuses to arbitrate. + */ + function getWinningChoice(uint256 _disputeID, uint256 _appeals) public view returns (uint256 winningChoice) { + return disputes[_disputeID].voteCounter[_appeals].winningChoice; + } + + /** @dev Getter for winningCount in VoteCounter. + * @param _disputeID ID of the dispute. + * @param _appeals Which appeal (or 0 for the initial session). + * @return winningCount The amount of votes the winning choice (or those who are tied) has. + */ + function getWinningCount(uint256 _disputeID, uint256 _appeals) public view returns (uint256 winningCount) { + return disputes[_disputeID].voteCounter[_appeals].winningCount; + } + + /** @dev Getter for voteCount in VoteCounter. + * @param _disputeID ID of the dispute. + * @param _appeals Which appeal (or 0 for the initial session). + * @param _choice The choice. + * @return voteCount The amount of votes the winning choice (or those who are tied) has. + */ + function getVoteCount(uint256 _disputeID, uint256 _appeals, uint256 _choice) public view returns (uint256 voteCount) { + return disputes[_disputeID].voteCounter[_appeals].voteCount[_choice]; + } + + /** @dev Getter for lastSessionVote in Dispute. + * @param _disputeID ID of the dispute. + * @param _juror The juror we want to get the last session he voted. + * @return lastSessionVote The last session the juror voted. + */ + function getLastSessionVote(uint256 _disputeID, address _juror) public view returns (uint256 lastSessionVote) { + return disputes[_disputeID].lastSessionVote[_juror]; + } + + /** @dev Is the juror drawn in the draw of the dispute. + * @param _disputeID ID of the dispute. + * @param _juror The juror. + * @param _draw The draw. Note that it starts at 1. + * @return drawn True if the juror is drawn, false otherwise. + */ + function isDrawn(uint256 _disputeID, address _juror, uint256 _draw) public view returns (bool drawn) { + Dispute storage dispute = disputes[_disputeID]; + Juror storage juror = jurors[_juror]; + if ( // AUDIT(@izqui): Code smell + juror.lastSession != session || (dispute.session + dispute.appeals != session) || period <= Period.Draw || _draw > amountJurors(_disputeID) || _draw == 0 || segmentSize == 0 + ) { + return false; + } else { + uint256 position = uint(keccak256(randomNumber,_disputeID,_draw)) % segmentSize; // AUDIT(@izqui): Use `validDrawns` or move hashing calculation to a pure function + return (position >= juror.segmentStart) && (position < juror.segmentEnd); + } + + } + + /** @dev Return the current ruling of a dispute. This is useful for parties to know if they should appeal. + * @param _disputeID ID of the dispute. + * @return ruling The current ruling which will be given if there is no appeal. If it is not available, return 0. + */ + function currentRuling(uint256 _disputeID) public view returns (uint256 ruling) { + Dispute storage dispute = disputes[_disputeID]; + return dispute.voteCounter[dispute.appeals].winningChoice; + } + + /** @dev Return the status of a dispute. + * @param _disputeID ID of the dispute to rule. + * @return status The status of the dispute. + */ + function disputeStatus(uint256 _disputeID) public view returns (DisputeStatus status) { + // AUDIT(@izqui): Code smell + + Dispute storage dispute = disputes[_disputeID]; + if (dispute.session+dispute.appeals < session) // Dispute of past session. + return DisputeStatus.Solved; + else if(dispute.session+dispute.appeals == session) { // Dispute of current session. + if (dispute.state == DisputeState.Open) { + if (period < Period.Appeal) + return DisputeStatus.Waiting; + else if (period == Period.Appeal) + return DisputeStatus.Appealable; + else return DisputeStatus.Solved; + } else return DisputeStatus.Solved; + } else return DisputeStatus.Waiting; // Dispute for future session. + } + + // **************************** // + // * Governor Functions * // + // **************************** // + + // AUDIT(@izqui): Governor could steal juror fees + + /** @dev General call function where the contract execute an arbitrary call with data and ETH following governor orders. + * @param _data Transaction data. + * @param _value Transaction value. + * @param _target Transaction target. + */ + function executeOrder(bytes32 _data, uint256 _value, address _target) public onlyGovernor { + _target.call.value(_value)(_data); // solium-disable-line security/no-call-value + } + + /** @dev Setter for rng. + * @param _rng An instance of RNG. + */ + function setRng(RNG _rng) public onlyGovernor { + rng = _rng; + } + + /** @dev Setter for arbitrationFeePerJuror. + * @param _arbitrationFeePerJuror The fee which will be paid to each juror. + */ + function setArbitrationFeePerJuror(uint256 _arbitrationFeePerJuror) public onlyGovernor { + arbitrationFeePerJuror = _arbitrationFeePerJuror; + } + + /** @dev Setter for defaultNumberJuror. + * @param _defaultNumberJuror Number of drawn jurors unless specified otherwise. + */ + function setDefaultNumberJuror(uint16 _defaultNumberJuror) public onlyGovernor { + defaultNumberJuror = _defaultNumberJuror; + } + + /** @dev Setter for minActivatedToken. + * @param _minActivatedToken Minimum of tokens to be activated (in basic units). + */ + function setMinActivatedToken(uint256 _minActivatedToken) public onlyGovernor { + minActivatedToken = _minActivatedToken; + } + + /** @dev Setter for timePerPeriod. + * @param _timePerPeriod The minimum time each period lasts (seconds). + */ + function setTimePerPeriod(uint[5] _timePerPeriod) public onlyGovernor { + timePerPeriod = _timePerPeriod; + } + + /** @dev Setter for alpha. + * @param _alpha Alpha in ‱. + */ + function setAlpha(uint256 _alpha) public onlyGovernor { + alpha = _alpha; + } + + /** @dev Setter for maxAppeals. + * @param _maxAppeals Number of times a dispute can be appealed. When exceeded appeal cost becomes NON_PAYABLE_AMOUNT. + */ + function setMaxAppeals(uint256 _maxAppeals) public onlyGovernor { + maxAppeals = _maxAppeals; + } + + /** @dev Setter for governor. + * @param _governor Address of the governor contract. + */ + function setGovernor(address _governor) public onlyGovernor { + governor = _governor; + } +} \ No newline at end of file diff --git a/contracts/HexSumTree.sol b/contracts/lib/HexSumTree.sol similarity index 100% rename from contracts/HexSumTree.sol rename to contracts/lib/HexSumTree.sol diff --git a/contracts/standards/arbitration/Arbitrable.sol b/contracts/standards/arbitration/Arbitrable.sol new file mode 100644 index 00000000..4e01152c --- /dev/null +++ b/contracts/standards/arbitration/Arbitrable.sol @@ -0,0 +1,51 @@ +/** + * @title Arbitrable + * @author Clément Lesaege - + * Bug Bounties: This code hasn't undertaken a bug bounty program yet. + */ + +pragma solidity ^0.4.15; + +import "./IArbitrable.sol"; + +/** @title Arbitrable + * Arbitrable abstract contract. + * When developing arbitrable contracts, we need to: + * -Define the action taken when a ruling is received by the contract. We should do so in executeRuling. + * -Allow dispute creation. For this a function must: + * -Call arbitrator.createDispute.value(_fee)(_choices,_extraData); + * -Create the event Dispute(_arbitrator,_disputeID,_rulingOptions); + */ +contract Arbitrable is IArbitrable { + Arbitrator public arbitrator; + bytes public arbitratorExtraData; // Extra data to require particular dispute and appeal behaviour. + + modifier onlyArbitrator {require(msg.sender == address(arbitrator), "Can only be called by the arbitrator."); _;} + + /** @dev Constructor. Choose the arbitrator. + * @param _arbitrator The arbitrator of the contract. + * @param _arbitratorExtraData Extra data for the arbitrator. + */ + constructor(Arbitrator _arbitrator, bytes _arbitratorExtraData) public { + arbitrator = _arbitrator; + arbitratorExtraData = _arbitratorExtraData; + } + + /** @dev Give a ruling for a dispute. Must be called by the arbitrator. + * The purpose of this function is to ensure that the address calling it has the right to rule on the contract. + * @param _disputeID ID of the dispute in the Arbitrator contract. + * @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Not able/wanting to make a decision". + */ + function rule(uint _disputeID, uint _ruling) public onlyArbitrator { + emit Ruling(Arbitrator(msg.sender),_disputeID,_ruling); + + executeRuling(_disputeID,_ruling); + } + + + /** @dev Execute a ruling of a dispute. + * @param _disputeID ID of the dispute in the Arbitrator contract. + * @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Not able/wanting to make a decision". + */ + function executeRuling(uint _disputeID, uint _ruling) internal; +} diff --git a/contracts/standards/arbitration/Arbitrator.sol b/contracts/standards/arbitration/Arbitrator.sol new file mode 100644 index 00000000..c732788b --- /dev/null +++ b/contracts/standards/arbitration/Arbitrator.sol @@ -0,0 +1,94 @@ +/** + * @title Arbitrator + * @author Clément Lesaege - + * Bug Bounties: This code hasn't undertaken a bug bounty program yet. + */ + +pragma solidity ^0.4.15; + +import "./Arbitrable.sol"; + +/** @title Arbitrator + * Arbitrator abstract contract. + * When developing arbitrator contracts we need to: + * -Define the functions for dispute creation (createDispute) and appeal (appeal). Don't forget to store the arbitrated contract and the disputeID (which should be unique, use nbDisputes). + * -Define the functions for cost display (arbitrationCost and appealCost). + * -Allow giving rulings. For this a function must call arbitrable.rule(disputeID, ruling). + */ +contract Arbitrator { + + enum DisputeStatus {Waiting, Appealable, Solved} + + modifier requireArbitrationFee(bytes _extraData) { + require(msg.value >= arbitrationCost(_extraData), "Not enough ETH to cover arbitration costs."); + _; + } + modifier requireAppealFee(uint _disputeID, bytes _extraData) { + require(msg.value >= appealCost(_disputeID, _extraData), "Not enough ETH to cover appeal costs."); + _; + } + + /** @dev To be raised when a dispute is created. + * @param _disputeID ID of the dispute. + * @param _arbitrable The contract which created the dispute. + */ + event DisputeCreation(uint indexed _disputeID, Arbitrable indexed _arbitrable); + + /** @dev To be raised when a dispute can be appealed. + * @param _disputeID ID of the dispute. + */ + event AppealPossible(uint indexed _disputeID, Arbitrable indexed _arbitrable); + + /** @dev To be raised when the current ruling is appealed. + * @param _disputeID ID of the dispute. + * @param _arbitrable The contract which created the dispute. + */ + event AppealDecision(uint indexed _disputeID, Arbitrable indexed _arbitrable); + + /** @dev Create a dispute. Must be called by the arbitrable contract. + * Must be paid at least arbitrationCost(_extraData). + * @param _choices Amount of choices the arbitrator can make in this dispute. + * @param _extraData Can be used to give additional info on the dispute to be created. + * @return disputeID ID of the dispute created. + */ + function createDispute(uint _choices, bytes _extraData) public requireArbitrationFee(_extraData) payable returns(uint disputeID) {} + + /** @dev Compute the cost of arbitration. It is recommended not to increase it often, as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. + * @param _extraData Can be used to give additional info on the dispute to be created. + * @return fee Amount to be paid. + */ + function arbitrationCost(bytes _extraData) public view returns(uint fee); + + /** @dev Appeal a ruling. Note that it has to be called before the arbitrator contract calls rule. + * @param _disputeID ID of the dispute to be appealed. + * @param _extraData Can be used to give extra info on the appeal. + */ + function appeal(uint _disputeID, bytes _extraData) public requireAppealFee(_disputeID,_extraData) payable { + emit AppealDecision(_disputeID, Arbitrable(msg.sender)); + } + + /** @dev Compute the cost of appeal. It is recommended not to increase it often, as it can be higly time and gas consuming for the arbitrated contracts to cope with fee augmentation. + * @param _disputeID ID of the dispute to be appealed. + * @param _extraData Can be used to give additional info on the dispute to be created. + * @return fee Amount to be paid. + */ + function appealCost(uint _disputeID, bytes _extraData) public view returns(uint fee); + + /** @dev Compute the start and end of the dispute's current or next appeal period, if possible. + * @param _disputeID ID of the dispute. + * @return The start and end of the period. + */ + function appealPeriod(uint _disputeID) public view returns(uint start, uint end) {} + + /** @dev Return the status of a dispute. + * @param _disputeID ID of the dispute to rule. + * @return status The status of the dispute. + */ + function disputeStatus(uint _disputeID) public view returns(DisputeStatus status); + + /** @dev Return the current ruling of a dispute. This is useful for parties to know if they should appeal. + * @param _disputeID ID of the dispute. + * @return ruling The ruling which has been given or the one which will be given if there is no appeal. + */ + function currentRuling(uint _disputeID) public view returns(uint ruling); +} diff --git a/contracts/standards/arbitration/IArbitrable.sol b/contracts/standards/arbitration/IArbitrable.sol new file mode 100644 index 00000000..381cb17d --- /dev/null +++ b/contracts/standards/arbitration/IArbitrable.sol @@ -0,0 +1,54 @@ +/** + * @title IArbitrable + * @author Enrique Piqueras - + * Bug Bounties: This code hasn't undertaken a bug bounty program yet. + */ + +pragma solidity ^0.4.15; + +import "./Arbitrator.sol"; + +/** @title IArbitrable + * Arbitrable interface. + * When developing arbitrable contracts, we need to: + * -Define the action taken when a ruling is received by the contract. We should do so in executeRuling. + * -Allow dispute creation. For this a function must: + * -Call arbitrator.createDispute.value(_fee)(_choices,_extraData); + * -Create the event Dispute(_arbitrator,_disputeID,_rulingOptions); + */ +interface IArbitrable { + /** @dev To be emmited when meta-evidence is submitted. + * @param _metaEvidenceID Unique identifier of meta-evidence. + * @param _evidence A link to the meta-evidence JSON. + */ + event MetaEvidence(uint indexed _metaEvidenceID, string _evidence); + + /** @dev To be emmited when a dispute is created to link the correct meta-evidence to the disputeID + * @param _arbitrator The arbitrator of the contract. + * @param _disputeID ID of the dispute in the Arbitrator contract. + * @param _metaEvidenceID Unique identifier of meta-evidence. + */ + event Dispute(Arbitrator indexed _arbitrator, uint indexed _disputeID, uint _metaEvidenceID); + + /** @dev To be raised when evidence are submitted. Should point to the ressource (evidences are not to be stored on chain due to gas considerations). + * @param _arbitrator The arbitrator of the contract. + * @param _disputeID ID of the dispute in the Arbitrator contract. + * @param _party The address of the party submiting the evidence. Note that 0x0 refers to evidence not submitted by any party. + * @param _evidence A URI to the evidence JSON file whose name should be its keccak256 hash followed by .json. + */ + event Evidence(Arbitrator indexed _arbitrator, uint indexed _disputeID, address indexed _party, string _evidence); + + /** @dev To be raised when a ruling is given. + * @param _arbitrator The arbitrator giving the ruling. + * @param _disputeID ID of the dispute in the Arbitrator contract. + * @param _ruling The ruling which was given. + */ + event Ruling(Arbitrator indexed _arbitrator, uint indexed _disputeID, uint _ruling); + + /** @dev Give a ruling for a dispute. Must be called by the arbitrator. + * The purpose of this function is to ensure that the address calling it has the right to rule on the contract. + * @param _disputeID ID of the dispute in the Arbitrator contract. + * @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Not able/wanting to make a decision". + */ + function rule(uint _disputeID, uint _ruling) public; +} diff --git a/contracts/standards/rng/RNG.sol b/contracts/standards/rng/RNG.sol new file mode 100644 index 00000000..1ce4e988 --- /dev/null +++ b/contracts/standards/rng/RNG.sol @@ -0,0 +1,42 @@ +/** +* @title Random Number Generator Standard +* @author Clément Lesaege - +* +*/ + +pragma solidity ^0.4.15; + +contract RNG{ + + /** @dev Contribute to the reward of a random number. + * @param _block Block the random number is linked to. + */ + function contribute(uint _block) public payable; + + /** @dev Request a random number. + * @param _block Block linked to the request. + */ + function requestRN(uint _block) public payable { + contribute(_block); + } + + /** @dev Get the random number. + * @param _block Block the random number is linked to. + * @return RN Random Number. If the number is not ready or has not been required 0 instead. + */ + function getRN(uint _block) public returns (uint RN); + + /** @dev Get a uncorrelated random number. Act like getRN but give a different number for each sender. + * This is to prevent users from getting correlated numbers. + * @param _block Block the random number is linked to. + * @return RN Random Number. If the number is not ready or has not been required 0 instead. + */ + function getUncorrelatedRN(uint _block) public returns (uint RN) { + uint baseRN = getRN(_block); + if (baseRN == 0) + return 0; + else + return uint(keccak256(msg.sender,baseRN)); + } + +} diff --git a/contracts/test/HexSumTreePublic.sol b/contracts/test/HexSumTreePublic.sol index 547df1e4..12b2a1d4 100644 --- a/contracts/test/HexSumTreePublic.sol +++ b/contracts/test/HexSumTreePublic.sol @@ -1,6 +1,6 @@ pragma solidity ^0.4.24; -import "../HexSumTree.sol"; +import "../lib/HexSumTree.sol"; contract HexSumTreePublic { diff --git a/package.json b/package.json index 82270446..031d8f3c 100644 --- a/package.json +++ b/package.json @@ -12,5 +12,9 @@ "@aragon/test-helpers": "^1.0.1", "truffle": "^4.1.14", "truffle-bytecode-manager": "^1.1.1" + }, + "dependencies": { + "@aragon/apps-shared-minime": "^1.0.1", + "@aragon/os": "^4.1.0-rc.1" } -} +} \ No newline at end of file From 7ff426fc6427a2ca07e5a3ed930c3501fabf580d Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Tue, 19 Feb 2019 19:44:02 +0100 Subject: [PATCH 02/27] Implement ERC900 --- contracts/Court.sol | 171 +++++++++++++++++++++++++++++--------------- 1 file changed, 114 insertions(+), 57 deletions(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index 554ebc0f..ef00da63 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -5,10 +5,11 @@ pragma solidity ^0.4.24; // TODO: pin solc import "./standards/rng/RNG.sol"; import "./standards/arbitration/Arbitrator.sol"; import "./standards/arbitration/Arbitrable.sol"; +import "./standards/erc900/ERC900.sol"; + import { ApproveAndCallFallBack } from "@aragon/apps-shared-minime/contracts/MiniMeToken.sol"; import "@aragon/os/contracts/lib/token/ERC20.sol"; -// AUDIT(@izqui): Use of implicitely sized `uint` // AUDIT(@izqui): Code format should be optimized for readability not reducing the amount of LOCs // AUDIT(@izqui): Not using SafeMath should be reviewed in a case by case basis // AUDIT(@izqui): Arbitration fees should be payable in an ERC20, no just ETH (can have native ETH support) @@ -17,20 +18,15 @@ import "@aragon/os/contracts/lib/token/ERC20.sol"; // AUDIT(@izqui): Magic strings in revert reasons -contract Court is Arbitrator, ApproveAndCallFallBack { - - // **************************** // - // * Contract variables * // - // **************************** // - +contract Court is ERC900, Arbitrator, ApproveAndCallFallBack { // Variables which should not change after initialization. ERC20 public jurorToken; uint256 public constant NON_PAYABLE_AMOUNT = (2**256 - 2) / 2; // An astronomic amount, practically can't be paid. - // Variables which will subject to the governance mechanism. - // Note they will only be able to be changed during the activation period (because a session assumes they don't change after it). - RNG public rng; // Random Number Generator used to draw jurors. - uint256 public arbitrationFeePerJuror = 0.05 ether; // The fee which will be paid to each juror. + // Config variables modifiable by the governor during activation phse + RNG public rng; + ERC20 public feeToken; + uint256 public feeAmount; // The fee which will be paid to each juror. uint16 public defaultNumberJuror = 3; // Number of drawn jurors unless specified otherwise. uint256 public minActivatedToken = 0.1 * 1e18; // Minimum of tokens to be activated (in basic units). uint[5] public timePerPeriod; // The minimum time each period lasts (seconds). @@ -59,7 +55,7 @@ contract Court is Arbitrator, ApproveAndCallFallBack { Period public period; // AUDIT(@izqui): It should be possible to many periods running in parallel struct Juror { - uint256 balance; // The amount of tokens the contract holds for this juror. + mapping (address => uint256) balances; // token addr -> balance // Total number of tokens the jurors can loose in disputes they are drawn in. Those tokens are locked. Note that we can have atStake > balance but it should be statistically unlikely and does not pose issues. uint256 atStake; uint256 lastSession; // Last session the tokens were activated. @@ -93,7 +89,7 @@ contract Court is Arbitrator, ApproveAndCallFallBack { uint256 appeals; // Number of appeals. uint256 choices; // The number of choices available to the jurors. uint16 initialNumberJurors; // The initial number of jurors. - uint256 arbitrationFeePerJuror; // The fee which will be paid to each juror. + uint256 feeAmount; // The fee which will be paid to each juror. DisputeState state; // The state of the dispute. Vote[][] votes; // The votes in the form vote[appeals][voteID]. VoteCounter[] voteCounter; // The vote counters in the form voteCounter[appeals]. @@ -156,49 +152,107 @@ contract Court is Arbitrator, ApproveAndCallFallBack { /** @dev Constructor. - * @param _jurorToken The address of the jurorToken contract. + * @param _jurorToken The address of the juror work token contract. + * @param _feeToken The address of the token contract that is used to pay for fees. + * @param _feeAmount The amount of _feeToken that is paid per juror per dispute * @param _rng The random number generator which will be used. * @param _timePerPeriod The minimal time for each period (seconds). * @param _governor Address of the governor contract. */ - constructor(ERC20 _jurorToken, RNG _rng, uint[5] _timePerPeriod, address _governor) public { + constructor( + ERC20 _jurorToken, + ERC20 _feeToken, + uint256 _feeAmount, + RNG _rng, + uint[5] _timePerPeriod, + address _governor + ) public { jurorToken = _jurorToken; rng = _rng; + + feeToken = _feeToken; + feeAmount = _feeAmount; + // solium-disable-next-line security/no-block-members lastPeriodChange = block.timestamp; timePerPeriod = _timePerPeriod; // AUDIT(@izqui): Verify the bytecode that solc produces here governor = _governor; } - // **************************** // - // * Functions interacting * // - // * with ANJ contract * // - // **************************** // + // ERC900 + + function stake(uint256 _amount, bytes) external { + _stake(msg.sender, msg.sender, _amount); + } + + function stakeFor(address _to, uint256 _amount, bytes) external { + _stake(msg.sender, _to, _amount); + } /** @dev Callback of approveAndCall - transfer jurorTokens of a juror in the contract. Should be called by the jurorToken contract. TRUSTED. * @param _from The address making the transfer. * @param _amount Amount of tokens to transfer to Kleros (in basic units). */ - function receiveApproval(address _from, uint256 _amount, address token, bytes) public onlyBy(jurorToken) onlyBy(token) { + function receiveApproval(address _from, uint256 _amount, address token, bytes) + public + onlyBy(jurorToken) + onlyBy(token) + { + _stake(_from, _from, _amount); + } + + function _stake(address _from, address _to, uint256 _amount) internal { require(jurorToken.transferFrom(_from, this, _amount), "Transfer failed."); - jurors[_from].balance += _amount; + jurors[_to].balances[jurorToken] += _amount; + + emit Staked(_to, _amount, totalStakedFor(_to), ""); + } + + function unstake(uint256 _amount, bytes) external { + return withdraw(jurorToken, _amount); + } + + function totalStakedFor(address _addr) public view returns (uint256) { + return jurors[_addr].balances[jurorToken]; + } + + function totalStaked() external view returns (uint256) { + return jurorToken.balanceOf(this); + } + + function token() external view returns (address) { + return address(jurorToken); + } + + function supportsHistory() external pure returns (bool) { + return false; } /** @dev Withdraw tokens. Note that we can't withdraw the tokens which are still atStake. * Jurors can't withdraw their tokens if they have deposited some during this session. * This is to prevent jurors from withdrawing tokens they could lose. - * @param _value The amount to withdraw. + * @param _token Token to withdraw + * @param _amount The amount to withdraw. */ - function withdraw(uint256 _value) public { - Juror storage juror = jurors[msg.sender]; - // Make sure that there is no more at stake than owned to avoid overflow. - require(juror.atStake <= juror.balance, "Balance is less than stake."); - require(_value <= juror.balance-juror.atStake, "Value is more than free balance."); // AUDIT(@izqui): Simpler to just safe math here - require(juror.lastSession != session, "You have deposited in this session."); + function withdraw(ERC20 _token, uint256 _amount) public { + address jurorAddress = msg.sender; + + Juror storage juror = jurors[jurorAddress]; + + uint256 balance = juror.balances[_token]; - juror.balance -= _value; - require(jurorToken.transfer(msg.sender,_value), "Transfer failed."); + if (_token == jurorToken) { + // Make sure that there is no more at stake than owned to avoid overflow. + require(juror.atStake <= balance, "Balance is less than stake."); + require(_amount <= balance - juror.atStake, "Value is more than free balance."); // AUDIT(@izqui): Simpler to just safe math here + require(juror.lastSession != session, "You have deposited in this session."); + + emit Unstaked(jurorAddress, _amount, totalStakedFor(jurorAddress), ""); + } + + juror.balances[jurorToken] -= _amount; + require(jurorToken.transfer(jurorAddress, _amount), "Transfer failed."); } // **************************** // @@ -249,7 +303,7 @@ contract Court is Arbitrator, ApproveAndCallFallBack { */ function activateTokens(uint256 _value) public onlyDuring(Period.Activation) { Juror storage juror = jurors[msg.sender]; - require(_value <= juror.balance, "Not enough balance."); + require(_value <= juror.balances[jurorToken], "Not enough balance."); require(_value >= minActivatedToken, "Value is less than the minimum stake."); // Verify that tokens were not already activated for this session. require(juror.lastSession != session, "You have already activated in this session."); @@ -297,7 +351,7 @@ contract Court is Arbitrator, ApproveAndCallFallBack { } juror.atStake += _draws.length * getStakePerDraw(); - uint256 feeToPay = _draws.length * dispute.arbitrationFeePerJuror; + uint256 feeToPay = _draws.length * dispute.feeAmount; msg.sender.transfer(feeToPay); emit ArbitrationReward(msg.sender, _disputeID, feeToPay); } @@ -317,16 +371,19 @@ contract Court is Arbitrator, ApproveAndCallFallBack { require(dispute.lastSessionVote[_jurorAddress] != session, "Juror did vote."); // Verify the juror hasn't voted. dispute.lastSessionVote[_jurorAddress] = session; // Update last session to avoid penalizing multiple times. require(validDraws(_jurorAddress, _disputeID, _draws), "Invalid draws."); - uint256 penality = _draws.length * minActivatedToken * 2 * alpha / ALPHA_DIVISOR; - // Make sure the penality is not higher than the balance. - penality = (penality < inactiveJuror.balance) ? penality : inactiveJuror.balance; - inactiveJuror.balance -= penality; - emit TokenShift(_jurorAddress, _disputeID, -int(penality)); - jurors[msg.sender].balance += penality / 2; // Give half of the penalty to the caller. - emit TokenShift(msg.sender, _disputeID, int(penality / 2)); - jurors[governor].balance += penality / 2; // The other half to the governor. - emit TokenShift(governor, _disputeID, int(penality / 2)); - msg.sender.transfer(_draws.length*dispute.arbitrationFeePerJuror); // Give the arbitration fees to the caller. + uint256 penalty = _draws.length * minActivatedToken * 2 * alpha / ALPHA_DIVISOR; + // Make sure the penalty is not higher than the balance. + uint256 jurorStake = inactiveJuror.balances[jurorToken]; + if (penalty >= jurorStake) { + penalty = jurorStake; + } + inactiveJuror.balances[jurorToken] -= penalty; + emit TokenShift(_jurorAddress, _disputeID, -int(penalty)); + jurors[msg.sender].balances[jurorToken] += penalty / 2; // Give half of the penalty to the caller. + emit TokenShift(msg.sender, _disputeID, int(penalty / 2)); + jurors[governor].balances[jurorToken] += penalty / 2; // The other half to the governor. + emit TokenShift(governor, _disputeID, int(penalty / 2)); + msg.sender.transfer(_draws.length*dispute.feeAmount); // Give the arbitration fees to the caller. } // AUDIT(@izqui): these two repartition functions could be simplified if the juror has to pull their own tokens. Total refactor required here. @@ -358,8 +415,8 @@ contract Court is Arbitrator, ApproveAndCallFallBack { Vote storage vote = dispute.votes[i][j]; if (vote.ruling != winningChoice) { Juror storage juror = jurors[vote.account]; - uint256 penalty = amountShift= maxAppeals) return NON_PAYABLE_AMOUNT; - return (2*amountJurors(_disputeID) + 1) * dispute.arbitrationFeePerJuror; + return (2*amountJurors(_disputeID) + 1) * dispute.feeAmount; } /** @dev Compute the amount of jurors to be drawn. @@ -791,11 +848,11 @@ contract Court is Arbitrator, ApproveAndCallFallBack { rng = _rng; } - /** @dev Setter for arbitrationFeePerJuror. - * @param _arbitrationFeePerJuror The fee which will be paid to each juror. + /** @dev Setter for feeAmount. + * @param _feeAmount The fee which will be paid to each juror. */ - function setArbitrationFeePerJuror(uint256 _arbitrationFeePerJuror) public onlyGovernor { - arbitrationFeePerJuror = _arbitrationFeePerJuror; + function setArbitrationFeePerJuror(uint256 _feeAmount) public onlyGovernor { + feeAmount = _feeAmount; } /** @dev Setter for defaultNumberJuror. From 3d91f7669d828b19be0039ded68fe77981ebbb30 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Wed, 20 Feb 2019 18:52:34 +0100 Subject: [PATCH 03/27] Remove untested Kleros code --- contracts/Court.sol | 766 ++------------------------ contracts/standards/erc900/ERC900.sol | 17 + contracts/test/TestFactory.sol | 45 ++ package.json | 9 +- test/court-staking.js | 96 ++++ truffle-config.js | 12 +- 6 files changed, 207 insertions(+), 738 deletions(-) create mode 100644 contracts/standards/erc900/ERC900.sol create mode 100644 contracts/test/TestFactory.sol create mode 100644 test/court-staking.js diff --git a/contracts/Court.sol b/contracts/Court.sol index ef00da63..5f4e283a 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -18,32 +18,7 @@ import "@aragon/os/contracts/lib/token/ERC20.sol"; // AUDIT(@izqui): Magic strings in revert reasons -contract Court is ERC900, Arbitrator, ApproveAndCallFallBack { - // Variables which should not change after initialization. - ERC20 public jurorToken; - uint256 public constant NON_PAYABLE_AMOUNT = (2**256 - 2) / 2; // An astronomic amount, practically can't be paid. - - // Config variables modifiable by the governor during activation phse - RNG public rng; - ERC20 public feeToken; - uint256 public feeAmount; // The fee which will be paid to each juror. - uint16 public defaultNumberJuror = 3; // Number of drawn jurors unless specified otherwise. - uint256 public minActivatedToken = 0.1 * 1e18; // Minimum of tokens to be activated (in basic units). - uint[5] public timePerPeriod; // The minimum time each period lasts (seconds). - uint256 public alpha = 2000; // alpha in ‱ (1 / 10 000). - uint256 constant ALPHA_DIVISOR = 1e4; // Amount we need to divided alpha in ‱ to get the float value of alpha. - uint256 public maxAppeals = 5; // Number of times a dispute can be appealed. When exceeded appeal cost becomes NON_PAYABLE_AMOUNT. - // Initially, the governor will be an address controlled by the Kleros team. At a later stage, - // the governor will be switched to a governance contract with liquid voting. - address public governor; // Address of the governor contract. - - // Variables changing during day to day interaction. - uint256 public session = 1; // Current session of the court. - uint256 public lastPeriodChange; // The last time we changed of period (seconds). - uint256 public segmentSize; // Size of the segment of activated tokens. - uint256 public rnBlock; // The block linked with the RN which is requested. - uint256 public randomNumber; // Random number of the session. - +contract Court is ERC900, /*Arbitrator,*/ ApproveAndCallFallBack { enum Period { Activation, // When juror can deposit their tokens and parties give evidences. Draw, // When jurors are drawn at random, note that this period is fast. @@ -52,8 +27,6 @@ contract Court is ERC900, Arbitrator, ApproveAndCallFallBack { Execution // When where token redistribution occurs and Kleros call the arbitrated contracts. } - Period public period; // AUDIT(@izqui): It should be possible to many periods running in parallel - struct Juror { mapping (address => uint256) balances; // token addr -> balance // Total number of tokens the jurors can loose in disputes they are drawn in. Those tokens are locked. Note that we can have atStake > balance but it should be statistically unlikely and does not pose issues. @@ -63,100 +36,46 @@ contract Court is ERC900, Arbitrator, ApproveAndCallFallBack { uint256 segmentEnd; // End of the segment of activated tokens. } - mapping (address => Juror) public jurors; - - struct Vote { - address account; // The juror who casted the vote. - uint256 ruling; // The ruling which was given. - } - - struct VoteCounter { - uint256 winningChoice; // The choice which currently has the highest amount of votes. Is 0 in case of a tie. - uint256 winningCount; // The number of votes for winningChoice. Or for the choices which are tied. AUDIT(@izqui): Is this redundant? - mapping (uint256 => uint) voteCount; // voteCount[choice] is the number of votes for choice. - } - - enum DisputeState { // Not to be confused this with DisputeStatus in Arbitrator contract. - Open, // The dispute is opened but the outcome is not available yet (this include when jurors voted but appeal is still possible). - Resolving, // The token repartition has started. Note that if it's done in just one call, this state is skipped. - Executable, // The arbitrated contract can be called to enforce the decision. - Executed // Everything has been done and the dispute can't be interacted with anymore. - } - - struct Dispute { - Arbitrable arbitrated; // Contract to be arbitrated. - uint256 session; // First session the dispute was schedule. - uint256 appeals; // Number of appeals. - uint256 choices; // The number of choices available to the jurors. - uint16 initialNumberJurors; // The initial number of jurors. - uint256 feeAmount; // The fee which will be paid to each juror. - DisputeState state; // The state of the dispute. - Vote[][] votes; // The votes in the form vote[appeals][voteID]. - VoteCounter[] voteCounter; // The vote counters in the form voteCounter[appeals]. - mapping (address => uint) lastSessionVote; // Last session a juror has voted on this dispute. Is 0 if he never did. - uint256 currentAppealToRepartition; // The current appeal we are repartitioning. - AppealsRepartitioned[] appealsRepartitioned; // Track a partially repartitioned appeal in the form AppealsRepartitioned[appeal]. - } + // Variables which should not change after initialization. + ERC20 public jurorToken; - enum RepartitionStage { // State of the token repartition if oneShotTokenRepartition would throw because there are too many votes. - Incoherent, - Coherent, - AtStake, - Complete - } + // Config variables modifiable by the governor during activation phse + RNG public rng; + ERC20 public feeToken; + uint256 public feeAmount; // per juror + uint256 public jurorMinActivation = 0.1 * 1e18; + uint256[5] public periodDurations; + uint256 public maxAppeals = 5; - struct AppealsRepartitioned { - uint256 totalToRedistribute; // Total amount of tokens we have to redistribute. - uint256 nbCoherent; // Number of coherent jurors for session. - uint256 currentIncoherentVote; // Current vote for the incoherent loop. - uint256 currentCoherentVote; // Current vote we need to count. - uint256 currentAtStakeVote; // Current vote we need to count. - RepartitionStage stage; // Use with multipleShotTokenRepartition if oneShotTokenRepartition would throw. - } + address public governor; // TODO: consider using aOS' ACL - Dispute[] public disputes; + uint256 public session = 1; // Current session of the court. + uint256 public lastPeriodChange; // The last time we changed of period (seconds). + uint256 public rnBlock; // The block linked with the RN which is requested. + uint256 public randomSeed; - // **************************** // - // * Events * // - // **************************** // + Period public period; // AUDIT(@izqui): It should be possible to many periods running in parallel + mapping (address => Juror) public jurors; - /** @dev Emitted when we pass to a new period. - * @param _period The new period. - * @param _session The current session. - */ event NewPeriod(Period _period, uint256 indexed _session); - /** @dev Emitted when a juror wins or loses tokens. - * @param _account The juror affected. - * @param _disputeID The ID of the dispute. - * @param _amount The amount of parts of token which was won. Can be negative for lost amounts. - */ - event TokenShift(address indexed _account, uint256 _disputeID, int _amount); - - /** @dev Emited when a juror wins arbitration fees. - * @param _account The account affected. - * @param _disputeID The ID of the dispute. - * @param _amount The amount of weis which was won. - */ - event ArbitrationReward(address indexed _account, uint256 _disputeID, uint256 _amount); - - // **************************** // - // * Modifiers * // - // **************************** // - - // AUDIT(@izqui): Code formatting - modifier onlyBy(address _account) {require(msg.sender == _account, "Wrong caller."); _;} - // AUDIT(@izqui): Currently not checking if the period should have been transitioned, so some periods can last longer if no one bothers to call `passPeriod()` - modifier onlyDuring(Period _period) {require(period == _period, "Wrong period."); _;} - modifier onlyGovernor() {require(msg.sender == governor, "Only callable by the governor."); _;} + string internal constant ERROR_INVALID_ADDR = "COURT_INVALID_ADDR"; + string internal constant ERROR_DEPOSIT_FAILED = "COURT_DEPOSIT_FAILED"; + string internal constant ERROR_ZERO_TRANSFER = "COURT_ZERO_TRANSFER"; + string internal constant ERROR_LOCKED_TOKENS = "COURT_LOCKED_TOKENS"; + string internal constant ERROR_ACTIVATED_TOKENS = "COURT_ACTIVATED_TOKENS"; + modifier only(address _addr) { + require(msg.sender == _addr, ERROR_INVALID_ADDR); + _; + } /** @dev Constructor. * @param _jurorToken The address of the juror work token contract. * @param _feeToken The address of the token contract that is used to pay for fees. * @param _feeAmount The amount of _feeToken that is paid per juror per dispute * @param _rng The random number generator which will be used. - * @param _timePerPeriod The minimal time for each period (seconds). + * @param _periodDurations The minimal time for each period (seconds). * @param _governor Address of the governor contract. */ constructor( @@ -164,7 +83,7 @@ contract Court is ERC900, Arbitrator, ApproveAndCallFallBack { ERC20 _feeToken, uint256 _feeAmount, RNG _rng, - uint[5] _timePerPeriod, + uint256[5] _periodDurations, address _governor ) public { jurorToken = _jurorToken; @@ -175,7 +94,7 @@ contract Court is ERC900, Arbitrator, ApproveAndCallFallBack { // solium-disable-next-line security/no-block-members lastPeriodChange = block.timestamp; - timePerPeriod = _timePerPeriod; // AUDIT(@izqui): Verify the bytecode that solc produces here + periodDurations = _periodDurations; // AUDIT(@izqui): Verify the bytecode that solc produces here governor = _governor; } @@ -195,14 +114,15 @@ contract Court is ERC900, Arbitrator, ApproveAndCallFallBack { */ function receiveApproval(address _from, uint256 _amount, address token, bytes) public - onlyBy(jurorToken) - onlyBy(token) + only(jurorToken) + only(token) { _stake(_from, _from, _amount); } function _stake(address _from, address _to, uint256 _amount) internal { - require(jurorToken.transferFrom(_from, this, _amount), "Transfer failed."); + require(_amount > 0, ERROR_ZERO_TRANSFER); + require(jurorToken.transferFrom(_from, this, _amount), ERROR_DEPOSIT_FAILED); jurors[_to].balances[jurorToken] += _amount; @@ -236,6 +156,8 @@ contract Court is ERC900, Arbitrator, ApproveAndCallFallBack { * @param _amount The amount to withdraw. */ function withdraw(ERC20 _token, uint256 _amount) public { + require(_amount > 0, ERROR_ZERO_TRANSFER); + address jurorAddress = msg.sender; Juror storage juror = jurors[jurorAddress]; @@ -244,9 +166,9 @@ contract Court is ERC900, Arbitrator, ApproveAndCallFallBack { if (_token == jurorToken) { // Make sure that there is no more at stake than owned to avoid overflow. - require(juror.atStake <= balance, "Balance is less than stake."); - require(_amount <= balance - juror.atStake, "Value is more than free balance."); // AUDIT(@izqui): Simpler to just safe math here - require(juror.lastSession != session, "You have deposited in this session."); + require(juror.atStake <= balance, ERROR_LOCKED_TOKENS); + require(_amount <= balance - juror.atStake, ERROR_LOCKED_TOKENS); // AUDIT(@izqui): Simpler to just safe math here + require(juror.lastSession != session, ERROR_ACTIVATED_TOKENS); emit Unstaked(jurorAddress, _amount, totalStakedFor(jurorAddress), ""); } @@ -268,15 +190,15 @@ contract Court is ERC900, Arbitrator, ApproveAndCallFallBack { function passPeriod() public { // solium-disable-next-line security/no-block-members uint256 time = block.timestamp; - require(time - lastPeriodChange >= timePerPeriod[uint8(period)], "Not enough time has passed."); + require(time - lastPeriodChange >= periodDurations[uint8(period)], "Not enough time has passed."); if (period == Period.Activation) { rnBlock = block.number + 1; rng.requestRN(rnBlock); period = Period.Draw; } else if (period == Period.Draw) { - randomNumber = rng.getUncorrelatedRN(rnBlock); // AUDIT(@izqui): For the block number RNG the next period transition must be done within 256 blocks - require(randomNumber != 0, "Random number not ready yet."); + randomSeed = rng.getUncorrelatedRN(rnBlock); // AUDIT(@izqui): For the block number RNG the next period transition must be done within 256 blocks + require(randomSeed != 0, "Random number not ready yet."); period = Period.Vote; } else if (period == Period.Vote) { period = Period.Appeal; @@ -285,615 +207,13 @@ contract Court is ERC900, Arbitrator, ApproveAndCallFallBack { } else if (period == Period.Execution) { period = Period.Activation; ++session; - segmentSize = 0; rnBlock = 0; - randomNumber = 0; + randomSeed = 0; } lastPeriodChange = time; emit NewPeriod(period, session); } - // AUDIT(@izqui): Really impractical to require jurors to send a transaction to activate every period. It costs ~50k gas per juror to activate per period (issue #2) - // AUDIT(@izqui): Jurors should provide either the period number or a TTL in case the transaction takes longer to mine resulting in a later activation - - /** @dev Deposit tokens in order to have chances of being drawn. Note that once tokens are deposited, - * there is no possibility of depositing more. - * @param _value Amount of tokens (in basic units) to deposit. - */ - function activateTokens(uint256 _value) public onlyDuring(Period.Activation) { - Juror storage juror = jurors[msg.sender]; - require(_value <= juror.balances[jurorToken], "Not enough balance."); - require(_value >= minActivatedToken, "Value is less than the minimum stake."); - // Verify that tokens were not already activated for this session. - require(juror.lastSession != session, "You have already activated in this session."); - - juror.lastSession = session; - juror.segmentStart = segmentSize; - segmentSize += _value; - juror.segmentEnd = segmentSize; - - } - - // AUDIT(@izqui): Lacking commit-reveal juror votes - // AUDIT(@izqui): Being drawn multiple times can lead to arbitration fees being kept by the contract and never distributed. - - /** @dev Vote a ruling. Juror must input the draw ID he was drawn. - * Note that the complexity is O(d), where d is amount of times the juror was drawn. - * Since being drawn multiple time is a rare occurrence and that a juror can always vote with less weight than it has, it is not a problem. - * But note that it can lead to arbitration fees being kept by the contract and never distributed. - * @param _disputeID The ID of the dispute the juror was drawn. - * @param _ruling The ruling given. - * @param _draws The list of draws the juror was drawn. Draw numbering starts at 1 and the numbers should be increasing. - */ - function voteRuling(uint256 _disputeID, uint256 _ruling, uint[] _draws) public onlyDuring(Period.Vote) { - Dispute storage dispute = disputes[_disputeID]; - Juror storage juror = jurors[msg.sender]; - VoteCounter storage voteCounter = dispute.voteCounter[dispute.appeals]; - require(dispute.lastSessionVote[msg.sender] != session, "You have already voted."); // Make sure juror hasn't voted yet. - require(_ruling <= dispute.choices, "Invalid ruling."); - // Note that it throws if the draws are incorrect. - require(validDraws(msg.sender, _disputeID, _draws), "Invalid draws."); - - dispute.lastSessionVote[msg.sender] = session; - voteCounter.voteCount[_ruling] += _draws.length; - if (voteCounter.winningCount < voteCounter.voteCount[_ruling]) { - voteCounter.winningCount = voteCounter.voteCount[_ruling]; - voteCounter.winningChoice = _ruling; - } else if (voteCounter.winningCount==voteCounter.voteCount[_ruling] && _draws.length!=0) { // Verify draw length to be non-zero to avoid the possibility of setting tie by casting 0 votes. - voteCounter.winningChoice = 0; // It's currently a tie. - } - for (uint256 i = 0; i < _draws.length; ++i) { - dispute.votes[dispute.appeals].push(Vote({ - account: msg.sender, - ruling: _ruling - })); - } - - juror.atStake += _draws.length * getStakePerDraw(); - uint256 feeToPay = _draws.length * dispute.feeAmount; - msg.sender.transfer(feeToPay); - emit ArbitrationReward(msg.sender, _disputeID, feeToPay); - } - - /** @dev Steal part of the tokens and the arbitration fee of a juror who failed to vote. - * Note that a juror who voted but without all his weight can't be penalized. - * It is possible to not penalize with the maximum weight. - * But note that it can lead to arbitration fees being kept by the contract and never distributed. - * @param _jurorAddress Address of the juror to steal tokens from. - * @param _disputeID The ID of the dispute the juror was drawn. - * @param _draws The list of draws the juror was drawn. Numbering starts at 1 and the numbers should be increasing. - */ - function penalizeInactiveJuror(address _jurorAddress, uint256 _disputeID, uint[] _draws) public { - Dispute storage dispute = disputes[_disputeID]; - Juror storage inactiveJuror = jurors[_jurorAddress]; - require(period > Period.Vote, "Must be called after the vote period."); - require(dispute.lastSessionVote[_jurorAddress] != session, "Juror did vote."); // Verify the juror hasn't voted. - dispute.lastSessionVote[_jurorAddress] = session; // Update last session to avoid penalizing multiple times. - require(validDraws(_jurorAddress, _disputeID, _draws), "Invalid draws."); - uint256 penalty = _draws.length * minActivatedToken * 2 * alpha / ALPHA_DIVISOR; - // Make sure the penalty is not higher than the balance. - uint256 jurorStake = inactiveJuror.balances[jurorToken]; - if (penalty >= jurorStake) { - penalty = jurorStake; - } - inactiveJuror.balances[jurorToken] -= penalty; - emit TokenShift(_jurorAddress, _disputeID, -int(penalty)); - jurors[msg.sender].balances[jurorToken] += penalty / 2; // Give half of the penalty to the caller. - emit TokenShift(msg.sender, _disputeID, int(penalty / 2)); - jurors[governor].balances[jurorToken] += penalty / 2; // The other half to the governor. - emit TokenShift(governor, _disputeID, int(penalty / 2)); - msg.sender.transfer(_draws.length*dispute.feeAmount); // Give the arbitration fees to the caller. - } - - // AUDIT(@izqui): these two repartition functions could be simplified if the juror has to pull their own tokens. Total refactor required here. - // AUDIT(@izqui): once a dispute appeal's period passes this should be executable at any time, not only during execution periods - - /** @dev Execute all the token repartition. - * Note that this function could consume to much gas if there is too much votes. - * It is O(v), where v is the number of votes for this dispute. - * In the next version, there will also be a function to execute it in multiple calls - * (but note that one shot execution, if possible, is less expensive). - * @param _disputeID ID of the dispute. - */ - function oneShotTokenRepartition(uint256 _disputeID) public onlyDuring(Period.Execution) { - Dispute storage dispute = disputes[_disputeID]; - require(dispute.state == DisputeState.Open, "Dispute is not open."); - require(dispute.session + dispute.appeals <= session, "Dispute is still active."); - - uint256 winningChoice = dispute.voteCounter[dispute.appeals].winningChoice; - uint256 amountShift = getStakePerDraw(); - for (uint256 i = 0; i <= dispute.appeals; ++i) { - // If the result is not a tie, some parties are incoherent. Note that 0 (refuse to arbitrate) winning is not a tie. - // Result is a tie if the winningChoice is 0 (refuse to arbitrate) and the choice 0 is not the most voted choice. - // Note that in case of a "tie" among some choices including 0, parties who did not vote 0 are considered incoherent. - if (winningChoice!=0 || (dispute.voteCounter[dispute.appeals].voteCount[0] == dispute.voteCounter[dispute.appeals].winningCount)) { - uint256 totalToRedistribute = 0; - uint256 nbCoherent = 0; - // First loop to penalize the incoherent votes. - for (uint256 j = 0; j < dispute.votes[i].length; ++j) { - Vote storage vote = dispute.votes[i][j]; - if (vote.ruling != winningChoice) { - Juror storage juror = jurors[vote.account]; - uint256 penalty = amountShift= _maxIterations) { - return; - } - Vote storage vote = dispute.votes[i][j]; - if (vote.ruling != winningChoice) { - Juror storage juror = jurors[vote.account]; - uint256 penalty = amountShift= _maxIterations) { - return; - } - vote = dispute.votes[i][j]; - if (vote.ruling == winningChoice) { - juror = jurors[vote.account]; - juror.balances[jurorToken] += toRedistribute; - emit TokenShift(vote.account, _disputeID, int(toRedistribute)); - } - - ++currentIterations; - ++dispute.appealsRepartitioned[i].currentCoherentVote; - } - - dispute.appealsRepartitioned[i].stage = RepartitionStage.AtStake; - } - } - - if (dispute.appealsRepartitioned[i].stage == RepartitionStage.AtStake) { - // Third loop to lower the atStake in order to unlock tokens. - for (j = dispute.appealsRepartitioned[i].currentAtStakeVote; j < dispute.votes[i].length; ++j) { - if (currentIterations >= _maxIterations) { - return; - } - vote = dispute.votes[i][j]; - juror = jurors[vote.account]; - juror.atStake -= amountShift; // Note that it can't underflow due to amountShift not changing between vote and redistribution. - - ++currentIterations; - ++dispute.appealsRepartitioned[i].currentAtStakeVote; - } - - dispute.appealsRepartitioned[i].stage = RepartitionStage.Complete; - } - - if (dispute.appealsRepartitioned[i].stage == RepartitionStage.Complete) { - ++dispute.currentAppealToRepartition; - } - } - - // AUDIT(@izqui): Since this requires the juror tokens to be distributed, rulings aren't executed until someone pays to distribute tokens - // As there is no incentive to do so, the party interested in the outcome will end up paying the gas for paying jurors - - dispute.state = DisputeState.Executable; - } - - // **************************** // - // * Court functions * // - // * Constant and Pure * // - // **************************** // - - /** @dev Return the amount of jurors which are or will be drawn in the dispute. - * The number of jurors is doubled and 1 is added at each appeal. We have proven the formula by recurrence. - * This avoid having a variable number of jurors which would be updated in order to save gas. - * @param _disputeID The ID of the dispute we compute the amount of jurors. - * @return nbJurors The number of jurors which are drawn. - */ - function amountJurors(uint256 _disputeID) public view returns (uint256 nbJurors) { - Dispute storage dispute = disputes[_disputeID]; - return (dispute.initialNumberJurors + 1) * 2 ** dispute.appeals - 1; - } - - /** @dev Must be used to verify that a juror has been draw at least _draws.length times. - * We have to require the user to specify the draws that lead the juror to be drawn. - * Because doing otherwise (looping through all draws) could consume too much gas. - * @param _jurorAddress Address of the juror we want to verify draws. - * @param _disputeID The ID of the dispute the juror was drawn. - * @param _draws The list of draws the juror was drawn. It draw numbering starts at 1 and the numbers should be increasing. - * Note that in most cases this list will just contain 1 number. - * @param valid true if the draws are valid. - */ - function validDraws(address _jurorAddress, uint256 _disputeID, uint[] _draws) public view returns (bool valid) { - uint256 draw = 0; - Juror storage juror = jurors[_jurorAddress]; - Dispute storage dispute = disputes[_disputeID]; - uint256 nbJurors = amountJurors(_disputeID); - - // AUDIT(@izqui): Code formatting - - if (juror.lastSession != session) return false; // Make sure that the tokens were deposited for this session. - if (dispute.session+dispute.appeals != session) return false; // Make sure there is currently a dispute. - if (period <= Period.Draw) return false; // Make sure that jurors are already drawn. - for (uint256 i = 0; i < _draws.length; ++i) { - if (_draws[i] <= draw) return false; // Make sure that draws are always increasing to avoid someone inputing the same multiple times. - draw = _draws[i]; - if (draw > nbJurors) return false; - uint256 position = uint(keccak256(randomNumber, _disputeID, draw)) % segmentSize; // Random position on the segment for draw. - require(position >= juror.segmentStart, "Invalid draw."); - require(position < juror.segmentEnd, "Invalid draw."); - } - - return true; - } - - // **************************** // - // * Arbitrator functions * // - // * Modifying the state * // - // **************************** // - - /** @dev Create a dispute. Must be called by the arbitrable contract. - * Must be paid at least arbitrationCost(). - * @param _choices Amount of choices the arbitrator can make in this dispute. - * @param _extraData Null for the default number. Otherwise, first 16 bytes will be used to return the number of jurors. - * @return disputeID ID of the dispute created. - */ - function createDispute(uint256 _choices, bytes _extraData) public payable returns (uint256 disputeID) { - uint16 nbJurors = extraDataToNbJurors(_extraData); - require(msg.value >= arbitrationCost(_extraData), "Not enough ETH to pay arbitration fees."); - - disputeID = disputes.length++; - Dispute storage dispute = disputes[disputeID]; - dispute.arbitrated = Arbitrable(msg.sender); - - if (period < Period.Draw) { // If drawing did not start schedule it for the current session. - dispute.session = session; - } else { // Otherwise schedule it for the next one. - dispute.session = session+1; - } - - dispute.choices = _choices; - dispute.initialNumberJurors = nbJurors; - // We store it as the general fee can be changed through the governance mechanism. - dispute.feeAmount = feeAmount; - dispute.votes.length++; // AUDIT(@izqui): Why it cannot be zero indexed? - dispute.voteCounter.length++; - - DisputeCreation(disputeID, Arbitrable(msg.sender)); - return disputeID; - } - - /** @dev Appeal a ruling. Note that it has to be called before the arbitrator contract calls rule. - * @param _disputeID ID of the dispute to be appealed. - */ - function appeal(uint256 _disputeID, bytes) public payable onlyDuring(Period.Appeal) { - bytes memory noAppealExtraData = new bytes(0); - super.appeal(_disputeID, noAppealExtraData); // AUDIT(@izqui): super.appeal just emits an event, inline it in this contract - Dispute storage dispute = disputes[_disputeID]; - require(msg.value >= appealCost(_disputeID, noAppealExtraData), "Not enough ETH to pay appeal fees."); - require(dispute.session+dispute.appeals == session, "Dispute is no longer active."); // Dispute of the current session. - require(dispute.arbitrated == msg.sender, "Caller is not the arbitrated contract."); - - dispute.appeals++; - dispute.votes.length++; - dispute.voteCounter.length++; - } - - // AUDIT(@izqui): Rulings can be executed infinite times, arbitrable contract should only be called once per ruling - - /** @dev Execute the ruling of a dispute which is in the state executable. UNTRUSTED. - * @param disputeID ID of the dispute to execute the ruling. - */ - function executeRuling(uint256 disputeID) public { - Dispute storage dispute = disputes[disputeID]; - require(dispute.state == DisputeState.Executable, "Dispute is not executable."); - - dispute.state = DisputeState.Executed; - dispute.arbitrated.rule(disputeID, dispute.voteCounter[dispute.appeals].winningChoice); - } - - // **************************** // - // * Arbitrator functions * // - // * Constant and pure * // - // **************************** // - - /** @dev Compute the cost of arbitration. It is recommended not to increase it often, - * as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. - * @param _extraData Null for the default number. Other first 16 bits will be used to return the number of jurors. - * @return fee Amount to be paid. - */ - function arbitrationCost(bytes _extraData) public view returns (uint256 fee) { - return extraDataToNbJurors(_extraData) * feeAmount; - } - - /** @dev Compute the cost of appeal. It is recommended not to increase it often, - * as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. - * @param _disputeID ID of the dispute to be appealed. - * @return fee Amount to be paid. - */ - function appealCost(uint256 _disputeID, bytes) public view returns (uint256 fee) { - Dispute storage dispute = disputes[_disputeID]; - - if(dispute.appeals >= maxAppeals) return NON_PAYABLE_AMOUNT; - - return (2*amountJurors(_disputeID) + 1) * dispute.feeAmount; - } - - /** @dev Compute the amount of jurors to be drawn. - * @param _extraData Null for the default number. Other first 16 bits will be used to return the number of jurors. - * Note that it does not check that the number of jurors is odd, but users are advised to choose a odd number of jurors. - */ - function extraDataToNbJurors(bytes _extraData) internal view returns (uint16 nbJurors) { - if (_extraData.length < 2) - return defaultNumberJuror; - else - return (uint16(_extraData[0]) << 8) + uint16(_extraData[1]); - } - - /** @dev Compute the minimum activated jurorTokens in alpha. - * Note there may be multiple draws for a single user on a single dispute. - */ - function getStakePerDraw() public view returns (uint256 minActivatedTokenInAlpha) { - return (alpha * minActivatedToken) / ALPHA_DIVISOR; - } - - - // **************************** // - // * Constant getters * // - // **************************** // - - /** @dev Getter for account in Vote. - * @param _disputeID ID of the dispute. - * @param _appeals Which appeal (or 0 for the initial session). - * @param _voteID The ID of the vote for this appeal (or initial session). - * @return account The address of the voter. - */ - function getVoteAccount(uint256 _disputeID, uint256 _appeals, uint256 _voteID) public view returns (address account) { - return disputes[_disputeID].votes[_appeals][_voteID].account; - } - - /** @dev Getter for ruling in Vote. - * @param _disputeID ID of the dispute. - * @param _appeals Which appeal (or 0 for the initial session). - * @param _voteID The ID of the vote for this appeal (or initial session). - * @return ruling The ruling given by the voter. - */ - function getVoteRuling(uint256 _disputeID, uint256 _appeals, uint256 _voteID) public view returns (uint256 ruling) { - return disputes[_disputeID].votes[_appeals][_voteID].ruling; - } - - /** @dev Getter for winningChoice in VoteCounter. - * @param _disputeID ID of the dispute. - * @param _appeals Which appeal (or 0 for the initial session). - * @return winningChoice The currently winning choice (or 0 if it's tied). Note that 0 can also be return if the majority refuses to arbitrate. - */ - function getWinningChoice(uint256 _disputeID, uint256 _appeals) public view returns (uint256 winningChoice) { - return disputes[_disputeID].voteCounter[_appeals].winningChoice; - } - - /** @dev Getter for winningCount in VoteCounter. - * @param _disputeID ID of the dispute. - * @param _appeals Which appeal (or 0 for the initial session). - * @return winningCount The amount of votes the winning choice (or those who are tied) has. - */ - function getWinningCount(uint256 _disputeID, uint256 _appeals) public view returns (uint256 winningCount) { - return disputes[_disputeID].voteCounter[_appeals].winningCount; - } - - /** @dev Getter for voteCount in VoteCounter. - * @param _disputeID ID of the dispute. - * @param _appeals Which appeal (or 0 for the initial session). - * @param _choice The choice. - * @return voteCount The amount of votes the winning choice (or those who are tied) has. - */ - function getVoteCount(uint256 _disputeID, uint256 _appeals, uint256 _choice) public view returns (uint256 voteCount) { - return disputes[_disputeID].voteCounter[_appeals].voteCount[_choice]; - } - - /** @dev Getter for lastSessionVote in Dispute. - * @param _disputeID ID of the dispute. - * @param _juror The juror we want to get the last session he voted. - * @return lastSessionVote The last session the juror voted. - */ - function getLastSessionVote(uint256 _disputeID, address _juror) public view returns (uint256 lastSessionVote) { - return disputes[_disputeID].lastSessionVote[_juror]; - } - - /** @dev Is the juror drawn in the draw of the dispute. - * @param _disputeID ID of the dispute. - * @param _juror The juror. - * @param _draw The draw. Note that it starts at 1. - * @return drawn True if the juror is drawn, false otherwise. - */ - function isDrawn(uint256 _disputeID, address _juror, uint256 _draw) public view returns (bool drawn) { - Dispute storage dispute = disputes[_disputeID]; - Juror storage juror = jurors[_juror]; - if ( // AUDIT(@izqui): Code smell - juror.lastSession != session || (dispute.session + dispute.appeals != session) || period <= Period.Draw || _draw > amountJurors(_disputeID) || _draw == 0 || segmentSize == 0 - ) { - return false; - } else { - uint256 position = uint(keccak256(randomNumber,_disputeID,_draw)) % segmentSize; // AUDIT(@izqui): Use `validDrawns` or move hashing calculation to a pure function - return (position >= juror.segmentStart) && (position < juror.segmentEnd); - } - - } - - /** @dev Return the current ruling of a dispute. This is useful for parties to know if they should appeal. - * @param _disputeID ID of the dispute. - * @return ruling The current ruling which will be given if there is no appeal. If it is not available, return 0. - */ - function currentRuling(uint256 _disputeID) public view returns (uint256 ruling) { - Dispute storage dispute = disputes[_disputeID]; - return dispute.voteCounter[dispute.appeals].winningChoice; - } - - /** @dev Return the status of a dispute. - * @param _disputeID ID of the dispute to rule. - * @return status The status of the dispute. - */ - function disputeStatus(uint256 _disputeID) public view returns (DisputeStatus status) { - // AUDIT(@izqui): Code smell - - Dispute storage dispute = disputes[_disputeID]; - if (dispute.session+dispute.appeals < session) // Dispute of past session. - return DisputeStatus.Solved; - else if(dispute.session+dispute.appeals == session) { // Dispute of current session. - if (dispute.state == DisputeState.Open) { - if (period < Period.Appeal) - return DisputeStatus.Waiting; - else if (period == Period.Appeal) - return DisputeStatus.Appealable; - else return DisputeStatus.Solved; - } else return DisputeStatus.Solved; - } else return DisputeStatus.Waiting; // Dispute for future session. - } - - // **************************** // - // * Governor Functions * // - // **************************** // - - // AUDIT(@izqui): Governor could steal juror fees - - /** @dev General call function where the contract execute an arbitrary call with data and ETH following governor orders. - * @param _data Transaction data. - * @param _value Transaction value. - * @param _target Transaction target. - */ - function executeOrder(bytes32 _data, uint256 _value, address _target) public onlyGovernor { - _target.call.value(_value)(_data); // solium-disable-line security/no-call-value - } - - /** @dev Setter for rng. - * @param _rng An instance of RNG. - */ - function setRng(RNG _rng) public onlyGovernor { - rng = _rng; - } - - /** @dev Setter for feeAmount. - * @param _feeAmount The fee which will be paid to each juror. - */ - function setArbitrationFeePerJuror(uint256 _feeAmount) public onlyGovernor { - feeAmount = _feeAmount; - } - - /** @dev Setter for defaultNumberJuror. - * @param _defaultNumberJuror Number of drawn jurors unless specified otherwise. - */ - function setDefaultNumberJuror(uint16 _defaultNumberJuror) public onlyGovernor { - defaultNumberJuror = _defaultNumberJuror; - } - - /** @dev Setter for minActivatedToken. - * @param _minActivatedToken Minimum of tokens to be activated (in basic units). - */ - function setMinActivatedToken(uint256 _minActivatedToken) public onlyGovernor { - minActivatedToken = _minActivatedToken; - } - - /** @dev Setter for timePerPeriod. - * @param _timePerPeriod The minimum time each period lasts (seconds). - */ - function setTimePerPeriod(uint[5] _timePerPeriod) public onlyGovernor { - timePerPeriod = _timePerPeriod; - } - - /** @dev Setter for alpha. - * @param _alpha Alpha in ‱. - */ - function setAlpha(uint256 _alpha) public onlyGovernor { - alpha = _alpha; - } - - /** @dev Setter for maxAppeals. - * @param _maxAppeals Number of times a dispute can be appealed. When exceeded appeal cost becomes NON_PAYABLE_AMOUNT. - */ - function setMaxAppeals(uint256 _maxAppeals) public onlyGovernor { - maxAppeals = _maxAppeals; - } - - /** @dev Setter for governor. - * @param _governor Address of the governor contract. - */ - function setGovernor(address _governor) public onlyGovernor { - governor = _governor; - } + // TODO: governor parametrization } \ No newline at end of file diff --git a/contracts/standards/erc900/ERC900.sol b/contracts/standards/erc900/ERC900.sol new file mode 100644 index 00000000..6f856f9e --- /dev/null +++ b/contracts/standards/erc900/ERC900.sol @@ -0,0 +1,17 @@ +pragma solidity ^0.4.24; + +// Interface for ERC900: https://eips.ethereum.org/EIPS/eip-900 +interface ERC900 { + event Staked(address indexed user, uint256 amount, uint256 total, bytes data); + event Unstaked(address indexed user, uint256 amount, uint256 total, bytes data); + + function stake(uint256 amount, bytes data) external; + function stakeFor(address user, uint256 amount, bytes data) external; + function unstake(uint256 amount, bytes data) external; + + function totalStakedFor(address addr) external view returns (uint256); + function totalStaked() external view returns (uint256); + function token() external view returns (address); + + function supportsHistory() external pure returns (bool); +} \ No newline at end of file diff --git a/contracts/test/TestFactory.sol b/contracts/test/TestFactory.sol new file mode 100644 index 00000000..b658baa3 --- /dev/null +++ b/contracts/test/TestFactory.sol @@ -0,0 +1,45 @@ +pragma solidity ^0.4.24; + +import "@aragon/apps-shared-minime/contracts/MiniMeToken.sol"; +import "../Court.sol"; + +import "@aragon/apps-shared-migrations/contracts/Migrations.sol"; + +contract Factory { + event Deployed(address addr); +} + +contract TokenFactory is Factory { + function newToken(string symbol, uint256 initialBalance) external { + MiniMeToken token = new MiniMeToken( + MiniMeTokenFactory(0), + MiniMeToken(0), + 0, + symbol, + 0, + symbol, + true + ); + + token.generateTokens(msg.sender, initialBalance); + token.changeController(msg.sender); + + emit Deployed(address(token)); + } +} + +contract CourtFactory is Factory { + function newCourtStaking(ERC20 anj) external { + uint256[5] periods; // no periods + Court court = new Court( + anj, + ERC20(0), // no fees + 0, + RNG(0), // no rng + periods, + address(this) + ); + + emit Deployed(address(court)); + } +} \ No newline at end of file diff --git a/package.json b/package.json index 031d8f3c..43ff4a88 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,16 @@ "version": "1.0.0", "description": "", "scripts": { - "test": "truffle test" + "test": "TRUFFLE_TEST=true npm run ganache-cli:test", + "ganache-cli:test": "./node_modules/@aragon/test-helpers/ganache-cli.sh" }, "author": "Aragon Association", "license": "GPL-3.0", "devDependencies": { - "@aragon/apps-shared-migrations": "1.0.0", + "@aragon/apps-shared-migrations": "^1.0.0", "@aragon/test-helpers": "^1.0.1", - "truffle": "^4.1.14", - "truffle-bytecode-manager": "^1.1.1" + "ganache-cli": "^6.1.0", + "truffle": "^4.1.14" }, "dependencies": { "@aragon/apps-shared-minime": "^1.0.1", diff --git a/test/court-staking.js b/test/court-staking.js new file mode 100644 index 00000000..2a5d322b --- /dev/null +++ b/test/court-staking.js @@ -0,0 +1,96 @@ +const TokenFactory = artifacts.require('TokenFactory') +const CourtFactory = artifacts.require('CourtFactory') + +const COURT = 'Court' +const MINIME = 'MiniMeToken' + +const getLog = (receipt, logName, argName) => + receipt.logs.find(({ event }) => event == logName).args[argName] + +const deployedContract = async (receipt, name) => + artifacts.require(name).at(getLog(await receipt, 'Deployed', 'addr')) + +const assertEqualBN = async (actualPromise, expected, message) => + assert.equal((await actualPromise).toNumber(), expected, message) + +contract('Court: Staking', ([ pleb, rich ]) => { + const INITIAL_BALANCE = 1e6 + const NO_DATA = '' + + before(async () => { + this.tokenFactory = await TokenFactory.new() + this.courtFactory = await CourtFactory.new() + }) + + beforeEach(async () => { + // Mints 1,000,000 tokens for sender + this.anj = await deployedContract(this.tokenFactory.newToken('ANJ', INITIAL_BALANCE, { from: rich }), MINIME) + assertEqualBN(this.anj.balanceOf(rich), INITIAL_BALANCE, 'rich balance') + assertEqualBN(this.anj.balanceOf(pleb), 0, 'pleb balance') + + this.court = await deployedContract(this.courtFactory.newCourtStaking(this.anj.address), COURT) + assert.equal(await this.court.token(), this.anj.address, 'court token') + assert.equal(await this.court.jurorToken(), this.anj.address, 'court juror token'); + }) + + const assertStaked = async (staker, amount, initialBalance, { recipient, initialStaked = 0 } = {}) => { + await assertEqualBN(this.court.totalStakedFor(recipient ? recipient : staker), initialStaked + amount, 'staked amount') + await assertEqualBN(this.court.totalStaked(), initialStaked + amount, 'rich stake') + await assertEqualBN(this.anj.balanceOf(staker), initialBalance - amount, 'rich token balance') + await assertEqualBN(this.anj.balanceOf(this.court.address), initialStaked + amount, 'court token balance') + } + + it('stakes', async () => { + const amount = 1000 + + await this.anj.approve(this.court.address, amount, { from: rich }) + await this.court.stake(amount, NO_DATA, { from: rich }) + + await assertStaked(rich, amount, INITIAL_BALANCE) + }) + + it('stakes using \'approveAndCall\'', async () => { + const amount = 3000 + + await this.anj.approveAndCall(this.court.address, amount, NO_DATA, { from: rich }) + + await assertStaked(rich, amount, INITIAL_BALANCE) + }) + + it('stakes using \'stakeFor\'', async () => { + const amount = 50 + + await this.anj.approve(this.court.address, amount, { from: rich }) + await this.court.stakeFor(pleb, amount, NO_DATA, { from: rich }) + + await assertStaked(rich, amount, INITIAL_BALANCE, { recipient: pleb }) + }) + + context('staked tokens', () => { + const amount = 6000 + + beforeEach(async () => { + await this.anj.approveAndCall(this.court.address, amount, NO_DATA, { from: rich }) + await assertStaked(rich, amount, INITIAL_BALANCE) + }) + + it('unstakes', async () => { + const unstaking = amount / 3 + + await this.court.unstake(unstaking, NO_DATA, { from: rich }) + + await assertStaked(rich, -unstaking, INITIAL_BALANCE - amount, { initialStaked: amount }) + }) + + it('unstakes using \'withdraw\'', async () => { + const unstaking = amount / 4 + + await this.court.withdraw(this.anj.address, unstaking, { from: rich }) + + await assertStaked(rich, -unstaking, INITIAL_BALANCE - amount, { initialStaked: amount }) + }) + + it('reverts if unstaking tokens at stake') + it('reverts if unstaking while juror is active') + }) +}) diff --git a/truffle-config.js b/truffle-config.js index 8db6a057..466a16a3 100644 --- a/truffle-config.js +++ b/truffle-config.js @@ -1,11 +1 @@ -module.exports = { - networks: { - development: { - host: "127.0.0.1", - gas: 4e6, - gasPrice: 10, - port: 8545, - network_id: "*" - } - } -} +module.exports = require("@aragon/os/truffle-config") From 500d3d124aaf5d9361b455caaf1fa089e57328c4 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Thu, 21 Feb 2019 16:00:20 +0100 Subject: [PATCH 04/27] Heartbeat wip --- contracts/Court.sol | 279 ++++++++++++++++++++++----------- contracts/test/TestFactory.sol | 9 +- 2 files changed, 194 insertions(+), 94 deletions(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index 5f4e283a..c0b98eb9 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -1,8 +1,7 @@ pragma solidity ^0.4.24; // TODO: pin solc -// Forked from: Kleros.sol https://github.com/kleros/kleros @ 7281e69 - -import "./standards/rng/RNG.sol"; +// Inspired by: Kleros.sol https://github.com/kleros/kleros @ 7281e69 +import "./lib/HexSumTree.sol"; import "./standards/arbitration/Arbitrator.sol"; import "./standards/arbitration/Arbitrable.sol"; import "./standards/erc900/ERC900.sol"; @@ -10,54 +9,65 @@ import "./standards/erc900/ERC900.sol"; import { ApproveAndCallFallBack } from "@aragon/apps-shared-minime/contracts/MiniMeToken.sol"; import "@aragon/os/contracts/lib/token/ERC20.sol"; -// AUDIT(@izqui): Code format should be optimized for readability not reducing the amount of LOCs -// AUDIT(@izqui): Not using SafeMath should be reviewed in a case by case basis -// AUDIT(@izqui): Arbitration fees should be payable in an ERC20, no just ETH (can have native ETH support) -// AUDIT(@izqui): Incorrect function order -// AUDIT(@izqui): Use emit for events -// AUDIT(@izqui): Magic strings in revert reasons +contract Court is ERC900, ApproveAndCallFallBack { + using HexSumTree for HexSumTree.Tree; -contract Court is ERC900, /*Arbitrator,*/ ApproveAndCallFallBack { - enum Period { - Activation, // When juror can deposit their tokens and parties give evidences. - Draw, // When jurors are drawn at random, note that this period is fast. - Vote, // Where jurors can vote on disputes. - Appeal, // When parties can appeal the rulings. - Execution // When where token redistribution occurs and Kleros call the arbitrated contracts. + enum AccountState { + NotJuror, + Juror, + PastJuror } - struct Juror { + struct Account { mapping (address => uint256) balances; // token addr -> balance - // Total number of tokens the jurors can loose in disputes they are drawn in. Those tokens are locked. Note that we can have atStake > balance but it should be statistically unlikely and does not pose issues. - uint256 atStake; - uint256 lastSession; // Last session the tokens were activated. - uint256 segmentStart; // Start of the segment of activated tokens. - uint256 segmentEnd; // End of the segment of activated tokens. + AccountState state; // whether the account is not a juror, a current juror or a past juror + uint32 fromTerm; // first term in which the juror can be drawn + uint32 toTerm; // last term in which the juror can be drawn + bytes32 sumTreeId; // key in the sum tree used for sortition + uint128[] pendingDisputes; // disputes in which the juror was drawn which haven't resolved } - // Variables which should not change after initialization. - ERC20 public jurorToken; + struct FeeStructure { + ERC20 feeToken; + uint256 jurorFee; // per juror, total dispute fee = jurorFee * jurors drawn + uint256 heartbeatFee; // per dispute, total heartbeat fee = heartbeatFee * disputes/appeals in term + } - // Config variables modifiable by the governor during activation phse - RNG public rng; - ERC20 public feeToken; - uint256 public feeAmount; // per juror - uint256 public jurorMinActivation = 0.1 * 1e18; - uint256[5] public periodDurations; - uint256 public maxAppeals = 5; + struct Term { + uint64 startTime; // timestamp when the term started + uint64 dependingDraws; // disputes or appeals pegged to this term for randomness + uint64 feeStructureId; // fee structure for this term (index in feeStructures array) + uint64 randomnessBN; // block number for entropy + uint256 randomness; // entropy from randomnessBN block hash + address[] jurorIngress; // jurors that will be added to the juror tree + address[] jurorEgress; // jurors that will be removed from to the juror tree + address[] jurorUpdates; // jurors whose stake has been updated + } - address public governor; // TODO: consider using aOS' ACL + struct Dispute { + Arbitrable subject; + uint64 termId; + // TODO + } - uint256 public session = 1; // Current session of the court. - uint256 public lastPeriodChange; // The last time we changed of period (seconds). - uint256 public rnBlock; // The block linked with the RN which is requested. - uint256 public randomSeed; + // State constants which are set in the constructor and can't change + ERC20 public jurorToken; + uint64 public termDuration; // recomended value ~1 hour as 256 blocks (available block hash) around an hour to mine - Period public period; // AUDIT(@izqui): It should be possible to many periods running in parallel - mapping (address => Juror) public jurors; + // Global config, configurable by governor + address public governor; // TODO: consider using aOS' ACL + uint256 public jurorActivationDust; + uint256 public maxAppeals = 5; + FeeStructure[] public feeStructures; - event NewPeriod(Period _period, uint256 indexed _session); + // Court state + uint256 public term; + mapping (address => Account) public jurors; + mapping (uint256 => Term) public terms; + HexSumTree.Tree internal sumTree; + Dispute[] public disputes; + uint256 public feeChangeTerm; string internal constant ERROR_INVALID_ADDR = "COURT_INVALID_ADDR"; string internal constant ERROR_DEPOSIT_FAILED = "COURT_DEPOSIT_FAILED"; @@ -65,37 +75,155 @@ contract Court is ERC900, /*Arbitrator,*/ ApproveAndCallFallBack { string internal constant ERROR_LOCKED_TOKENS = "COURT_LOCKED_TOKENS"; string internal constant ERROR_ACTIVATED_TOKENS = "COURT_ACTIVATED_TOKENS"; + uint256 internal constant ZERO_TERM = 0; // invalid term that doesn't accept disputes + uint256 internal constant MODIFIER_ALLOWED_TERM_TRANSITIONS = 1; + + event NewTerm(uint256 term, address indexed heartbeatSender); + modifier only(address _addr) { require(msg.sender == _addr, ERROR_INVALID_ADDR); _; } /** @dev Constructor. + * @param _termDuration Duration in seconds per term (recommended 1 hour) * @param _jurorToken The address of the juror work token contract. * @param _feeToken The address of the token contract that is used to pay for fees. - * @param _feeAmount The amount of _feeToken that is paid per juror per dispute - * @param _rng The random number generator which will be used. - * @param _periodDurations The minimal time for each period (seconds). + * @param _jurorFee The amount of _feeToken that is paid per juror per dispute + * @param _heartbeatFee The amount of _feeToken per dispute to cover maintenance costs. * @param _governor Address of the governor contract. + * @param _firstTermStartTime Timestamp in seconds when the court will open (to give time for juror onboarding) */ constructor( + uint64 _termDuration, ERC20 _jurorToken, ERC20 _feeToken, - uint256 _feeAmount, - RNG _rng, - uint256[5] _periodDurations, - address _governor + uint256 _jurorFee, + uint256 _heartbeatFee, + address _governor, + uint64 _firstTermStartTime, + uint256 _jurorActivationDust ) public { + termDuration = _termDuration; jurorToken = _jurorToken; - rng = _rng; + jurorActivationDust = _jurorActivationDust; + governor = _governor; + + feeStructures.length = 1; // leave index 0 empty + _setFeeStructure(ZERO_TERM, _feeToken, _jurorFee, _heartbeatFee); + terms[ZERO_TERM].startTime = _firstTermStartTime - _termDuration; + + sumTree.init(); + } - feeToken = _feeToken; - feeAmount = _feeAmount; + string internal constant ERROR_TOO_MANY_TRANSITIONS = "COURT_TOO_MANY_TRANSITIONS"; + string internal constant ERROR_FIRST_TERM_NOT_STARTED = "COURT_FIRST_TERM_NOT_STARTED"; - // solium-disable-next-line security/no-block-members - lastPeriodChange = block.timestamp; - periodDurations = _periodDurations; // AUDIT(@izqui): Verify the bytecode that solc produces here - governor = _governor; + modifier ensureTerm { + require(term > ZERO_TERM, ERROR_FIRST_TERM_NOT_STARTED); + + uint256 requiredTransitions = neededTermTransitions(); + require(requiredTransitions <= MODIFIER_ALLOWED_TERM_TRANSITIONS, ERROR_TOO_MANY_TRANSITIONS); + + if (requiredTransitions > 0) { + heartbeat(requiredTransitions); + } + + _; + } + + string internal constant ERROR_UNFINISHED_TERM = "COURT_UNFINISHED_TERM"; + + function heartbeat(uint256 _termTransitions) public { + require(canTransitionTerm(), ERROR_UNFINISHED_TERM); + + Term storage prevTerm = terms[term]; + Term storage nextTerm = terms[term + 1]; + address heartbeatSender = msg.sender; + + // Set fee structure for term + if (nextTerm.feeStructureId == 0) { + nextTerm.feeStructureId = prevTerm.feeStructureId; + } else { + feeChangeTerm = ZERO_TERM; // fee structure changed in this term + } + + // TODO: skip period if you can + + // Set the start time of the term (ensures equally long terms, regardless of heartbeats) + nextTerm.startTime = prevTerm.startTime + termDuration; + nextTerm.randomnessBN = blockNumber() + 1; // randomness source set to next block (unknown when heartbeat happens) + processJurorQueues(nextTerm); + + FeeStructure storage feeStructure = feeStructures[nextTerm.feeStructureId]; + uint256 totalFee = nextTerm.dependingDraws * feeStructure.heartbeatFee; + + if (totalFee > 0) { + assignTokens(feeStructure.feeToken, heartbeatSender, totalFee); + } + + term += 1; + emit NewTerm(term, heartbeatSender); + + if (_termTransitions > 0 && canTransitionTerm()) { + heartbeat(_termTransitions - 1); + } + } + + function processJurorQueues(Term storage _incomingTerm) internal { + + } + + event TokenBalanceChange(address indexed token, address indexed owner, uint256 amount, bool positive); + + function assignTokens(ERC20 _feeToken, address _to, uint256 _amount) internal { + jurors[_to].balances[_feeToken] += _amount; + + emit TokenBalanceChange(_feeToken, _to, _amount, true); + } + + function canTransitionTerm() public view returns (bool) { + return neededTermTransitions() >= 1; + } + + function neededTermTransitions() public view returns (uint256) { + return (time() - terms[term].startTime) / termDuration; + } + + function time() public view returns (uint64) { + return uint64(block.timestamp); + } + + function blockNumber() public view returns (uint64) { + return uint64(block.number); + } + + string internal constant ERROR_PAST_TERM_TERM_FEE_CHANGE = "COURT_PAST_TERM_FEE_CHANGE"; + event NewFeeStructure(uint256 fromTerm, uint256 feeStructureId); + + function _setFeeStructure( + uint256 _fromTerm, + ERC20 _feeToken, + uint256 _jurorFee, + uint256 _heartbeatFee + ) internal { + require(feeChangeTerm > term || term == ZERO_TERM, ERROR_PAST_TERM_TERM_FEE_CHANGE); + + if (feeChangeTerm != ZERO_TERM) { + terms[feeChangeTerm].feeStructureId = 0; // reset previously set fee structure change + } + + FeeStructure memory feeStructure = FeeStructure({ + feeToken: _feeToken, + jurorFee: _jurorFee, + heartbeatFee: _heartbeatFee + }); + + uint256 feeStructureId = feeStructures.push(feeStructure) - 1; + terms[feeChangeTerm].feeStructureId = uint64(feeStructureId); + feeChangeTerm = _fromTerm; + + emit NewFeeStructure(_fromTerm, feeStructureId); } // ERC900 @@ -160,15 +288,17 @@ contract Court is ERC900, /*Arbitrator,*/ ApproveAndCallFallBack { address jurorAddress = msg.sender; - Juror storage juror = jurors[jurorAddress]; + Account storage juror = jurors[jurorAddress]; uint256 balance = juror.balances[_token]; if (_token == jurorToken) { - // Make sure that there is no more at stake than owned to avoid overflow. + /* + TODO require(juror.atStake <= balance, ERROR_LOCKED_TOKENS); require(_amount <= balance - juror.atStake, ERROR_LOCKED_TOKENS); // AUDIT(@izqui): Simpler to just safe math here require(juror.lastSession != session, ERROR_ACTIVATED_TOKENS); + */ emit Unstaked(jurorAddress, _amount, totalStakedFor(jurorAddress), ""); } @@ -177,43 +307,12 @@ contract Court is ERC900, /*Arbitrator,*/ ApproveAndCallFallBack { require(jurorToken.transfer(jurorAddress, _amount), "Transfer failed."); } - // **************************** // - // * Court functions * // - // * Modifying the state * // - // **************************** // + function activate(uint256 fromSession, uint256 toSession) external { - // AUDIT(@izqui): This could automatically be triggered by any other court function that requires a period transition. - // AUDIT(@izqui): No incentive for anyone to call this, delaying to call the function can result in periods lasting longer. + } - /** @dev To call to go to a new period. TRUSTED. - */ - function passPeriod() public { - // solium-disable-next-line security/no-block-members - uint256 time = block.timestamp; - require(time - lastPeriodChange >= periodDurations[uint8(period)], "Not enough time has passed."); - - if (period == Period.Activation) { - rnBlock = block.number + 1; - rng.requestRN(rnBlock); - period = Period.Draw; - } else if (period == Period.Draw) { - randomSeed = rng.getUncorrelatedRN(rnBlock); // AUDIT(@izqui): For the block number RNG the next period transition must be done within 256 blocks - require(randomSeed != 0, "Random number not ready yet."); - period = Period.Vote; - } else if (period == Period.Vote) { - period = Period.Appeal; - } else if (period == Period.Appeal) { - period = Period.Execution; - } else if (period == Period.Execution) { - period = Period.Activation; - ++session; - rnBlock = 0; - randomSeed = 0; - } + function deactivate() external { - lastPeriodChange = time; - emit NewPeriod(period, session); } +} - // TODO: governor parametrization -} \ No newline at end of file diff --git a/contracts/test/TestFactory.sol b/contracts/test/TestFactory.sol index b658baa3..5385bc64 100644 --- a/contracts/test/TestFactory.sol +++ b/contracts/test/TestFactory.sol @@ -30,14 +30,15 @@ contract TokenFactory is Factory { contract CourtFactory is Factory { function newCourtStaking(ERC20 anj) external { - uint256[5] periods; // no periods Court court = new Court( + 60 * 60, // 1h anj, ERC20(0), // no fees 0, - RNG(0), // no rng - periods, - address(this) + 0, + address(this), + uint64(block.timestamp + 60 * 60), + 1 ); emit Deployed(address(court)); From 3297fce4e16b46bad1d2750c1d4e296e5867d886 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Fri, 22 Feb 2019 14:37:01 +0100 Subject: [PATCH 05/27] Court state machine: activate, deactivate, withdraw --- contracts/Court.sol | 324 +++++++++++++++++++--------- contracts/lib/ArrayUtils.sol | 21 ++ contracts/test/CourtMock.sol | 51 +++++ contracts/test/HexSumTreePublic.sol | 4 + contracts/test/TestFactory.sol | 3 +- test/court-lifecycle.js | 207 ++++++++++++++++++ test/hex-tree.js | 11 + 7 files changed, 516 insertions(+), 105 deletions(-) create mode 100644 contracts/lib/ArrayUtils.sol create mode 100644 contracts/test/CourtMock.sol create mode 100644 test/court-lifecycle.js diff --git a/contracts/Court.sol b/contracts/Court.sol index c0b98eb9..1b598ec3 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -2,16 +2,20 @@ pragma solidity ^0.4.24; // TODO: pin solc // Inspired by: Kleros.sol https://github.com/kleros/kleros @ 7281e69 import "./lib/HexSumTree.sol"; +import "./lib/ArrayUtils.sol"; import "./standards/arbitration/Arbitrator.sol"; import "./standards/arbitration/Arbitrable.sol"; import "./standards/erc900/ERC900.sol"; import { ApproveAndCallFallBack } from "@aragon/apps-shared-minime/contracts/MiniMeToken.sol"; import "@aragon/os/contracts/lib/token/ERC20.sol"; +import "@aragon/os/contracts/common/SafeERC20.sol"; contract Court is ERC900, ApproveAndCallFallBack { using HexSumTree for HexSumTree.Tree; + using ArrayUtils for address[]; + using SafeERC20 for ERC20; enum AccountState { NotJuror, @@ -22,8 +26,8 @@ contract Court is ERC900, ApproveAndCallFallBack { struct Account { mapping (address => uint256) balances; // token addr -> balance AccountState state; // whether the account is not a juror, a current juror or a past juror - uint32 fromTerm; // first term in which the juror can be drawn - uint32 toTerm; // last term in which the juror can be drawn + uint64 fromTerm; // first term in which the juror can be drawn + uint64 toTerm; // last term in which the juror can be drawn bytes32 sumTreeId; // key in the sum tree used for sortition uint128[] pendingDisputes; // disputes in which the juror was drawn which haven't resolved } @@ -40,9 +44,9 @@ contract Court is ERC900, ApproveAndCallFallBack { uint64 feeStructureId; // fee structure for this term (index in feeStructures array) uint64 randomnessBN; // block number for entropy uint256 randomness; // entropy from randomnessBN block hash - address[] jurorIngress; // jurors that will be added to the juror tree - address[] jurorEgress; // jurors that will be removed from to the juror tree - address[] jurorUpdates; // jurors whose stake has been updated + address[] ingressQueue; // jurors that will be added to the juror tree + address[] egressQueue; // jurors that will be removed from to the juror tree + address[] updatesQueue; // jurors whose stake has been updated } struct Dispute { @@ -57,34 +61,64 @@ contract Court is ERC900, ApproveAndCallFallBack { // Global config, configurable by governor address public governor; // TODO: consider using aOS' ACL + uint64 public jurorCooldownTerms; uint256 public jurorActivationDust; uint256 public maxAppeals = 5; FeeStructure[] public feeStructures; // Court state - uint256 public term; - mapping (address => Account) public jurors; - mapping (uint256 => Term) public terms; + uint64 public term; + uint64 public feeChangeTerm; + mapping (address => Account) public accounts; + mapping (bytes32 => address) public jurorsByTreeId; + mapping (uint64 => Term) public terms; HexSumTree.Tree internal sumTree; Dispute[] public disputes; - uint256 public feeChangeTerm; string internal constant ERROR_INVALID_ADDR = "COURT_INVALID_ADDR"; string internal constant ERROR_DEPOSIT_FAILED = "COURT_DEPOSIT_FAILED"; string internal constant ERROR_ZERO_TRANSFER = "COURT_ZERO_TRANSFER"; string internal constant ERROR_LOCKED_TOKENS = "COURT_LOCKED_TOKENS"; string internal constant ERROR_ACTIVATED_TOKENS = "COURT_ACTIVATED_TOKENS"; - - uint256 internal constant ZERO_TERM = 0; // invalid term that doesn't accept disputes - uint256 internal constant MODIFIER_ALLOWED_TERM_TRANSITIONS = 1; - - event NewTerm(uint256 term, address indexed heartbeatSender); + string internal constant ERROR_TOO_MANY_TRANSITIONS = "COURT_TOO_MANY_TRANSITIONS"; + string internal constant ERROR_FIRST_TERM_NOT_STARTED = "COURT_FIRST_TERM_NOT_STARTED"; + string internal constant ERROR_UNFINISHED_TERM = "COURT_UNFINISHED_TERM"; + string internal constant ERROR_PAST_TERM_TERM_FEE_CHANGE = "COURT_PAST_TERM_FEE_CHANGE"; + string internal constant ERROR_INVALID_ACCOUNT_STATE = "COURT_INVALID_ACCOUNT_STATE"; + string internal constant ERROR_TOKENS_BELOW_DUST = "COURT_TOKENS_BELOW_DUST"; + string internal constant ERROR_INVALID_ACTIVATION_TERM = "COURT_INVALID_ACTIVATION_TERM"; + string internal constant ERROR_INVALID_DEACTIVATION_TERM = "COURT_INVALID_DEACTIVATION_TERM"; + string internal constant ERROR_JUROR_TOKENS_AT_STAKE = "COURT_JUROR_TOKENS_AT_STAKE"; + string internal constant ERROR_BALANCE_TOO_LOW = "COURT_BALANCE_TOO_LOW"; + string internal constant ERROR_TOKEN_TRANSFER_FAILED = "COURT_TOKEN_TRANSFER_FAILED"; + + uint64 internal constant ZERO_TERM = 0; // invalid term that doesn't accept disputes + uint64 public constant MANUAL_DEACTIVATION = uint64(-1); + uint64 internal constant MODIFIER_ALLOWED_TERM_TRANSITIONS = 1; + + event NewTerm(uint64 term, address indexed heartbeatSender); + event NewFeeStructure(uint64 fromTerm, uint64 feeStructureId); + event TokenBalanceChange(address indexed token, address indexed owner, uint256 amount, bool positive); + event JurorActivate(address indexed juror, uint64 fromTerm, uint64 toTerm); + event JurorDeactivate(address indexed juror, uint64 lastTerm); + event TokenWithdrawal(address indexed token, address indexed account, uint256 amount); modifier only(address _addr) { require(msg.sender == _addr, ERROR_INVALID_ADDR); _; } + modifier ensureTerm { + uint64 requiredTransitions = neededTermTransitions(); + require(requiredTransitions <= MODIFIER_ALLOWED_TERM_TRANSITIONS, ERROR_TOO_MANY_TRANSITIONS); + + if (requiredTransitions > 0) { + heartbeat(requiredTransitions); + } + + _; + } + /** @dev Constructor. * @param _termDuration Duration in seconds per term (recommended 1 hour) * @param _jurorToken The address of the juror work token contract. @@ -93,6 +127,7 @@ contract Court is ERC900, ApproveAndCallFallBack { * @param _heartbeatFee The amount of _feeToken per dispute to cover maintenance costs. * @param _governor Address of the governor contract. * @param _firstTermStartTime Timestamp in seconds when the court will open (to give time for juror onboarding) + * @param _jurorCooldownTerms Number of terms before a juror tokens can be withdrawn after deactivation () */ constructor( uint64 _termDuration, @@ -102,12 +137,14 @@ contract Court is ERC900, ApproveAndCallFallBack { uint256 _heartbeatFee, address _governor, uint64 _firstTermStartTime, - uint256 _jurorActivationDust + uint256 _jurorActivationDust, + uint64 _jurorCooldownTerms ) public { termDuration = _termDuration; jurorToken = _jurorToken; jurorActivationDust = _jurorActivationDust; governor = _governor; + jurorCooldownTerms = _jurorCooldownTerms; feeStructures.length = 1; // leave index 0 empty _setFeeStructure(ZERO_TERM, _feeToken, _jurorFee, _heartbeatFee); @@ -116,25 +153,7 @@ contract Court is ERC900, ApproveAndCallFallBack { sumTree.init(); } - string internal constant ERROR_TOO_MANY_TRANSITIONS = "COURT_TOO_MANY_TRANSITIONS"; - string internal constant ERROR_FIRST_TERM_NOT_STARTED = "COURT_FIRST_TERM_NOT_STARTED"; - - modifier ensureTerm { - require(term > ZERO_TERM, ERROR_FIRST_TERM_NOT_STARTED); - - uint256 requiredTransitions = neededTermTransitions(); - require(requiredTransitions <= MODIFIER_ALLOWED_TERM_TRANSITIONS, ERROR_TOO_MANY_TRANSITIONS); - - if (requiredTransitions > 0) { - heartbeat(requiredTransitions); - } - - _; - } - - string internal constant ERROR_UNFINISHED_TERM = "COURT_UNFINISHED_TERM"; - - function heartbeat(uint256 _termTransitions) public { + function heartbeat(uint64 _termTransitions) public { require(canTransitionTerm(), ERROR_UNFINISHED_TERM); Term storage prevTerm = terms[term]; @@ -153,13 +172,13 @@ contract Court is ERC900, ApproveAndCallFallBack { // Set the start time of the term (ensures equally long terms, regardless of heartbeats) nextTerm.startTime = prevTerm.startTime + termDuration; nextTerm.randomnessBN = blockNumber() + 1; // randomness source set to next block (unknown when heartbeat happens) - processJurorQueues(nextTerm); + _processJurorQueues(nextTerm); FeeStructure storage feeStructure = feeStructures[nextTerm.feeStructureId]; uint256 totalFee = nextTerm.dependingDraws * feeStructure.heartbeatFee; if (totalFee > 0) { - assignTokens(feeStructure.feeToken, heartbeatSender, totalFee); + _assignTokens(feeStructure.feeToken, heartbeatSender, totalFee); } term += 1; @@ -170,60 +189,68 @@ contract Court is ERC900, ApproveAndCallFallBack { } } - function processJurorQueues(Term storage _incomingTerm) internal { - - } + // TODO: should we charge heartbeat fees to jurors? + function activate(uint64 _fromTerm, uint64 _toTerm) external ensureTerm { + address jurorAddress = msg.sender; + Account storage account = accounts[jurorAddress]; - event TokenBalanceChange(address indexed token, address indexed owner, uint256 amount, bool positive); + require(_fromTerm > term, ERROR_INVALID_ACTIVATION_TERM); + require(_toTerm > _fromTerm, ERROR_INVALID_DEACTIVATION_TERM); + require(account.state == AccountState.NotJuror, ERROR_INVALID_ACCOUNT_STATE); + require(account.balances[jurorToken] >= jurorActivationDust, ERROR_TOKENS_BELOW_DUST); - function assignTokens(ERC20 _feeToken, address _to, uint256 _amount) internal { - jurors[_to].balances[_feeToken] += _amount; + if (term == ZERO_TERM && _fromTerm == ZERO_TERM + 1) { + // allow direct judge onboardings before term 1 starts (no disputes depend on term 0) + _insertJurorToSumTree(jurorAddress); + } else { + // TODO: check queue size limit + terms[_fromTerm].ingressQueue.push(jurorAddress); + } - emit TokenBalanceChange(_feeToken, _to, _amount, true); - } + if (_toTerm != MANUAL_DEACTIVATION) { + // TODO: check queue size limit + terms[_toTerm].egressQueue.push(jurorAddress); + } - function canTransitionTerm() public view returns (bool) { - return neededTermTransitions() >= 1; - } + account.fromTerm = _fromTerm; + account.toTerm = _toTerm; + account.state = AccountState.Juror; - function neededTermTransitions() public view returns (uint256) { - return (time() - terms[term].startTime) / termDuration; + emit JurorActivate(jurorAddress, _fromTerm, _toTerm); } - function time() public view returns (uint64) { - return uint64(block.timestamp); - } + // TODO: activate more tokens - function blockNumber() public view returns (uint64) { - return uint64(block.number); - } + // this can't called if the juror is deactivated on the schedule specified when calling activate + // can be called many times to modify the deactivation date + function deactivate(uint64 _lastTerm) external ensureTerm { + address jurorAddress = msg.sender; + Account storage account = accounts[jurorAddress]; - string internal constant ERROR_PAST_TERM_TERM_FEE_CHANGE = "COURT_PAST_TERM_FEE_CHANGE"; - event NewFeeStructure(uint256 fromTerm, uint256 feeStructureId); + require(account.state == AccountState.Juror, ERROR_INVALID_ACCOUNT_STATE); + require(_lastTerm > term, ERROR_INVALID_DEACTIVATION_TERM); - function _setFeeStructure( - uint256 _fromTerm, - ERC20 _feeToken, - uint256 _jurorFee, - uint256 _heartbeatFee - ) internal { - require(feeChangeTerm > term || term == ZERO_TERM, ERROR_PAST_TERM_TERM_FEE_CHANGE); + // Juror didn't actually become activated + if (term < account.fromTerm && term != ZERO_TERM) { + terms[account.fromTerm].ingressQueue.deleteItem(jurorAddress); + } - if (feeChangeTerm != ZERO_TERM) { - terms[feeChangeTerm].feeStructureId = 0; // reset previously set fee structure change + if (account.toTerm != MANUAL_DEACTIVATION) { + terms[account.toTerm].egressQueue.deleteItem(jurorAddress); } - FeeStructure memory feeStructure = FeeStructure({ - feeToken: _feeToken, - jurorFee: _jurorFee, - heartbeatFee: _heartbeatFee - }); + terms[_lastTerm].egressQueue.push(jurorAddress); + account.toTerm = _lastTerm; - uint256 feeStructureId = feeStructures.push(feeStructure) - 1; - terms[feeChangeTerm].feeStructureId = uint64(feeStructureId); - feeChangeTerm = _fromTerm; + emit JurorDeactivate(jurorAddress, _lastTerm); + } - emit NewFeeStructure(_fromTerm, feeStructureId); + function canTransitionTerm() public view returns (bool) { + return neededTermTransitions() >= 1; + } + + function neededTermTransitions() public view returns (uint64) { + return (time() - terms[term].startTime) / termDuration; } // ERC900 @@ -242,27 +269,18 @@ contract Court is ERC900, ApproveAndCallFallBack { */ function receiveApproval(address _from, uint256 _amount, address token, bytes) public - only(jurorToken) + only(jurorToken) // allow sending fees with it as well only(token) { _stake(_from, _from, _amount); } - function _stake(address _from, address _to, uint256 _amount) internal { - require(_amount > 0, ERROR_ZERO_TRANSFER); - require(jurorToken.transferFrom(_from, this, _amount), ERROR_DEPOSIT_FAILED); - - jurors[_to].balances[jurorToken] += _amount; - - emit Staked(_to, _amount, totalStakedFor(_to), ""); - } - function unstake(uint256 _amount, bytes) external { return withdraw(jurorToken, _amount); } function totalStakedFor(address _addr) public view returns (uint256) { - return jurors[_addr].balances[jurorToken]; + return accounts[_addr].balances[jurorToken]; } function totalStaked() external view returns (uint256) { @@ -278,41 +296,139 @@ contract Court is ERC900, ApproveAndCallFallBack { } /** @dev Withdraw tokens. Note that we can't withdraw the tokens which are still atStake. - * Jurors can't withdraw their tokens if they have deposited some during this session. + * Jurors can't withdraw their tokens if they have deposited some during this term. * This is to prevent jurors from withdrawing tokens they could lose. * @param _token Token to withdraw * @param _amount The amount to withdraw. */ - function withdraw(ERC20 _token, uint256 _amount) public { + function withdraw(ERC20 _token, uint256 _amount) public ensureTerm { require(_amount > 0, ERROR_ZERO_TRANSFER); - address jurorAddress = msg.sender; + address addr = msg.sender; + Account storage account = accounts[addr]; + uint256 balance = account.balances[_token]; + require(balance >= _amount, ERROR_BALANCE_TOO_LOW); + + if (_token == jurorToken) { + if (account.state == AccountState.Juror) { + require(isJurorBalanceUnlocked(addr), ERROR_JUROR_TOKENS_AT_STAKE); + account.state = AccountState.PastJuror; + } - Account storage juror = jurors[jurorAddress]; + emit Unstaked(addr, _amount, totalStakedFor(addr), ""); + } + + account.balances[_token] -= _amount; + require(_token.safeTransfer(addr, _amount), ERROR_TOKEN_TRANSFER_FAILED); - uint256 balance = juror.balances[_token]; + emit TokenWithdrawal(_token, addr, _amount); + } - if (_token == jurorToken) { - /* - TODO - require(juror.atStake <= balance, ERROR_LOCKED_TOKENS); - require(_amount <= balance - juror.atStake, ERROR_LOCKED_TOKENS); // AUDIT(@izqui): Simpler to just safe math here - require(juror.lastSession != session, ERROR_ACTIVATED_TOKENS); - */ - - emit Unstaked(jurorAddress, _amount, totalStakedFor(jurorAddress), ""); + function unlockedBalanceOf(address _addr) public view returns (uint256) { + Account storage account = accounts[_addr]; + if (account.state == AccountState.Juror) { + if (!isJurorBalanceUnlocked(_addr)) { + return 0; + } + } + return account.balances[jurorToken]; + } + + function sortition(uint256 v) public view returns (address) { + return jurorsByTreeId[sumTree.sortition(v)]; + } + + function treeTotalSum() public view returns (uint256) { + return sumTree.totalSum(); + } + + function _processJurorQueues(Term storage _incomingTerm) internal { + uint256 ingressLength = _incomingTerm.ingressQueue.length; + uint256 egressLength = _incomingTerm.egressQueue.length; + uint256 updatesLength = _incomingTerm.updatesQueue.length; + + // Insert cost = 40k + tree insertion + for (uint256 i = 0; i < ingressLength; i++) { + _insertJurorToSumTree(_incomingTerm.ingressQueue[i]); } + for (uint256 j = 0; j < egressLength; j++) { + address jurorEgress = _incomingTerm.egressQueue[j]; + sumTree.set(accounts[jurorEgress].sumTreeId, 0); + delete accounts[jurorEgress].sumTreeId; + } + for (uint256 k = 0; k < updatesLength; k++) { + address jurorUpdate = _incomingTerm.updatesQueue[k]; + sumTree.set(accounts[jurorUpdate].sumTreeId, totalStakedFor(jurorUpdate)); + } + + if (ingressLength > 0) { + delete _incomingTerm.ingressQueue; + } + if (egressLength > 0) { + delete _incomingTerm.egressQueue; + } + if (updatesLength > 0) { + delete _incomingTerm.updatesQueue; + } + } + + function _insertJurorToSumTree(address _jurorAddress) internal { + bytes32 sumTreeId = sumTree.insert(totalStakedFor(_jurorAddress)); + accounts[_jurorAddress].sumTreeId = sumTreeId; + jurorsByTreeId[sumTreeId] = _jurorAddress; + } + + function _stake(address _from, address _to, uint256 _amount) internal { + require(_amount > 0, ERROR_ZERO_TRANSFER); + require(jurorToken.transferFrom(_from, this, _amount), ERROR_DEPOSIT_FAILED); - juror.balances[jurorToken] -= _amount; - require(jurorToken.transfer(jurorAddress, _amount), "Transfer failed."); + accounts[_to].balances[jurorToken] += _amount; + + emit Staked(_to, _amount, totalStakedFor(_to), ""); } - function activate(uint256 fromSession, uint256 toSession) external { + function _assignTokens(ERC20 _feeToken, address _to, uint256 _amount) internal { + accounts[_to].balances[_feeToken] += _amount; + emit TokenBalanceChange(_feeToken, _to, _amount, true); } - function deactivate() external { + function _setFeeStructure( + uint64 _fromTerm, + ERC20 _feeToken, + uint256 _jurorFee, + uint256 _heartbeatFee + ) internal { + require(feeChangeTerm > term || term == ZERO_TERM, ERROR_PAST_TERM_TERM_FEE_CHANGE); + + if (feeChangeTerm != ZERO_TERM) { + terms[feeChangeTerm].feeStructureId = 0; // reset previously set fee structure change + } + + FeeStructure memory feeStructure = FeeStructure({ + feeToken: _feeToken, + jurorFee: _jurorFee, + heartbeatFee: _heartbeatFee + }); + + uint64 feeStructureId = uint64(feeStructures.push(feeStructure) - 1); + terms[feeChangeTerm].feeStructureId = feeStructureId; + feeChangeTerm = _fromTerm; + + emit NewFeeStructure(_fromTerm, feeStructureId); + } + + function time() internal view returns (uint64) { + return uint64(block.timestamp); + } + + function blockNumber() internal view returns (uint64) { + return uint64(block.number); + } + function isJurorBalanceUnlocked(address _jurorAddress) internal view returns (bool) { + Account storage account = accounts[_jurorAddress]; + return term > account.toTerm + jurorCooldownTerms && account.pendingDisputes.length == 0; } } diff --git a/contracts/lib/ArrayUtils.sol b/contracts/lib/ArrayUtils.sol new file mode 100644 index 00000000..95b8d7c4 --- /dev/null +++ b/contracts/lib/ArrayUtils.sol @@ -0,0 +1,21 @@ +pragma solidity ^0.4.24; + + +library ArrayUtils { + function deleteItem(address[] storage self, address item) internal returns (bool) { + uint256 length = self.length; + for (uint256 i = 0; i < length; i++) { + if (self[i] == item) { + uint256 newLength = self.length - 1; + if (i != newLength) { + self[i] = self[newLength]; + } + + delete self[newLength]; + self.length = newLength; + + return true; + } + } + } +} diff --git a/contracts/test/CourtMock.sol b/contracts/test/CourtMock.sol new file mode 100644 index 00000000..c1f82283 --- /dev/null +++ b/contracts/test/CourtMock.sol @@ -0,0 +1,51 @@ +pragma solidity ^0.4.24; + +import "../Court.sol"; + + +contract CourtMock is Court { + uint64 internal mockTime = 0; + uint64 internal mockBn = 0; + + constructor( + uint64 _termDuration, + ERC20 _jurorToken, + ERC20 _feeToken, + uint256 _jurorFee, + uint256 _heartbeatFee, + address _governor, + uint64 _firstTermStartTime, + uint256 _jurorActivationDust, + uint64 _jurorCooldownTerms + ) Court( + _termDuration, + _jurorToken, + _feeToken, + _jurorFee, + _heartbeatFee, + _governor, + _firstTermStartTime, + _jurorActivationDust, + _jurorCooldownTerms + ) public {} + + function mock_setTime(uint64 time) external { + mockTime = time; + } + + function mock_timeTravel(uint64 time) external { + mockTime += time; + } + + function mock_setBlockNumber(uint64 bn) external { + mockBn = bn; + } + + function time() internal view returns (uint64) { + return mockTime; + } + + function blockNumber() internal view returns (uint64) { + return mockBn; + } +} \ No newline at end of file diff --git a/contracts/test/HexSumTreePublic.sol b/contracts/test/HexSumTreePublic.sol index 12b2a1d4..461e8a79 100644 --- a/contracts/test/HexSumTreePublic.sol +++ b/contracts/test/HexSumTreePublic.sol @@ -40,6 +40,10 @@ contract HexSumTreePublic { } } + function set(uint256 k, uint256 v) public { + tree.set(bytes32(k), v); + } + function sortition(uint256 v) public view returns (uint256) { return uint256(tree.sortition(v)); } diff --git a/contracts/test/TestFactory.sol b/contracts/test/TestFactory.sol index 5385bc64..ac139c29 100644 --- a/contracts/test/TestFactory.sol +++ b/contracts/test/TestFactory.sol @@ -38,9 +38,10 @@ contract CourtFactory is Factory { 0, address(this), uint64(block.timestamp + 60 * 60), + 1, 1 ); emit Deployed(address(court)); } -} \ No newline at end of file +} diff --git a/test/court-lifecycle.js b/test/court-lifecycle.js new file mode 100644 index 00000000..0ff9a55f --- /dev/null +++ b/test/court-lifecycle.js @@ -0,0 +1,207 @@ +const TokenFactory = artifacts.require('TokenFactory') +const CourtMock = artifacts.require('CourtMock') + +const MINIME = 'MiniMeToken' + +const getLog = (receipt, logName, argName) => { + const log = receipt.logs.find(({ event }) => event == logName) + return log ? log.args[argName] : null +} + +const assertRevert = async (receiptPromise, reason) => { + try { + await receiptPromise + assert.fail(`Expected a revert for reason: ${reason}`) + } catch (e) { + if (reason) { + e.reason = e.message.replace('VM Exception while processing transaction: revert ', '') + assert.equal(e.reason, reason, 'Incorrect revert reason') + } + } +} + +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: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { + const NO_DATA = '' + const ZERO_ADDRESS = '0x' + '00'.repeat(20) + + const termDuration = 10 + const firstTermStart = 5 + const jurorActivationDust = 100 + const cooldown = 10 + const startBlock = 1000 + + const initialBalance = 1e6 + const richStake = 1000 + const juror1Stake = 700 + const juror2Stake = 300 + + const NEW_TERM_EVENT = 'NewTerm' + const NEW_FEE_STRUCTURE_EVENT = 'NewFeeStructure' + + 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) + assertEqualBN(this.anj.balanceOf(rich), initialBalance, 'rich balance') + assertEqualBN(this.anj.balanceOf(poor), 0, 'poor balance') + + this.court = await CourtMock.new( + termDuration, + this.anj.address, + ZERO_ADDRESS, + 0, + 0, + governor, + firstTermStart, + jurorActivationDust, + cooldown + ) + 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.treeTotalSum(), 0, 'empty sum tree') + + await this.anj.approveAndCall(this.court.address, richStake, NO_DATA, { from: rich }) + await this.anj.approve(this.court.address, juror1Stake, { from: rich }) + await this.court.stakeFor(juror1, juror1Stake, NO_DATA, { from: rich }) + await this.anj.approve(this.court.address, juror2Stake, { from: rich }) + await this.court.stakeFor(juror2, juror2Stake, NO_DATA, { from: rich }) + + await assertEqualBN(this.court.totalStakedFor(rich), richStake, 'rich stake') + await assertEqualBN(this.court.totalStakedFor(juror1), juror1Stake, 'juror1 stake') + await assertEqualBN(this.court.totalStakedFor(juror2), juror2Stake, 'juror2 stake') + }) + + context('before first term', () => { + it('it in term #0', async () => { + await assertEqualBN(this.court.term(), 0, 'court term #0') + }) + + it('transitions to term #1 on heartbeat', async () => { + await this.court.mock_setTime(10) + await assertLogs(this.court.heartbeat(1), NEW_TERM_EVENT) + + await assertEqualBN(this.court.term(), 1, 'court term #1') + const [ + startTime, + dependingDraws, + feeStructureId, + randomnessBn + ] = await this.court.terms(1) + + await assertEqualBN(startTime, firstTermStart, 'first term start') + await assertEqualBN(dependingDraws, 0, 'depending draws') + await assertEqualBN(feeStructureId, 1, 'fee structure id') + await assertEqualBN(randomnessBn, startBlock + 1, 'randomeness bn') + }) + + it('can activate during period before heartbeat', async () => { + await this.court.mock_setTime(firstTermStart - 1) + await this.court.activate(1, 10, { from: rich }) + + await assertEqualBN(this.court.treeTotalSum(), richStake, 'total tree sum') + }) + + it('reverts if activating balance is below dust', async () => { + await this.court.mock_setTime(firstTermStart - 1) + await assertRevert(this.court.activate(1, 10, { from: poor }), 'COURT_TOKENS_BELOW_DUST') + }) + }) + + context('on regular court terms', () => { + const term = 3 + + const passTerms = async terms => { + await this.court.mock_timeTravel(terms * termDuration) + await this.court.heartbeat(terms) + assert.isFalse(await this.court.canTransitionTerm(), 'all terms transitioned') + } + + beforeEach(async () => { + await this.court.mock_setTime(firstTermStart) + await this.court.heartbeat(1) + + await passTerms(2) + + await assertEqualBN(this.court.term(), term, 'term #3') + }) + + it('has correct term state', async () => { + const [ + startTime, + dependingDraws, + feeStructureId, + randomnessBn + ] = await this.court.terms(term) + + await assertEqualBN(startTime, firstTermStart + (term - 1) * termDuration, 'term start') + await assertEqualBN(dependingDraws, 0, 'depending draws') + await assertEqualBN(feeStructureId, 1, 'fee structure id') + await assertEqualBN(randomnessBn, startBlock + 1, 'randomeness bn') + }) + + it('jurors can activate', async () => { + await this.court.activate(term + 1, term + 3, { from: juror1 }) + await this.court.activate(term + 1, term + 4, { from: juror2 }) + + await passTerms(1) + + assert.equal(await this.court.sortition(0), juror1, 'sortition start edge juror1') + assert.equal(await this.court.sortition(juror1Stake / 2), juror1, 'sortition juror1') + assert.equal(await this.court.sortition(juror1Stake - 1), juror1, 'sortition juror1 end edge') + assert.equal(await this.court.sortition(juror1Stake), juror2, 'sortition juror2 start edge') + assert.equal(await this.court.sortition(juror1Stake + juror2Stake / 2), juror2, 'sortition juror2') + assert.equal(await this.court.sortition(juror1Stake + juror2Stake - 1), juror2, 'sortition juror2 end edge') + + await assertRevert(this.court.sortition(juror1Stake + juror2Stake), 'SORTITION_OUT_OF_BOUNDS') + await assertEqualBN(this.court.treeTotalSum(), juror1Stake + juror2Stake, 'both jurors in the tree') + }) + + it('jurors can deactivate', async () => { + await this.court.activate(term + 1, term + 2, { from: juror1 }) + await this.court.activate(term + 1, term + 3, { from: juror2 }) + await passTerms(1) + await assertEqualBN(this.court.treeTotalSum(), juror1Stake + juror2Stake, 'both jurors in the tree') + await passTerms(1) + await assertEqualBN(this.court.treeTotalSum(), juror2Stake, 'only juror2 in tree') + await passTerms(1) + await assertEqualBN(this.court.treeTotalSum(), 0, 'no jurors in tree') + }) + + it('juror can manually deactivate') + + it('juror can withdraw after cooldown', async () => { + await this.court.activate(term + 1, term + 2, { from: juror1 }) + await passTerms(1) + await assertEqualBN(this.court.treeTotalSum(), juror1Stake, 'juror added to tree') + await passTerms(1) + await assertEqualBN(this.court.treeTotalSum(), 0, 'juror removed from to tree') + + await assertRevert(this.court.unstake(1, NO_DATA, { from: juror1 }), 'COURT_JUROR_TOKENS_AT_STAKE') + + await passTerms(cooldown + 1) + await this.court.unstake(juror1Stake, NO_DATA, { from: juror1 }) + + await assertEqualBN(this.anj.balanceOf(juror1), juror1Stake, 'juror tokens withdrawn') + await assertEqualBN(this.court.totalStakedFor(juror1), 0, 'juror no longer staked') + // TODO: state account check + }) + }) +}) diff --git a/test/hex-tree.js b/test/hex-tree.js index d8642b1f..0508466e 100644 --- a/test/hex-tree.js +++ b/test/hex-tree.js @@ -37,6 +37,17 @@ contract('Hex Sum Tree', (accounts) => { assertBN(await tree.get(1, 0), 10, 'get sum') }) + it('inserts and modifies', async () => { + await tree.insert(10) + await tree.insert(5) + assertBN(await tree.get(1, 15), 15, 'get sum') + + await tree.set(0, 5) + + assertBN(await tree.get(0, 0), 5, 'get node') + assertBN(await tree.get(1, 15), 10, 'get sum') + }) + it('inserts three', async () => { await tree.insert(10) await tree.insert(10) From fee8c87a180077a9fd27458994aca20550b27db8 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Fri, 1 Mar 2019 11:56:14 +0100 Subject: [PATCH 06/27] Address review comments --- contracts/Court.sol | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index 1b598ec3..4d745dc2 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -83,7 +83,7 @@ contract Court is ERC900, ApproveAndCallFallBack { string internal constant ERROR_TOO_MANY_TRANSITIONS = "COURT_TOO_MANY_TRANSITIONS"; string internal constant ERROR_FIRST_TERM_NOT_STARTED = "COURT_FIRST_TERM_NOT_STARTED"; string internal constant ERROR_UNFINISHED_TERM = "COURT_UNFINISHED_TERM"; - string internal constant ERROR_PAST_TERM_TERM_FEE_CHANGE = "COURT_PAST_TERM_FEE_CHANGE"; + string internal constant ERROR_PAST_TERM_FEE_CHANGE = "COURT_PAST_TERM_FEE_CHANGE"; string internal constant ERROR_INVALID_ACCOUNT_STATE = "COURT_INVALID_ACCOUNT_STATE"; string internal constant ERROR_TOKENS_BELOW_DUST = "COURT_TOKENS_BELOW_DUST"; string internal constant ERROR_INVALID_ACTIVATION_TERM = "COURT_INVALID_ACTIVATION_TERM"; @@ -127,6 +127,7 @@ contract Court is ERC900, ApproveAndCallFallBack { * @param _heartbeatFee The amount of _feeToken per dispute to cover maintenance costs. * @param _governor Address of the governor contract. * @param _firstTermStartTime Timestamp in seconds when the court will open (to give time for juror onboarding) + * @param _jurorActivationDust Minimum amount of juror tokens that can be activated * @param _jurorCooldownTerms Number of terms before a juror tokens can be withdrawn after deactivation () */ constructor( @@ -151,6 +152,7 @@ contract Court is ERC900, ApproveAndCallFallBack { terms[ZERO_TERM].startTime = _firstTermStartTime - _termDuration; sumTree.init(); + assert(sumTree.insert(0) == bytes32(0)); // first tree item is an empty juror } function heartbeat(uint64 _termTransitions) public { @@ -353,8 +355,11 @@ contract Court is ERC900, ApproveAndCallFallBack { } for (uint256 j = 0; j < egressLength; j++) { address jurorEgress = _incomingTerm.egressQueue[j]; - sumTree.set(accounts[jurorEgress].sumTreeId, 0); - delete accounts[jurorEgress].sumTreeId; + + if (accounts[jurorEgress].sumTreeId != bytes32(0)) { + sumTree.set(accounts[jurorEgress].sumTreeId, 0); + delete accounts[jurorEgress].sumTreeId; + } } for (uint256 k = 0; k < updatesLength; k++) { address jurorUpdate = _incomingTerm.updatesQueue[k]; @@ -399,7 +404,7 @@ contract Court is ERC900, ApproveAndCallFallBack { uint256 _jurorFee, uint256 _heartbeatFee ) internal { - require(feeChangeTerm > term || term == ZERO_TERM, ERROR_PAST_TERM_TERM_FEE_CHANGE); + require(feeChangeTerm > term || term == ZERO_TERM, ERROR_PAST_TERM_FEE_CHANGE); if (feeChangeTerm != ZERO_TERM) { terms[feeChangeTerm].feeStructureId = 0; // reset previously set fee structure change From 6545acc50b191591fbb0cd8ffa50c08a81b4a9a8 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Fri, 1 Mar 2019 19:58:41 +0100 Subject: [PATCH 07/27] Disputes scaffolding --- contracts/Agreement.sol | 33 +++ contracts/Court.sol | 222 ++++++++++++++++-- .../standards/arbitration/Arbitrable.sol | 64 ++--- .../standards/arbitration/Arbitrator.sol | 94 -------- .../standards/arbitration/IArbitrable.sol | 69 +++--- contracts/standards/erc165/ERC165.sol | 6 + contracts/test/CourtMock.sol | 4 + contracts/test/TestFactory.sol | 2 + test/court-lifecycle.js | 2 + 9 files changed, 305 insertions(+), 191 deletions(-) create mode 100644 contracts/Agreement.sol delete mode 100644 contracts/standards/arbitration/Arbitrator.sol create mode 100644 contracts/standards/erc165/ERC165.sol diff --git a/contracts/Agreement.sol b/contracts/Agreement.sol new file mode 100644 index 00000000..0a3a36a0 --- /dev/null +++ b/contracts/Agreement.sol @@ -0,0 +1,33 @@ +pragma solidity ^0.4.15; + +import "./standards/arbitration/Arbitrable.sol"; + + +contract Agreement is Arbitrable /* AragonApp/Trigger */ { + address[] parties; + + // TODO: Probably needs to be moved into an 'initialize()' function at some point + constructor(address _court, address[] _parties) + public + Arbitrable(_court) { + + parties = _parties; + } + + function canSubmitEvidence(uint256 _disputeId, address _submitter) public view returns (bool) { + // TODO: should check court to see whether evidence can be submitted for this particular dispute at this point + uint256 partiesLength = parties.length; + for (uint256 i = 0; i < partiesLength; i++) { + if (parties[i] == msg.sender) { + return true; + } + } + } + + /** + * @dev Execute a ruling of a dispute. + * @param _disputeId Id of the dispute in the Court contract. + * @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Not able/wanting to make a decision". + */ + function _executeRuling(uint256 _disputeId, uint256 _ruling) internal; +} diff --git a/contracts/Court.sol b/contracts/Court.sol index 4d745dc2..224f5132 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -3,8 +3,7 @@ pragma solidity ^0.4.24; // TODO: pin solc // Inspired by: Kleros.sol https://github.com/kleros/kleros @ 7281e69 import "./lib/HexSumTree.sol"; import "./lib/ArrayUtils.sol"; -import "./standards/arbitration/Arbitrator.sol"; -import "./standards/arbitration/Arbitrable.sol"; +import "./standards/arbitration/IArbitrable.sol"; import "./standards/erc900/ERC900.sol"; import { ApproveAndCallFallBack } from "@aragon/apps-shared-minime/contracts/MiniMeToken.sol"; @@ -25,22 +24,24 @@ contract Court is ERC900, ApproveAndCallFallBack { struct Account { mapping (address => uint256) balances; // token addr -> balance - AccountState state; // whether the account is not a juror, a current juror or a past juror - uint64 fromTerm; // first term in which the juror can be drawn - uint64 toTerm; // last term in which the juror can be drawn - bytes32 sumTreeId; // key in the sum tree used for sortition - uint128[] pendingDisputes; // disputes in which the juror was drawn which haven't resolved + AccountState state; // whether the account is not a juror, a current juror or a past juror + uint64 fromTerm; // first term in which the juror can be drawn + uint64 toTerm; // last term in which the juror can be drawn + uint64 pendingDisputes; // disputes in which the juror was drawn which haven't resolved + bytes32 sumTreeId; // key in the sum tree used for sortition } struct FeeStructure { ERC20 feeToken; - uint256 jurorFee; // per juror, total dispute fee = jurorFee * jurors drawn - uint256 heartbeatFee; // per dispute, total heartbeat fee = heartbeatFee * disputes/appeals in term + uint16 governanceShare; // ‱ of fees going to the governor (1/10,000) + uint256 jurorFee; // per juror, total dispute fee = jurorFee * jurors drawn + uint256 heartbeatFee; // per dispute, total heartbeat fee = heartbeatFee * disputes/appeals in term + uint256 draftFee; // per dispute } struct Term { uint64 startTime; // timestamp when the term started - uint64 dependingDraws; // disputes or appeals pegged to this term for randomness + uint64 dependingDrafts; // disputes or appeals pegged to this term for randomness uint64 feeStructureId; // fee structure for this term (index in feeStructures array) uint64 randomnessBN; // block number for entropy uint256 randomness; // entropy from randomnessBN block hash @@ -49,10 +50,38 @@ contract Court is ERC900, ApproveAndCallFallBack { address[] updatesQueue; // jurors whose stake has been updated } + struct JurorVote { + bytes32 commitment; + uint8 ruling; + address juror; + } + + struct AdjudicationRound { + uint64 draftTerm; + uint64 jurorNumber; + address triggeredBy; + JurorVote[] votes; + } + + enum Ruling { + Missing, + RefusedRuling + // After this, it is specicific to the case + } + + enum DisputeState { + PreDraft, + Adjudicating, + Appealable, // TODO: do we need to store this state? + Executable, // TODO: do we need to store this state? + Executed, + Dismissed + } + struct Dispute { - Arbitrable subject; - uint64 termId; - // TODO + IArbitrable subject; + DisputeState state; + AdjudicationRound[] rounds; } // State constants which are set in the constructor and can't change @@ -91,10 +120,15 @@ contract Court is ERC900, ApproveAndCallFallBack { string internal constant ERROR_JUROR_TOKENS_AT_STAKE = "COURT_JUROR_TOKENS_AT_STAKE"; string internal constant ERROR_BALANCE_TOO_LOW = "COURT_BALANCE_TOO_LOW"; string internal constant ERROR_TOKEN_TRANSFER_FAILED = "COURT_TOKEN_TRANSFER_FAILED"; + string internal constant ERROR_GOVENANCE_FEE_TOO_HIGH = "COURT_GOVENANCE_FEE_TOO_HIGH"; + string internal constant ERROR_ENTITY_CANT_DISMISS = "COURT_ENTITY_CANT_DISMISS"; + string internal constant ERROR_CANT_DISMISS_AFTER_DRAFT = "COURT_CANT_DISMISS_AFTER_DRAFT"; uint64 internal constant ZERO_TERM = 0; // invalid term that doesn't accept disputes uint64 public constant MANUAL_DEACTIVATION = uint64(-1); uint64 internal constant MODIFIER_ALLOWED_TERM_TRANSITIONS = 1; + bytes4 private constant ARBITRABLE_INTERFACE_ID = 0xabababab; // TODO: interface id + uint16 internal constant GOVERNANCE_FEE_DIVISOR = 10000; // ‱ event NewTerm(uint64 term, address indexed heartbeatSender); event NewFeeStructure(uint64 fromTerm, uint64 feeStructureId); @@ -125,6 +159,8 @@ contract Court is ERC900, ApproveAndCallFallBack { * @param _feeToken The address of the token contract that is used to pay for fees. * @param _jurorFee The amount of _feeToken that is paid per juror per dispute * @param _heartbeatFee The amount of _feeToken per dispute to cover maintenance costs. + * @param _draftFee The amount of _feeToken per juror to cover the drafting cost. + * @param _governanceShare Share in ‱ of fees that are paid to the governor. * @param _governor Address of the governor contract. * @param _firstTermStartTime Timestamp in seconds when the court will open (to give time for juror onboarding) * @param _jurorActivationDust Minimum amount of juror tokens that can be activated @@ -136,6 +172,8 @@ contract Court is ERC900, ApproveAndCallFallBack { ERC20 _feeToken, uint256 _jurorFee, uint256 _heartbeatFee, + uint256 _draftFee, + uint16 _governanceShare, address _governor, uint64 _firstTermStartTime, uint256 _jurorActivationDust, @@ -148,7 +186,14 @@ contract Court is ERC900, ApproveAndCallFallBack { jurorCooldownTerms = _jurorCooldownTerms; feeStructures.length = 1; // leave index 0 empty - _setFeeStructure(ZERO_TERM, _feeToken, _jurorFee, _heartbeatFee); + _setFeeStructure( + ZERO_TERM, + _feeToken, + _jurorFee, + _heartbeatFee, + _draftFee, + _governanceShare + ); terms[ZERO_TERM].startTime = _firstTermStartTime - _termDuration; sumTree.init(); @@ -177,10 +222,10 @@ contract Court is ERC900, ApproveAndCallFallBack { _processJurorQueues(nextTerm); FeeStructure storage feeStructure = feeStructures[nextTerm.feeStructureId]; - uint256 totalFee = nextTerm.dependingDraws * feeStructure.heartbeatFee; + uint256 totalFee = nextTerm.dependingDrafts * feeStructure.heartbeatFee; if (totalFee > 0) { - _assignTokens(feeStructure.feeToken, heartbeatSender, totalFee); + _payFees(feeStructure.feeToken, heartbeatSender, totalFee, feeStructure.governanceShare); } term += 1; @@ -191,6 +236,122 @@ contract Court is ERC900, ApproveAndCallFallBack { } } + event NewDispute(uint256 indexed disputeId, address indexed subject, uint64 indexed draftTerm, uint64 jurorNumber); + + function createDispute(IArbitrable _subject, uint64 _jurorNumber, uint64 _draftTerm) + external + ensureTerm + { + // 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 interface + + (ERC20 feeToken, uint256 feeAmount,) = feeForJurorDraft(_draftTerm, _jurorNumber); + require(feeToken.safeTransferFrom(msg.sender, this, feeAmount), ERROR_DEPOSIT_FAILED); + + uint256 disputeId = disputes.length; + disputes.length = disputeId + 1; + + Dispute storage dispute = disputes[disputeId]; + dispute.subject = _subject; + dispute.state = DisputeState.PreDraft; + dispute.rounds.length = 1; + + AdjudicationRound storage round = dispute.rounds[0]; + round.draftTerm = _draftTerm; + round.jurorNumber = _jurorNumber; + round.triggeredBy = msg.sender; + + terms[_draftTerm].dependingDrafts += 1; + + emit NewDispute(disputeId, _subject, _draftTerm, _jurorNumber); + } + + event DisputeStateChanged(uint256 indexed disputeId, DisputeState indexed state); + + function dismissDispute(uint256 _disputeId) + external + ensureTerm + { + Dispute storage dispute = disputes[_disputeId]; + uint256 roundId = dispute.rounds.length - 1; + AdjudicationRound storage round = dispute.rounds[roundId]; + + require(round.triggeredBy == msg.sender, ERROR_ENTITY_CANT_DISMISS); + require(dispute.state == DisputeState.PreDraft && round.draftTerm > term, ERROR_CANT_DISMISS_AFTER_DRAFT); + + dispute.state = roundId == 0 ? DisputeState.Dismissed : DisputeState.Appealable; + + terms[round.draftTerm].dependingDrafts -= 1; + + // refund fees + (ERC20 feeToken, uint256 feeAmount, uint16 governanceShare) = feeForJurorDraft(round.draftTerm, round.jurorNumber); + _payFees(feeToken, round.triggeredBy, feeAmount, governanceShare); + + emit DisputeStateChanged(_disputeId, dispute.state); + } + + string internal constant ERROR_ROUND_ALREADY_DRAFTED = "COURT_ROUND_ALREADY_DRAFTED"; + string internal constant ERROR_NOT_DRAFT_TERM = "COURT_NOT_DRAFT_TERM"; + + function draftAdjudicationRound(uint256 _disputeId) + external + ensureTerm + { + Dispute storage dispute = disputes[_disputeId]; + uint256 roundId = dispute.rounds.length - 1; + AdjudicationRound storage round = dispute.rounds[roundId]; + + // TODO: Work on recovery if draft doesn't occur in the term it was supposed to + // it should be scheduled for a future draft and require to pay the heartbeat fee for the term + require(round.draftTerm == term, ERROR_NOT_DRAFT_TERM); + require(dispute.state == DisputeState.PreDraft, ERROR_ROUND_ALREADY_DRAFTED); + + // TODO: actually draft jurors + + dispute.state = DisputeState.Adjudicating; + + FeeStructure storage fees = feeStructureForTerm(term); + _payFees(fees.feeToken, msg.sender, fees.draftFee * round.jurorNumber, fees.governanceShare); + + emit DisputeStateChanged(_disputeId, dispute.state); + } + + /** + * @dev Assumes term is up to date + */ + function feeForJurorDraft(uint64 _draftTerm, uint64 _jurorNumber) public view returns (ERC20 feeToken, uint256 feeAmount, uint16 governanceShare) { + FeeStructure storage fees = feeStructureForTerm(_draftTerm); + + feeToken = fees.feeToken; + governanceShare = fees.governanceShare; + feeAmount = fees.heartbeatFee + _jurorNumber * (fees.jurorFee + fees.draftFee); + } + + function feeStructureForTerm(uint64 _term) internal view returns (FeeStructure storage) { + uint64 feeTerm; + + if (_term <= term) { + feeTerm = _term; // for past terms, use the fee structure of the specific term + } else if (feeChangeTerm <= _term) { + feeTerm = feeChangeTerm; // if fees are changing before the draft, use the incoming fee schedule + } else { + feeTerm = term; // if no changes are scheduled, use the current term fee schedule (which CANNOT change for this term) + } + + uint256 feeStructureId = uint256(terms[feeTerm].feeStructureId); + return feeStructures[feeStructureId]; + } + + function _payFees(ERC20 _feeToken, address _to, uint256 _amount, uint16 _governanceShare) internal { + _assignTokens(_feeToken, governor, _amount * uint256(GOVERNANCE_FEE_DIVISOR - _governanceShare) / GOVERNANCE_FEE_DIVISOR); + + if (_governanceShare > 0) { + _assignTokens(_feeToken, governor, _amount * uint256(_governanceShare) / GOVERNANCE_FEE_DIVISOR); + } + } + // TODO: should we charge heartbeat fees to jurors? function activate(uint64 _fromTerm, uint64 _toTerm) external ensureTerm { address jurorAddress = msg.sender; @@ -202,7 +363,7 @@ contract Court is ERC900, ApproveAndCallFallBack { require(account.balances[jurorToken] >= jurorActivationDust, ERROR_TOKENS_BELOW_DUST); if (term == ZERO_TERM && _fromTerm == ZERO_TERM + 1) { - // allow direct judge onboardings before term 1 starts (no disputes depend on term 0) + // allow direct juror onboardings before term 1 starts (no disputes depend on term 0) _insertJurorToSumTree(jurorAddress); } else { // TODO: check queue size limit @@ -271,10 +432,11 @@ contract Court is ERC900, ApproveAndCallFallBack { */ function receiveApproval(address _from, uint256 _amount, address token, bytes) public - only(jurorToken) // allow sending fees with it as well only(token) { - _stake(_from, _from, _amount); + if (token == address(jurorToken)) { + _stake(_from, _from, _amount); + } } function unstake(uint256 _amount, bytes) external { @@ -385,26 +547,32 @@ contract Court is ERC900, ApproveAndCallFallBack { function _stake(address _from, address _to, uint256 _amount) internal { require(_amount > 0, ERROR_ZERO_TRANSFER); - require(jurorToken.transferFrom(_from, this, _amount), ERROR_DEPOSIT_FAILED); + require(jurorToken.safeTransferFrom(_from, this, _amount), ERROR_DEPOSIT_FAILED); accounts[_to].balances[jurorToken] += _amount; emit Staked(_to, _amount, totalStakedFor(_to), ""); } - function _assignTokens(ERC20 _feeToken, address _to, uint256 _amount) internal { - accounts[_to].balances[_feeToken] += _amount; + function _assignTokens(ERC20 _token, address _to, uint256 _amount) internal { + accounts[_to].balances[_token] += _amount; - emit TokenBalanceChange(_feeToken, _to, _amount, true); + emit TokenBalanceChange(_token, _to, _amount, true); } function _setFeeStructure( uint64 _fromTerm, ERC20 _feeToken, uint256 _jurorFee, - uint256 _heartbeatFee + uint256 _heartbeatFee, + uint256 _draftFee, + uint16 _governanceShare ) internal { + // TODO: Require fee changes happening at least X terms in the future + // Where X is the amount of terms in the future a dispute can be scheduled to be drafted at + require(feeChangeTerm > term || term == ZERO_TERM, ERROR_PAST_TERM_FEE_CHANGE); + require(_governanceShare <= GOVERNANCE_FEE_DIVISOR, ERROR_GOVENANCE_FEE_TOO_HIGH); if (feeChangeTerm != ZERO_TERM) { terms[feeChangeTerm].feeStructureId = 0; // reset previously set fee structure change @@ -412,8 +580,10 @@ contract Court is ERC900, ApproveAndCallFallBack { FeeStructure memory feeStructure = FeeStructure({ feeToken: _feeToken, + governanceShare: _governanceShare, jurorFee: _jurorFee, - heartbeatFee: _heartbeatFee + heartbeatFee: _heartbeatFee, + draftFee: _draftFee }); uint64 feeStructureId = uint64(feeStructures.push(feeStructure) - 1); @@ -433,7 +603,7 @@ contract Court is ERC900, ApproveAndCallFallBack { function isJurorBalanceUnlocked(address _jurorAddress) internal view returns (bool) { Account storage account = accounts[_jurorAddress]; - return term > account.toTerm + jurorCooldownTerms && account.pendingDisputes.length == 0; + return term > account.toTerm + jurorCooldownTerms && account.pendingDisputes == 0; } } diff --git a/contracts/standards/arbitration/Arbitrable.sol b/contracts/standards/arbitration/Arbitrable.sol index 4e01152c..b1f64513 100644 --- a/contracts/standards/arbitration/Arbitrable.sol +++ b/contracts/standards/arbitration/Arbitrable.sol @@ -1,51 +1,53 @@ -/** - * @title Arbitrable - * @author Clément Lesaege - - * Bug Bounties: This code hasn't undertaken a bug bounty program yet. - */ - pragma solidity ^0.4.15; import "./IArbitrable.sol"; +import "../erc165/ERC165.sol"; + -/** @title Arbitrable - * Arbitrable abstract contract. - * When developing arbitrable contracts, we need to: - * -Define the action taken when a ruling is received by the contract. We should do so in executeRuling. - * -Allow dispute creation. For this a function must: - * -Call arbitrator.createDispute.value(_fee)(_choices,_extraData); - * -Create the event Dispute(_arbitrator,_disputeID,_rulingOptions); - */ -contract Arbitrable is IArbitrable { - Arbitrator public arbitrator; - bytes public arbitratorExtraData; // Extra data to require particular dispute and appeal behaviour. +contract Arbitrable is IArbitrable, ERC165 { + address public court; // TODO: replace for ICourt or Court interface - modifier onlyArbitrator {require(msg.sender == address(arbitrator), "Can only be called by the arbitrator."); _;} + bytes4 private constant ERC165_INTERFACE_ID = 0x01ffc9a7; + bytes4 private constant ARBITRABLE_INTERFACE_ID = 0xabababab; // TODO: interface id + + string private constant ERROR_NOT_COURT = "ARBITRABLE_NOT_COURT"; + string private constant ERROR_CANNOT_SUBMIT_EVIDENCE = "ARBITRABLE_CANNOT_SUBMIT_EVIDENCE"; /** @dev Constructor. Choose the arbitrator. - * @param _arbitrator The arbitrator of the contract. - * @param _arbitratorExtraData Extra data for the arbitrator. + * @param _court The address of the court that arbitrates the contract. */ - constructor(Arbitrator _arbitrator, bytes _arbitratorExtraData) public { - arbitrator = _arbitrator; - arbitratorExtraData = _arbitratorExtraData; + constructor(address _court) public { + court = _court; } - /** @dev Give a ruling for a dispute. Must be called by the arbitrator. + /** + * @dev Give a ruling for a dispute. Must be called by the arbitrator. * The purpose of this function is to ensure that the address calling it has the right to rule on the contract. - * @param _disputeID ID of the dispute in the Arbitrator contract. + * @param _disputeId Id of the dispute in the Court contract. * @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Not able/wanting to make a decision". */ - function rule(uint _disputeID, uint _ruling) public onlyArbitrator { - emit Ruling(Arbitrator(msg.sender),_disputeID,_ruling); + function rule(uint256 _disputeId, uint256 _ruling) external { + require(msg.sender == court, ERROR_NOT_COURT); + + _executeRuling(_disputeId, _ruling); - executeRuling(_disputeID,_ruling); + emit CourtRuling(msg.sender, _disputeId, _ruling); } + function submitEvidence(uint256 _disputeId, bytes _evidence) external { + require(canSubmitEvidence(_disputeId, msg.sender), ERROR_CANNOT_SUBMIT_EVIDENCE); + + emit NewEvidence(court, _disputeId, msg.sender, _evidence); + } + + function supportsInterface(bytes4 _interfaceId) external pure returns (bool) { + return _interfaceId == ARBITRABLE_INTERFACE_ID || _interfaceId == ERC165_INTERFACE_ID; + } - /** @dev Execute a ruling of a dispute. - * @param _disputeID ID of the dispute in the Arbitrator contract. + /** + * @dev Execute a ruling of a dispute. + * @param _disputeId Id of the dispute in the Court contract. * @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Not able/wanting to make a decision". */ - function executeRuling(uint _disputeID, uint _ruling) internal; + function _executeRuling(uint256 _disputeId, uint256 _ruling) internal; } diff --git a/contracts/standards/arbitration/Arbitrator.sol b/contracts/standards/arbitration/Arbitrator.sol deleted file mode 100644 index c732788b..00000000 --- a/contracts/standards/arbitration/Arbitrator.sol +++ /dev/null @@ -1,94 +0,0 @@ -/** - * @title Arbitrator - * @author Clément Lesaege - - * Bug Bounties: This code hasn't undertaken a bug bounty program yet. - */ - -pragma solidity ^0.4.15; - -import "./Arbitrable.sol"; - -/** @title Arbitrator - * Arbitrator abstract contract. - * When developing arbitrator contracts we need to: - * -Define the functions for dispute creation (createDispute) and appeal (appeal). Don't forget to store the arbitrated contract and the disputeID (which should be unique, use nbDisputes). - * -Define the functions for cost display (arbitrationCost and appealCost). - * -Allow giving rulings. For this a function must call arbitrable.rule(disputeID, ruling). - */ -contract Arbitrator { - - enum DisputeStatus {Waiting, Appealable, Solved} - - modifier requireArbitrationFee(bytes _extraData) { - require(msg.value >= arbitrationCost(_extraData), "Not enough ETH to cover arbitration costs."); - _; - } - modifier requireAppealFee(uint _disputeID, bytes _extraData) { - require(msg.value >= appealCost(_disputeID, _extraData), "Not enough ETH to cover appeal costs."); - _; - } - - /** @dev To be raised when a dispute is created. - * @param _disputeID ID of the dispute. - * @param _arbitrable The contract which created the dispute. - */ - event DisputeCreation(uint indexed _disputeID, Arbitrable indexed _arbitrable); - - /** @dev To be raised when a dispute can be appealed. - * @param _disputeID ID of the dispute. - */ - event AppealPossible(uint indexed _disputeID, Arbitrable indexed _arbitrable); - - /** @dev To be raised when the current ruling is appealed. - * @param _disputeID ID of the dispute. - * @param _arbitrable The contract which created the dispute. - */ - event AppealDecision(uint indexed _disputeID, Arbitrable indexed _arbitrable); - - /** @dev Create a dispute. Must be called by the arbitrable contract. - * Must be paid at least arbitrationCost(_extraData). - * @param _choices Amount of choices the arbitrator can make in this dispute. - * @param _extraData Can be used to give additional info on the dispute to be created. - * @return disputeID ID of the dispute created. - */ - function createDispute(uint _choices, bytes _extraData) public requireArbitrationFee(_extraData) payable returns(uint disputeID) {} - - /** @dev Compute the cost of arbitration. It is recommended not to increase it often, as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. - * @param _extraData Can be used to give additional info on the dispute to be created. - * @return fee Amount to be paid. - */ - function arbitrationCost(bytes _extraData) public view returns(uint fee); - - /** @dev Appeal a ruling. Note that it has to be called before the arbitrator contract calls rule. - * @param _disputeID ID of the dispute to be appealed. - * @param _extraData Can be used to give extra info on the appeal. - */ - function appeal(uint _disputeID, bytes _extraData) public requireAppealFee(_disputeID,_extraData) payable { - emit AppealDecision(_disputeID, Arbitrable(msg.sender)); - } - - /** @dev Compute the cost of appeal. It is recommended not to increase it often, as it can be higly time and gas consuming for the arbitrated contracts to cope with fee augmentation. - * @param _disputeID ID of the dispute to be appealed. - * @param _extraData Can be used to give additional info on the dispute to be created. - * @return fee Amount to be paid. - */ - function appealCost(uint _disputeID, bytes _extraData) public view returns(uint fee); - - /** @dev Compute the start and end of the dispute's current or next appeal period, if possible. - * @param _disputeID ID of the dispute. - * @return The start and end of the period. - */ - function appealPeriod(uint _disputeID) public view returns(uint start, uint end) {} - - /** @dev Return the status of a dispute. - * @param _disputeID ID of the dispute to rule. - * @return status The status of the dispute. - */ - function disputeStatus(uint _disputeID) public view returns(DisputeStatus status); - - /** @dev Return the current ruling of a dispute. This is useful for parties to know if they should appeal. - * @param _disputeID ID of the dispute. - * @return ruling The ruling which has been given or the one which will be given if there is no appeal. - */ - function currentRuling(uint _disputeID) public view returns(uint ruling); -} diff --git a/contracts/standards/arbitration/IArbitrable.sol b/contracts/standards/arbitration/IArbitrable.sol index 381cb17d..97e85b93 100644 --- a/contracts/standards/arbitration/IArbitrable.sol +++ b/contracts/standards/arbitration/IArbitrable.sol @@ -1,54 +1,43 @@ -/** - * @title IArbitrable - * @author Enrique Piqueras - - * Bug Bounties: This code hasn't undertaken a bug bounty program yet. - */ - pragma solidity ^0.4.15; -import "./Arbitrator.sol"; -/** @title IArbitrable - * Arbitrable interface. - * When developing arbitrable contracts, we need to: - * -Define the action taken when a ruling is received by the contract. We should do so in executeRuling. - * -Allow dispute creation. For this a function must: - * -Call arbitrator.createDispute.value(_fee)(_choices,_extraData); - * -Create the event Dispute(_arbitrator,_disputeID,_rulingOptions); - */ interface IArbitrable { - /** @dev To be emmited when meta-evidence is submitted. - * @param _metaEvidenceID Unique identifier of meta-evidence. - * @param _evidence A link to the meta-evidence JSON. - */ - event MetaEvidence(uint indexed _metaEvidenceID, string _evidence); - - /** @dev To be emmited when a dispute is created to link the correct meta-evidence to the disputeID - * @param _arbitrator The arbitrator of the contract. - * @param _disputeID ID of the dispute in the Arbitrator contract. - * @param _metaEvidenceID Unique identifier of meta-evidence. + /** + * @dev To be emmited when a dispute is created to link the correct meta-evidence to the disputeId + * @param _court The court resolving the dispute. + * @param _disputeId Id of the dispute in the Court. + * @param _baseEvidence Base evidence or location of the base evidence being submitted */ - event Dispute(Arbitrator indexed _arbitrator, uint indexed _disputeID, uint _metaEvidenceID); + event NewDispute(address indexed _court, uint256 indexed _disputeId, bytes _baseEvidence); - /** @dev To be raised when evidence are submitted. Should point to the ressource (evidences are not to be stored on chain due to gas considerations). - * @param _arbitrator The arbitrator of the contract. - * @param _disputeID ID of the dispute in the Arbitrator contract. - * @param _party The address of the party submiting the evidence. Note that 0x0 refers to evidence not submitted by any party. - * @param _evidence A URI to the evidence JSON file whose name should be its keccak256 hash followed by .json. + /** + * @dev To be raised when evidence are submitted. Should point to the ressource (evidences are not to be stored on chain due to gas considerations). + * @param _court The court resolving the dispute. + * @param _disputeId Id of the dispute in the Court. + * @param _submitter The address of the entity submitting the evidence. + * @param _evidence Evidence or location of the evidence being submitted */ - event Evidence(Arbitrator indexed _arbitrator, uint indexed _disputeID, address indexed _party, string _evidence); + event NewEvidence(address indexed _court, uint256 indexed _disputeId, address indexed _submitter, bytes _evidence); - /** @dev To be raised when a ruling is given. - * @param _arbitrator The arbitrator giving the ruling. - * @param _disputeID ID of the dispute in the Arbitrator contract. + /** + * @dev To be raised when a ruling is given. + * @param _court The court giving the ruling. + * @param _disputeId Id of the dispute in the Court. * @param _ruling The ruling which was given. */ - event Ruling(Arbitrator indexed _arbitrator, uint indexed _disputeID, uint _ruling); + event CourtRuling(address indexed _court, uint256 indexed _disputeId, uint256 _ruling); - /** @dev Give a ruling for a dispute. Must be called by the arbitrator. + /** @dev Give a ruling for a dispute. Must be called by the court. * The purpose of this function is to ensure that the address calling it has the right to rule on the contract. - * @param _disputeID ID of the dispute in the Arbitrator contract. - * @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Not able/wanting to make a decision". + * @param _disputeId Id of the dispute in the Court. + * @param _ruling Ruling given by the court. Note that 0 is reserved for "Not able/wanting to make a decision". + */ + function rule(uint256 _disputeId, uint256 _ruling) external; + + /** + * @param _disputeId Id of the dispute in the Court + * @param _submitter address of the entity that wishes to submit evidence + * @return bool whether the submitter is allowed to submit evidence for the dispute */ - function rule(uint _disputeID, uint _ruling) public; + function canSubmitEvidence(uint256 _disputeId, address _submitter) public view returns (bool); } diff --git a/contracts/standards/erc165/ERC165.sol b/contracts/standards/erc165/ERC165.sol new file mode 100644 index 00000000..3dd62f71 --- /dev/null +++ b/contracts/standards/erc165/ERC165.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.4.24; + + +interface ERC165 { + function supportsInterface(bytes4 interfaceId) external pure returns (bool); +} diff --git a/contracts/test/CourtMock.sol b/contracts/test/CourtMock.sol index c1f82283..f218606a 100644 --- a/contracts/test/CourtMock.sol +++ b/contracts/test/CourtMock.sol @@ -13,6 +13,8 @@ contract CourtMock is Court { ERC20 _feeToken, uint256 _jurorFee, uint256 _heartbeatFee, + uint256 _draftFee, + uint16 _governanceShare, address _governor, uint64 _firstTermStartTime, uint256 _jurorActivationDust, @@ -23,6 +25,8 @@ contract CourtMock is Court { _feeToken, _jurorFee, _heartbeatFee, + _draftFee, + _governanceShare, _governor, _firstTermStartTime, _jurorActivationDust, diff --git a/contracts/test/TestFactory.sol b/contracts/test/TestFactory.sol index ac139c29..96d6e2cc 100644 --- a/contracts/test/TestFactory.sol +++ b/contracts/test/TestFactory.sol @@ -36,6 +36,8 @@ contract CourtFactory is Factory { ERC20(0), // no fees 0, 0, + 0, + 0, address(this), uint64(block.timestamp + 60 * 60), 1, diff --git a/test/court-lifecycle.js b/test/court-lifecycle.js index 0ff9a55f..11da21e7 100644 --- a/test/court-lifecycle.js +++ b/test/court-lifecycle.js @@ -67,6 +67,8 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { ZERO_ADDRESS, 0, 0, + 0, + 0, governor, firstTermStart, jurorActivationDust, From c405771044bf3bce185ba2b7876cb5d0a19738b5 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Fri, 1 Mar 2019 21:10:46 +0100 Subject: [PATCH 08/27] Rebase sortition --- contracts/Court.sol | 10 +++++----- contracts/lib/HexSumTree.sol | 14 +++++++++----- contracts/test/HexSumTreePublic.sol | 4 ---- test/court-lifecycle.js | 14 ++------------ test/helpers/assert-revert.js | 13 +++++++++++++ test/hex-tree.js | 18 ++++++------------ 6 files changed, 35 insertions(+), 38 deletions(-) create mode 100644 test/helpers/assert-revert.js diff --git a/contracts/Court.sol b/contracts/Court.sol index 224f5132..8c52c48d 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -28,7 +28,7 @@ contract Court is ERC900, ApproveAndCallFallBack { uint64 fromTerm; // first term in which the juror can be drawn uint64 toTerm; // last term in which the juror can be drawn uint64 pendingDisputes; // disputes in which the juror was drawn which haven't resolved - bytes32 sumTreeId; // key in the sum tree used for sortition + uint256 sumTreeId; // key in the sum tree used for sortition } struct FeeStructure { @@ -99,7 +99,7 @@ contract Court is ERC900, ApproveAndCallFallBack { uint64 public term; uint64 public feeChangeTerm; mapping (address => Account) public accounts; - mapping (bytes32 => address) public jurorsByTreeId; + mapping (uint256 => address) public jurorsByTreeId; mapping (uint64 => Term) public terms; HexSumTree.Tree internal sumTree; Dispute[] public disputes; @@ -197,7 +197,7 @@ contract Court is ERC900, ApproveAndCallFallBack { terms[ZERO_TERM].startTime = _firstTermStartTime - _termDuration; sumTree.init(); - assert(sumTree.insert(0) == bytes32(0)); // first tree item is an empty juror + assert(sumTree.insert(0) == 0); // first tree item is an empty juror } function heartbeat(uint64 _termTransitions) public { @@ -518,7 +518,7 @@ contract Court is ERC900, ApproveAndCallFallBack { for (uint256 j = 0; j < egressLength; j++) { address jurorEgress = _incomingTerm.egressQueue[j]; - if (accounts[jurorEgress].sumTreeId != bytes32(0)) { + if (accounts[jurorEgress].sumTreeId != 0) { sumTree.set(accounts[jurorEgress].sumTreeId, 0); delete accounts[jurorEgress].sumTreeId; } @@ -540,7 +540,7 @@ contract Court is ERC900, ApproveAndCallFallBack { } function _insertJurorToSumTree(address _jurorAddress) internal { - bytes32 sumTreeId = sumTree.insert(totalStakedFor(_jurorAddress)); + uint256 sumTreeId = sumTree.insert(totalStakedFor(_jurorAddress)); accounts[_jurorAddress].sumTreeId = sumTreeId; jurorsByTreeId[sumTreeId] = _jurorAddress; } diff --git a/contracts/lib/HexSumTree.sol b/contracts/lib/HexSumTree.sol index 40c81c72..303a55ae 100644 --- a/contracts/lib/HexSumTree.sol +++ b/contracts/lib/HexSumTree.sol @@ -21,7 +21,7 @@ library HexSumTree { string private constant ERROR_SORTITION_OUT_OF_BOUNDS = "SUM_TREE_SORTITION_OUT_OF_BOUNDS"; string private constant ERROR_NEW_KEY_NOT_ADJACENT = "SUM_TREE_NEW_KEY_NOT_ADJACENT"; - string private constant ERROR_UPDATE_OVERFLOW = "SUM_UPDATE_OVERFLOW"; + string private constant ERROR_UPDATE_OVERFLOW = "SUM_TREE_UPDATE_OVERFLOW"; function init(Tree storage self) internal { self.rootDepth = INSERTION_DEPTH + 1; @@ -52,7 +52,11 @@ library HexSumTree { uint256 oldValue = self.nodes[INSERTION_DEPTH][key]; self.nodes[INSERTION_DEPTH][key] = value; - updateSums(self, key, int256(value - oldValue)); + if (value > oldValue) { + updateSums(self, key, value - oldValue, true); + } else if (value < oldValue) { + updateSums(self, key, oldValue - value, false); + } } function _sortition(Tree storage self, uint256 value, uint256 node, uint256 depth) private view returns (uint256 key) { @@ -94,7 +98,7 @@ library HexSumTree { // Invariant: this point should never be reached } - function updateSums(Tree storage self, uint256 key, int256 delta) private { + function updateSums(Tree storage self, uint256 key, uint256 delta, bool sum) private { uint256 newRootDepth = sharedPrefix(self.rootDepth, key); if (self.rootDepth != newRootDepth) { @@ -109,10 +113,10 @@ library HexSumTree { ancestorKey = ancestorKey & mask; // Invariant: this will never underflow. - self.nodes[i][ancestorKey] = uint256(int256(self.nodes[i][ancestorKey]) + delta); + self.nodes[i][ancestorKey] = sum ? self.nodes[i][ancestorKey] + delta : self.nodes[i][ancestorKey] - delta; } // it's only needed to check the last one, as the sum increases going up through the tree - require(delta <= 0 || self.nodes[self.rootDepth][ancestorKey] >= uint256(delta), ERROR_UPDATE_OVERFLOW); + require(!sum || self.nodes[self.rootDepth][ancestorKey] >= delta, ERROR_UPDATE_OVERFLOW); } function totalSum(Tree storage self) internal view returns (uint256) { diff --git a/contracts/test/HexSumTreePublic.sol b/contracts/test/HexSumTreePublic.sol index 461e8a79..12b2a1d4 100644 --- a/contracts/test/HexSumTreePublic.sol +++ b/contracts/test/HexSumTreePublic.sol @@ -40,10 +40,6 @@ contract HexSumTreePublic { } } - function set(uint256 k, uint256 v) public { - tree.set(bytes32(k), v); - } - function sortition(uint256 v) public view returns (uint256) { return uint256(tree.sortition(v)); } diff --git a/test/court-lifecycle.js b/test/court-lifecycle.js index 11da21e7..9dab1935 100644 --- a/test/court-lifecycle.js +++ b/test/court-lifecycle.js @@ -1,3 +1,5 @@ +const assertRevert = require('./helpers/assert-revert') + const TokenFactory = artifacts.require('TokenFactory') const CourtMock = artifacts.require('CourtMock') @@ -8,18 +10,6 @@ const getLog = (receipt, logName, argName) => { return log ? log.args[argName] : null } -const assertRevert = async (receiptPromise, reason) => { - try { - await receiptPromise - assert.fail(`Expected a revert for reason: ${reason}`) - } catch (e) { - if (reason) { - e.reason = e.message.replace('VM Exception while processing transaction: revert ', '') - assert.equal(e.reason, reason, 'Incorrect revert reason') - } - } -} - const deployedContract = async (receiptPromise, name) => artifacts.require(name).at(getLog(await receiptPromise, 'Deployed', 'addr')) diff --git a/test/helpers/assert-revert.js b/test/helpers/assert-revert.js new file mode 100644 index 00000000..b4cc9f2f --- /dev/null +++ b/test/helpers/assert-revert.js @@ -0,0 +1,13 @@ +module.exports = async (receiptPromise, reason) => { + try { + await receiptPromise + } catch (e) { + if (reason) { + e.reason = e.message.replace('VM Exception while processing transaction: revert ', '') + assert.equal(e.reason, reason, 'Incorrect revert reason') + } + return + } + + assert.fail(`Expected a revert for reason: ${reason}`) +} diff --git a/test/hex-tree.js b/test/hex-tree.js index 0508466e..14919a79 100644 --- a/test/hex-tree.js +++ b/test/hex-tree.js @@ -1,4 +1,4 @@ -const { assertRevert } = require('@aragon/test-helpers/assertThrow') +const assertRevert = require('./helpers/assert-revert') const HexSumTreePublic = artifacts.require('HexSumTreePublic') @@ -40,12 +40,12 @@ contract('Hex Sum Tree', (accounts) => { it('inserts and modifies', async () => { await tree.insert(10) await tree.insert(5) - assertBN(await tree.get(1, 15), 15, 'get sum') + assertBN(await tree.get(1, 0), 15, 'get sum') await tree.set(0, 5) assertBN(await tree.get(0, 0), 5, 'get node') - assertBN(await tree.get(1, 15), 10, 'get sum') + assertBN(await tree.get(1, 0), 10, 'get sum') }) it('inserts three', async () => { @@ -82,20 +82,14 @@ contract('Hex Sum Tree', (accounts) => { await tree.insert(5) await tree.insert(5) - await assertRevert(async () => { - // after 2 insertions (0, 1), next key is 2 - await tree.set(3, 5) - }) + await assertRevert(tree.set(3, 5), 'SUM_TREE_NEW_KEY_NOT_ADJACENT') }) it('fails inserting a number that makes sum overflow', async () => { await tree.insert(5) - await tree.insert(5) - await assertRevert(async () => { - const MAX_UINT256 = (new web3.BigNumber(2)).pow(256).minus(1) - await tree.insert(MAX_UINT256) - }) + const MAX_UINT256 = (new web3.BigNumber(2)).pow(256).minus(1) + await assertRevert(tree.insert(MAX_UINT256), 'SUM_TREE_UPDATE_OVERFLOW') }) it('sortition', async () => { From 9cf033468a83dd4be829d177f417e06f045a7484 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Fri, 1 Mar 2019 21:47:56 +0100 Subject: [PATCH 09/27] Juror drafts --- contracts/Court.sol | 22 ++++++++++++++++++++++ contracts/lib/HexSumTree.sol | 14 ++++++++++++++ test/court-lifecycle.js | 2 +- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index 8c52c48d..8baf8d84 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -295,6 +295,8 @@ contract Court is ERC900, ApproveAndCallFallBack { string internal constant ERROR_ROUND_ALREADY_DRAFTED = "COURT_ROUND_ALREADY_DRAFTED"; string internal constant ERROR_NOT_DRAFT_TERM = "COURT_NOT_DRAFT_TERM"; + event JurorDrafted(uint256 indexed disputeId, address indexed juror, uint256 draftId); + function draftAdjudicationRound(uint256 _disputeId) external ensureTerm @@ -302,6 +304,7 @@ contract Court is ERC900, ApproveAndCallFallBack { Dispute storage dispute = disputes[_disputeId]; uint256 roundId = dispute.rounds.length - 1; AdjudicationRound storage round = dispute.rounds[roundId]; + Term storage draftTerm = terms[term]; // TODO: Work on recovery if draft doesn't occur in the term it was supposed to // it should be scheduled for a future draft and require to pay the heartbeat fee for the term @@ -309,6 +312,25 @@ contract Court is ERC900, ApproveAndCallFallBack { require(dispute.state == DisputeState.PreDraft, ERROR_ROUND_ALREADY_DRAFTED); // TODO: actually draft jurors + if (draftTerm.randomness == 0) { + // the blockhash could be 0 if the first dispute draft happens 256 blocks after the term starts + draftTerm.randomness = uint256(block.blockhash(draftTerm.randomnessBN)); + } + + uint256[] memory jurorKeys = sumTree.randomSortition(round.jurorNumber, draftTerm.randomness); + assert(jurorKeys.length == round.jurorNumber); + + for (uint256 i = 0; i < jurorKeys.length; i++) { + address juror = jurorsByTreeId[jurorKeys[i]]; + + accounts[juror].pendingDisputes += 1; + + JurorVote memory vote; + vote.juror = juror; + round.votes.push(vote); + + emit JurorDrafted(_disputeId, juror, i); + } dispute.state = DisputeState.Adjudicating; diff --git a/contracts/lib/HexSumTree.sol b/contracts/lib/HexSumTree.sol index 303a55ae..ae7d7998 100644 --- a/contracts/lib/HexSumTree.sol +++ b/contracts/lib/HexSumTree.sol @@ -48,6 +48,20 @@ library HexSumTree { return _sortition(self, value, BASE_KEY, self.rootDepth); } + function randomSortition(Tree storage self, uint256 n, uint256 seed) internal view returns (uint256[] memory keys) { + keys = new uint256[](n); + + // cache in the stack. TODO: verify these don't go to memory + uint256 sum = totalSum(self); + uint256 rootDepth = self.rootDepth; + uint256 baseKey = BASE_KEY; + + for (uint256 i = 0; i < n; i++) { + uint256 random = sum % (seed * i); // intended overflow + keys[i] = _sortition(self, random, baseKey, rootDepth); + } + } + function _set(Tree storage self, uint256 key, uint256 value) private { uint256 oldValue = self.nodes[INSERTION_DEPTH][key]; self.nodes[INSERTION_DEPTH][key] = value; diff --git a/test/court-lifecycle.js b/test/court-lifecycle.js index 9dab1935..9cfe1f68 100644 --- a/test/court-lifecycle.js +++ b/test/court-lifecycle.js @@ -162,7 +162,7 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { assert.equal(await this.court.sortition(juror1Stake + juror2Stake / 2), juror2, 'sortition juror2') assert.equal(await this.court.sortition(juror1Stake + juror2Stake - 1), juror2, 'sortition juror2 end edge') - await assertRevert(this.court.sortition(juror1Stake + juror2Stake), 'SORTITION_OUT_OF_BOUNDS') + await assertRevert(this.court.sortition(juror1Stake + juror2Stake), 'SUM_TREE_SORTITION_OUT_OF_BOUNDS') await assertEqualBN(this.court.treeTotalSum(), juror1Stake + juror2Stake, 'both jurors in the tree') }) From 3ca94bb8fe6b1ce92d5874d879ed3041aacb0f18 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Sat, 2 Mar 2019 13:37:25 +0100 Subject: [PATCH 10/27] WIP: dispute voting --- contracts/Court.sol | 191 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 173 insertions(+), 18 deletions(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index 8baf8d84..e7c437c8 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -31,12 +31,14 @@ contract Court is ERC900, ApproveAndCallFallBack { uint256 sumTreeId; // key in the sum tree used for sortition } + // TODO: Rename to TermConfig struct FeeStructure { ERC20 feeToken; uint16 governanceShare; // ‱ of fees going to the governor (1/10,000) uint256 jurorFee; // per juror, total dispute fee = jurorFee * jurors drawn uint256 heartbeatFee; // per dispute, total heartbeat fee = heartbeatFee * disputes/appeals in term uint256 draftFee; // per dispute + // TODO: add commit/reveal/appeal durations } struct Term { @@ -57,16 +59,18 @@ contract Court is ERC900, ApproveAndCallFallBack { } struct AdjudicationRound { + JurorVote[] votes; + mapping (uint8 => uint256) rulingVotes; + uint8 winningRuling; uint64 draftTerm; uint64 jurorNumber; address triggeredBy; - JurorVote[] votes; } enum Ruling { Missing, RefusedRuling - // After this, it is specicific to the case + // ruling options are dispute specific } enum DisputeState { @@ -80,10 +84,16 @@ contract Court is ERC900, ApproveAndCallFallBack { struct Dispute { IArbitrable subject; + uint8 possibleRulings; // number of possible rulings the court can decide on DisputeState state; AdjudicationRound[] rounds; } + enum AdjudicationState { + Commit, + Reveal + } + // State constants which are set in the constructor and can't change ERC20 public jurorToken; uint64 public termDuration; // recomended value ~1 hour as 256 blocks (available block hash) around an hour to mine @@ -123,19 +133,41 @@ contract Court is ERC900, ApproveAndCallFallBack { string internal constant ERROR_GOVENANCE_FEE_TOO_HIGH = "COURT_GOVENANCE_FEE_TOO_HIGH"; string internal constant ERROR_ENTITY_CANT_DISMISS = "COURT_ENTITY_CANT_DISMISS"; string internal constant ERROR_CANT_DISMISS_AFTER_DRAFT = "COURT_CANT_DISMISS_AFTER_DRAFT"; + string internal constant ERROR_ROUND_ALREADY_DRAFTED = "COURT_ROUND_ALREADY_DRAFTED"; + string internal constant ERROR_NOT_DRAFT_TERM = "COURT_NOT_DRAFT_TERM"; + string internal constant ERROR_INVALID_DISPUTE_STATE = "COURT_INVALID_DISPUTE_STATE"; + string internal constant ERROR_INVALID_ADJUDICATION_ROUND = "COURT_INVALID_ADJUDICATION_ROUND"; + string internal constant ERROR_INVALID_ADJUDICATION_STATE = "COURT_INVALID_ADJUDICATION_STATE"; + string internal constant ERROR_INVALID_JUROR = "COURT_INVALID_JUROR"; + string internal constant ERROR_ALREADY_VOTED = "COURT_ALREADY_VOTED"; + string internal constant ERROR_INVALID_VOTE = "COURT_INVALID_VOTE"; + string internal constant ERROR_INVALID_RULING_OPTIONS = "COURT_INVALID_RULING_OPTIONS"; + uint64 internal constant ZERO_TERM = 0; // invalid term that doesn't accept disputes uint64 public constant MANUAL_DEACTIVATION = uint64(-1); uint64 internal constant MODIFIER_ALLOWED_TERM_TRANSITIONS = 1; bytes4 private constant ARBITRABLE_INTERFACE_ID = 0xabababab; // TODO: interface id uint16 internal constant GOVERNANCE_FEE_DIVISOR = 10000; // ‱ + uint8 public constant MIN_RULING_OPTIONS = 2; + uint8 public constant MAX_RULING_OPTIONS = 254; + + // TODO: Move into term configuration (currently fee schedule) + uint64 public constant COMMIT_TERMS = 72; + uint64 public constant REVEAL_TERMS = 24; event NewTerm(uint64 term, address indexed heartbeatSender); event NewFeeStructure(uint64 fromTerm, uint64 feeStructureId); event TokenBalanceChange(address indexed token, address indexed owner, uint256 amount, bool positive); event JurorActivate(address indexed juror, uint64 fromTerm, uint64 toTerm); event JurorDeactivate(address indexed juror, uint64 lastTerm); + event JurorDrafted(uint256 indexed disputeId, address indexed juror, uint256 draftId); + event DisputeStateChanged(uint256 indexed disputeId, DisputeState indexed state); + event NewDispute(uint256 indexed disputeId, address indexed subject, uint64 indexed draftTerm, uint64 jurorNumber); event TokenWithdrawal(address indexed token, address indexed account, uint256 amount); + event VoteCommitted(uint256 indexed disputeId, uint256 indexed roundId, address indexed juror, bytes32 commitment); + event VoteRevealed(uint256 indexed disputeId, uint256 indexed roundId, address indexed juror, uint8 ruling); + event VoteLeaked(uint256 indexed disputeId, uint256 indexed roundId, address indexed juror, address leaker); modifier only(address _addr) { require(msg.sender == _addr, ERROR_INVALID_ADDR); @@ -236,9 +268,7 @@ contract Court is ERC900, ApproveAndCallFallBack { } } - event NewDispute(uint256 indexed disputeId, address indexed subject, uint64 indexed draftTerm, uint64 jurorNumber); - - function createDispute(IArbitrable _subject, uint64 _jurorNumber, uint64 _draftTerm) + function createDispute(IArbitrable _subject, uint8 _possibleRulings, uint64 _jurorNumber, uint64 _draftTerm) external ensureTerm { @@ -247,8 +277,12 @@ contract Court is ERC900, ApproveAndCallFallBack { // TODO: Limit the max number of initial jurors // TODO: ERC165 check that _subject conforms to the interface + require(_possibleRulings >= MIN_RULING_OPTIONS && _possibleRulings <= MAX_RULING_OPTIONS, ERROR_INVALID_RULING_OPTIONS); + (ERC20 feeToken, uint256 feeAmount,) = feeForJurorDraft(_draftTerm, _jurorNumber); - require(feeToken.safeTransferFrom(msg.sender, this, feeAmount), ERROR_DEPOSIT_FAILED); + if (feeAmount > 0) { + require(feeToken.safeTransferFrom(msg.sender, this, feeAmount), ERROR_DEPOSIT_FAILED); + } uint256 disputeId = disputes.length; disputes.length = disputeId + 1; @@ -257,6 +291,7 @@ contract Court is ERC900, ApproveAndCallFallBack { dispute.subject = _subject; dispute.state = DisputeState.PreDraft; dispute.rounds.length = 1; + dispute.possibleRulings = _possibleRulings; AdjudicationRound storage round = dispute.rounds[0]; round.draftTerm = _draftTerm; @@ -268,8 +303,6 @@ contract Court is ERC900, ApproveAndCallFallBack { emit NewDispute(disputeId, _subject, _draftTerm, _jurorNumber); } - event DisputeStateChanged(uint256 indexed disputeId, DisputeState indexed state); - function dismissDispute(uint256 _disputeId) external ensureTerm @@ -292,13 +325,8 @@ contract Court is ERC900, ApproveAndCallFallBack { emit DisputeStateChanged(_disputeId, dispute.state); } - string internal constant ERROR_ROUND_ALREADY_DRAFTED = "COURT_ROUND_ALREADY_DRAFTED"; - string internal constant ERROR_NOT_DRAFT_TERM = "COURT_NOT_DRAFT_TERM"; - - event JurorDrafted(uint256 indexed disputeId, address indexed juror, uint256 draftId); - function draftAdjudicationRound(uint256 _disputeId) - external + public ensureTerm { Dispute storage dispute = disputes[_disputeId]; @@ -340,6 +368,133 @@ contract Court is ERC900, ApproveAndCallFallBack { emit DisputeStateChanged(_disputeId, dispute.state); } + function commitVote( + uint256 _disputeId, + uint256 _roundId, + uint256 _draftId, + bytes32 _commitment + ) + external + ensureTerm + ensureAdjudicationState(_disputeId, _roundId, AdjudicationState.Commit) + ensureDraft(_disputeId, _roundId, _draftId, msg.sender) + { + JurorVote storage vote = getJurorVote(_disputeId, _roundId, _draftId); + require(vote.commitment == bytes32(0) && vote.ruling == uint8(Ruling.Missing), ERROR_ALREADY_VOTED); + + vote.commitment = _commitment; + + emit VoteCommitted(_disputeId, _roundId, msg.sender, _commitment); + } + + function leakVote( + uint256 _disputeId, + uint256 _roundId, + uint256 _draftId, + address _juror, + uint8 _leakedRuling, + bytes32 _salt + ) + external + ensureTerm + ensureAdjudicationState(_disputeId, _roundId, AdjudicationState.Commit) + ensureDraft(_disputeId, _roundId, _draftId, _juror) + ensureNoReveal(_disputeId, _roundId, _draftId, _leakedRuling, _salt) + { + uint8 ruling = uint8(Ruling.RefusedRuling); + JurorVote storage vote = getJurorVote(_disputeId, _roundId, _draftId); + vote.ruling = ruling; + + // TODO: slash juror + + updateTally(_disputeId, _roundId, ruling); + + emit VoteLeaked(_disputeId, _roundId, _juror, msg.sender); + emit VoteRevealed(_disputeId, _roundId, _juror, ruling); + } + + function revealVote( + uint256 _disputeId, + uint256 _roundId, + uint256 _draftId, + uint8 _ruling, + bytes32 _salt + ) + external + ensureTerm + ensureAdjudicationState(_disputeId, _roundId, AdjudicationState.Reveal) + ensureDraft(_disputeId, _roundId, _draftId, msg.sender) + ensureNoReveal(_disputeId, _roundId, _draftId, _ruling, _salt) + { + Dispute storage dispute = disputes[_disputeId]; + JurorVote storage vote = getJurorVote(_disputeId, _roundId, _draftId); + + require(_ruling > uint8(Ruling.Missing) && _ruling <= dispute.possibleRulings + 1, ERROR_INVALID_VOTE); + + vote.ruling = _ruling; + updateTally(_disputeId, _roundId, _ruling); + + emit VoteRevealed(_disputeId, _roundId, msg.sender, _ruling); + } + + function updateTally(uint256 _disputeId, uint256 _roundId, uint8 _ruling) internal { + AdjudicationRound storage round = disputes[_disputeId].rounds[_roundId]; + + uint256 rulingVotes = round.rulingVotes[_ruling] + 1; + round.rulingVotes[_ruling] = rulingVotes; + + uint8 winningRuling = round.winningRuling; + uint256 winningSupport = round.rulingVotes[winningRuling]; + + // If it passes the currently winning option + // Or if there is a tie, the lowest ruling option is set as the winning ruling + if (rulingVotes > winningSupport || (rulingVotes == winningSupport && _ruling < winningRuling)) { + round.winningRuling = _ruling; + } + } + + modifier ensureNoReveal(uint256 _disputeId, uint256 _roundId, uint256 _draftId, uint8 _ruling, bytes32 _salt) { + JurorVote storage vote = getJurorVote(_disputeId, _roundId, _draftId); + bytes32 commit = encryptVote(_ruling, _salt); + require(vote.commitment == commit && vote.ruling == uint8(Ruling.Missing), ERROR_ALREADY_VOTED); + + _; + } + + modifier ensureAdjudicationState(uint256 _disputeId, uint256 _roundId, AdjudicationState _state) { + Dispute storage dispute = disputes[_disputeId]; + if (dispute.state == DisputeState.PreDraft) { + draftAdjudicationRound(_disputeId); + } + + require(dispute.state == DisputeState.Adjudicating, ERROR_INVALID_DISPUTE_STATE); + require(_roundId == dispute.rounds.length - 1, ERROR_INVALID_ADJUDICATION_ROUND); + + AdjudicationRound storage round = dispute.rounds[_roundId]; + + // fromTerm is inclusive, toTerm is exclusive + uint256 fromTerm = _state == AdjudicationState.Commit ? round.draftTerm : round.draftTerm + COMMIT_TERMS; + uint256 toTerm = fromTerm + (_state == AdjudicationState.Commit ? COMMIT_TERMS : REVEAL_TERMS); + + require(term >= fromTerm && term < toTerm, ERROR_INVALID_ADJUDICATION_STATE); + + _; + } + + modifier ensureDraft(uint256 _disputeId, uint256 _roundId, uint256 _draftId, address _juror) { + require(getJurorVote(_disputeId, _roundId, _draftId).juror == _juror, ERROR_INVALID_JUROR); + + _; + } + + function getJurorVote(uint256 _disputeId, uint256 _roundId, uint256 _draftId) internal view returns (JurorVote storage) { + return disputes[_disputeId].rounds[_roundId].votes[_draftId]; + } + + function encryptVote(uint8 _ruling, bytes32 _salt) public view returns (bytes32) { + return keccak256(abi.encodePacked(_ruling, _salt)); + } + /** * @dev Assumes term is up to date */ @@ -367,11 +522,11 @@ contract Court is ERC900, ApproveAndCallFallBack { } function _payFees(ERC20 _feeToken, address _to, uint256 _amount, uint16 _governanceShare) internal { - _assignTokens(_feeToken, governor, _amount * uint256(GOVERNANCE_FEE_DIVISOR - _governanceShare) / GOVERNANCE_FEE_DIVISOR); + if (_amount == 0) return; + _assignTokens(_feeToken, _to, _amount * uint256(GOVERNANCE_FEE_DIVISOR - _governanceShare) / GOVERNANCE_FEE_DIVISOR); - if (_governanceShare > 0) { - _assignTokens(_feeToken, governor, _amount * uint256(_governanceShare) / GOVERNANCE_FEE_DIVISOR); - } + if (_governanceShare == 0) return; + _assignTokens(_feeToken, governor, _amount * uint256(_governanceShare) / GOVERNANCE_FEE_DIVISOR); } // TODO: should we charge heartbeat fees to jurors? From e167cbc7a4f0157db14ef9251a70a6b831215281 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Mon, 4 Mar 2019 19:17:48 +0100 Subject: [PATCH 11/27] Fix stack too deep issues with voting --- contracts/Court.sol | 55 ++++++++++++++++++++++------------------- test/court-lifecycle.js | 14 +++++++++++ truffle-config.js | 13 +++++++++- 3 files changed, 56 insertions(+), 26 deletions(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index e7c437c8..3389a1b9 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -142,7 +142,7 @@ contract Court is ERC900, ApproveAndCallFallBack { string internal constant ERROR_ALREADY_VOTED = "COURT_ALREADY_VOTED"; string internal constant ERROR_INVALID_VOTE = "COURT_INVALID_VOTE"; string internal constant ERROR_INVALID_RULING_OPTIONS = "COURT_INVALID_RULING_OPTIONS"; - + string internal constant ERROR_FAILURE_COMMITMENT_CHECK = "COURT_FAILURE_COMMITMENT_CHECK"; uint64 internal constant ZERO_TERM = 0; // invalid term that doesn't accept disputes uint64 public constant MANUAL_DEACTIVATION = uint64(-1); @@ -185,6 +185,20 @@ contract Court is ERC900, ApproveAndCallFallBack { _; } + modifier ensureDrafted( + uint256 _disputeId, + uint256 _roundId, + uint256 _draftId, + address _juror, + AdjudicationState _state + ) { + checkDisputeState(_disputeId, _roundId); + checkAdjudicationState(_disputeId, _roundId, _state); + require(getJurorVote(_disputeId, _roundId, _draftId).juror == _juror, ERROR_INVALID_JUROR); + + _; + } + /** @dev Constructor. * @param _termDuration Duration in seconds per term (recommended 1 hour) * @param _jurorToken The address of the juror work token contract. @@ -339,7 +353,6 @@ contract Court is ERC900, ApproveAndCallFallBack { require(round.draftTerm == term, ERROR_NOT_DRAFT_TERM); require(dispute.state == DisputeState.PreDraft, ERROR_ROUND_ALREADY_DRAFTED); - // TODO: actually draft jurors if (draftTerm.randomness == 0) { // the blockhash could be 0 if the first dispute draft happens 256 blocks after the term starts draftTerm.randomness = uint256(block.blockhash(draftTerm.randomnessBN)); @@ -376,8 +389,7 @@ contract Court is ERC900, ApproveAndCallFallBack { ) external ensureTerm - ensureAdjudicationState(_disputeId, _roundId, AdjudicationState.Commit) - ensureDraft(_disputeId, _roundId, _draftId, msg.sender) + ensureDrafted(_disputeId, _roundId, _draftId, msg.sender, AdjudicationState.Commit) { JurorVote storage vote = getJurorVote(_disputeId, _roundId, _draftId); require(vote.commitment == bytes32(0) && vote.ruling == uint8(Ruling.Missing), ERROR_ALREADY_VOTED); @@ -397,10 +409,10 @@ contract Court is ERC900, ApproveAndCallFallBack { ) external ensureTerm - ensureAdjudicationState(_disputeId, _roundId, AdjudicationState.Commit) - ensureDraft(_disputeId, _roundId, _draftId, _juror) - ensureNoReveal(_disputeId, _roundId, _draftId, _leakedRuling, _salt) + ensureDrafted(_disputeId, _roundId, _draftId, _juror, AdjudicationState.Commit) { + checkVote(_disputeId, _roundId, _draftId, _leakedRuling, _salt); + uint8 ruling = uint8(Ruling.RefusedRuling); JurorVote storage vote = getJurorVote(_disputeId, _roundId, _draftId); vote.ruling = ruling; @@ -422,10 +434,10 @@ contract Court is ERC900, ApproveAndCallFallBack { ) external ensureTerm - ensureAdjudicationState(_disputeId, _roundId, AdjudicationState.Reveal) - ensureDraft(_disputeId, _roundId, _draftId, msg.sender) - ensureNoReveal(_disputeId, _roundId, _draftId, _ruling, _salt) + ensureDrafted(_disputeId, _roundId, _draftId, msg.sender, AdjudicationState.Reveal) { + checkVote(_disputeId, _roundId, _draftId, _ruling, _salt); + Dispute storage dispute = disputes[_disputeId]; JurorVote storage vote = getJurorVote(_disputeId, _roundId, _draftId); @@ -453,15 +465,14 @@ contract Court is ERC900, ApproveAndCallFallBack { } } - modifier ensureNoReveal(uint256 _disputeId, uint256 _roundId, uint256 _draftId, uint8 _ruling, bytes32 _salt) { - JurorVote storage vote = getJurorVote(_disputeId, _roundId, _draftId); - bytes32 commit = encryptVote(_ruling, _salt); - require(vote.commitment == commit && vote.ruling == uint8(Ruling.Missing), ERROR_ALREADY_VOTED); + function checkVote(uint256 _disputeId, uint256 _roundId, uint256 _draftId, uint8 _ruling, bytes32 _salt) internal { + JurorVote storage jurorVote = getJurorVote(_disputeId, _roundId, _draftId); - _; + require(jurorVote.commitment == encryptVote(_ruling, _salt), ERROR_FAILURE_COMMITMENT_CHECK); + require(jurorVote.ruling == uint8(Ruling.Missing), ERROR_ALREADY_VOTED); } - modifier ensureAdjudicationState(uint256 _disputeId, uint256 _roundId, AdjudicationState _state) { + function checkDisputeState(uint256 _disputeId, uint256 _roundId) internal { Dispute storage dispute = disputes[_disputeId]; if (dispute.state == DisputeState.PreDraft) { draftAdjudicationRound(_disputeId); @@ -469,22 +480,16 @@ contract Court is ERC900, ApproveAndCallFallBack { require(dispute.state == DisputeState.Adjudicating, ERROR_INVALID_DISPUTE_STATE); require(_roundId == dispute.rounds.length - 1, ERROR_INVALID_ADJUDICATION_ROUND); + } - AdjudicationRound storage round = dispute.rounds[_roundId]; + function checkAdjudicationState(uint256 _disputeId, uint256 _roundId, AdjudicationState _state) internal { + AdjudicationRound storage round = disputes[_disputeId].rounds[_roundId]; // fromTerm is inclusive, toTerm is exclusive uint256 fromTerm = _state == AdjudicationState.Commit ? round.draftTerm : round.draftTerm + COMMIT_TERMS; uint256 toTerm = fromTerm + (_state == AdjudicationState.Commit ? COMMIT_TERMS : REVEAL_TERMS); require(term >= fromTerm && term < toTerm, ERROR_INVALID_ADJUDICATION_STATE); - - _; - } - - modifier ensureDraft(uint256 _disputeId, uint256 _roundId, uint256 _draftId, address _juror) { - require(getJurorVote(_disputeId, _roundId, _draftId).juror == _juror, ERROR_INVALID_JUROR); - - _; } function getJurorVote(uint256 _disputeId, uint256 _roundId, uint256 _draftId) internal view returns (JurorVote storage) { diff --git a/test/court-lifecycle.js b/test/court-lifecycle.js index 9cfe1f68..9b82f132 100644 --- a/test/court-lifecycle.js +++ b/test/court-lifecycle.js @@ -1,9 +1,11 @@ const assertRevert = require('./helpers/assert-revert') +const { promisify } = require('util') const TokenFactory = artifacts.require('TokenFactory') const CourtMock = artifacts.require('CourtMock') const MINIME = 'MiniMeToken' +const BLOCK_GAS_LIMIT = 8e6 const getLog = (receipt, logName, argName) => { const log = receipt.logs.find(({ event }) => event == logName) @@ -81,6 +83,18 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { await assertEqualBN(this.court.totalStakedFor(juror2), juror2Stake, 'juror2 stake') }) + it('can be deployed under the block gas limit', async () => { + const getReceipt = tx => + new Promise((res, rej) => + web3.eth.getTransactionReceipt(tx, (e, rec) => { + if (e) rej(e) + res(rec) + })) + + const { gasUsed } = await getReceipt(this.court.transactionHash) + assert.isBelow(gasUsed, BLOCK_GAS_LIMIT, 'should be deployable to under the gas limit') + }) + context('before first term', () => { it('it in term #0', async () => { await assertEqualBN(this.court.term(), 0, 'court term #0') diff --git a/truffle-config.js b/truffle-config.js index 466a16a3..c107e6cc 100644 --- a/truffle-config.js +++ b/truffle-config.js @@ -1 +1,12 @@ -module.exports = require("@aragon/os/truffle-config") +const aOSConfig = require("@aragon/os/truffle-config") + +module.exports = { + ...aOSConfig, + + solc: { + optimizer: { + enabled: true, + runs: 1000 // could be increased depending on the final size of Court.sol + }, + }, +} From 6d0b322becfb5ff528437c14087ebd07cc918520 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Mon, 18 Mar 2019 09:59:31 +0100 Subject: [PATCH 12/27] Clean up --- contracts/Court.sol | 38 ++++++++++++++++++++++++------------ contracts/lib/HexSumTree.sol | 2 +- contracts/test/CourtMock.sol | 4 ++++ 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index 3389a1b9..c6e14051 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -46,7 +46,7 @@ contract Court is ERC900, ApproveAndCallFallBack { uint64 dependingDrafts; // disputes or appeals pegged to this term for randomness uint64 feeStructureId; // fee structure for this term (index in feeStructures array) uint64 randomnessBN; // block number for entropy - uint256 randomness; // entropy from randomnessBN block hash + bytes32 randomness; // entropy from randomnessBN block hash address[] ingressQueue; // jurors that will be added to the juror tree address[] egressQueue; // jurors that will be removed from to the juror tree address[] updatesQueue; // jurors whose stake has been updated @@ -194,7 +194,7 @@ contract Court is ERC900, ApproveAndCallFallBack { ) { checkDisputeState(_disputeId, _roundId); checkAdjudicationState(_disputeId, _roundId, _state); - require(getJurorVote(_disputeId, _roundId, _draftId).juror == _juror, ERROR_INVALID_JUROR); + require(_getJurorVote(_disputeId, _roundId, _draftId).juror == _juror, ERROR_INVALID_JUROR); _; } @@ -284,7 +284,8 @@ contract Court is ERC900, ApproveAndCallFallBack { function createDispute(IArbitrable _subject, uint8 _possibleRulings, uint64 _jurorNumber, uint64 _draftTerm) external - ensureTerm + 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 @@ -315,6 +316,8 @@ contract Court is ERC900, ApproveAndCallFallBack { terms[_draftTerm].dependingDrafts += 1; emit NewDispute(disputeId, _subject, _draftTerm, _jurorNumber); + + return disputeId; } function dismissDispute(uint256 _disputeId) @@ -353,13 +356,13 @@ contract Court is ERC900, ApproveAndCallFallBack { require(round.draftTerm == term, ERROR_NOT_DRAFT_TERM); require(dispute.state == DisputeState.PreDraft, ERROR_ROUND_ALREADY_DRAFTED); - if (draftTerm.randomness == 0) { + if (draftTerm.randomness == bytes32(0)) { // the blockhash could be 0 if the first dispute draft happens 256 blocks after the term starts - draftTerm.randomness = uint256(block.blockhash(draftTerm.randomnessBN)); + draftTerm.randomness = block.blockhash(draftTerm.randomnessBN); } - uint256[] memory jurorKeys = sumTree.randomSortition(round.jurorNumber, draftTerm.randomness); - assert(jurorKeys.length == round.jurorNumber); + uint256[] memory jurorKeys = treeRandomSearch(round.jurorNumber, draftTerm.randomness, _disputeId); + require(jurorKeys.length == round.jurorNumber, "expected more jurors yo"); for (uint256 i = 0; i < jurorKeys.length; i++) { address juror = jurorsByTreeId[jurorKeys[i]]; @@ -391,7 +394,7 @@ contract Court is ERC900, ApproveAndCallFallBack { ensureTerm ensureDrafted(_disputeId, _roundId, _draftId, msg.sender, AdjudicationState.Commit) { - JurorVote storage vote = getJurorVote(_disputeId, _roundId, _draftId); + JurorVote storage vote = _getJurorVote(_disputeId, _roundId, _draftId); require(vote.commitment == bytes32(0) && vote.ruling == uint8(Ruling.Missing), ERROR_ALREADY_VOTED); vote.commitment = _commitment; @@ -414,7 +417,7 @@ contract Court is ERC900, ApproveAndCallFallBack { checkVote(_disputeId, _roundId, _draftId, _leakedRuling, _salt); uint8 ruling = uint8(Ruling.RefusedRuling); - JurorVote storage vote = getJurorVote(_disputeId, _roundId, _draftId); + JurorVote storage vote = _getJurorVote(_disputeId, _roundId, _draftId); vote.ruling = ruling; // TODO: slash juror @@ -439,7 +442,7 @@ contract Court is ERC900, ApproveAndCallFallBack { checkVote(_disputeId, _roundId, _draftId, _ruling, _salt); Dispute storage dispute = disputes[_disputeId]; - JurorVote storage vote = getJurorVote(_disputeId, _roundId, _draftId); + JurorVote storage vote = _getJurorVote(_disputeId, _roundId, _draftId); require(_ruling > uint8(Ruling.Missing) && _ruling <= dispute.possibleRulings + 1, ERROR_INVALID_VOTE); @@ -466,7 +469,7 @@ contract Court is ERC900, ApproveAndCallFallBack { } function checkVote(uint256 _disputeId, uint256 _roundId, uint256 _draftId, uint8 _ruling, bytes32 _salt) internal { - JurorVote storage jurorVote = getJurorVote(_disputeId, _roundId, _draftId); + JurorVote storage jurorVote = _getJurorVote(_disputeId, _roundId, _draftId); require(jurorVote.commitment == encryptVote(_ruling, _salt), ERROR_FAILURE_COMMITMENT_CHECK); require(jurorVote.ruling == uint8(Ruling.Missing), ERROR_ALREADY_VOTED); @@ -492,7 +495,13 @@ contract Court is ERC900, ApproveAndCallFallBack { require(term >= fromTerm && term < toTerm, ERROR_INVALID_ADJUDICATION_STATE); } - function getJurorVote(uint256 _disputeId, uint256 _roundId, uint256 _draftId) internal view returns (JurorVote storage) { + function getJurorVote(uint256 _disputeId, uint256 _roundId, uint256 _draftId) public view returns (address juror, uint8 ruling) { + JurorVote storage jurorVote = _getJurorVote(_disputeId, _roundId, _draftId); + + return (jurorVote.juror, jurorVote.ruling); + } + + function _getJurorVote(uint256 _disputeId, uint256 _roundId, uint256 _draftId) internal view returns (JurorVote storage) { return disputes[_disputeId].rounds[_roundId].votes[_draftId]; } @@ -500,6 +509,11 @@ contract Court is ERC900, ApproveAndCallFallBack { return keccak256(abi.encodePacked(_ruling, _salt)); } + function treeRandomSearch(uint64 _jurorNumber, bytes32 _termRandomness, uint256 _disputeId) internal returns (uint256[] memory) { + bytes32 seed = keccak256(abi.encodePacked(_termRandomness, _disputeId)); + return sumTree.randomSortition(_jurorNumber, uint256(seed)); + } + /** * @dev Assumes term is up to date */ diff --git a/contracts/lib/HexSumTree.sol b/contracts/lib/HexSumTree.sol index ae7d7998..61af37e0 100644 --- a/contracts/lib/HexSumTree.sol +++ b/contracts/lib/HexSumTree.sol @@ -57,7 +57,7 @@ library HexSumTree { uint256 baseKey = BASE_KEY; for (uint256 i = 0; i < n; i++) { - uint256 random = sum % (seed * i); // intended overflow + uint256 random = (seed * i) % sum; // intended overflow keys[i] = _sortition(self, random, baseKey, rootDepth); } } diff --git a/contracts/test/CourtMock.sol b/contracts/test/CourtMock.sol index f218606a..8ae57d01 100644 --- a/contracts/test/CourtMock.sol +++ b/contracts/test/CourtMock.sol @@ -45,6 +45,10 @@ contract CourtMock is Court { mockBn = bn; } + function mock_blockTravel(uint64 inc) external { + mockBn += inc; + } + function time() internal view returns (uint64) { return mockTime; } From fe1abf9b1d3b7b8fec8193ae3e0a4f90a9367f37 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Mon, 18 Mar 2019 12:41:09 +0100 Subject: [PATCH 13/27] Rename for clarity --- contracts/Court.sol | 19 +++++++++++++------ contracts/lib/HexSumTree.sol | 24 ++++++++++++++++++++---- contracts/test/CourtMock.sol | 4 ++-- test/court-lifecycle.js | 4 ++-- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index c6e14051..93cd2765 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -101,7 +101,7 @@ contract Court is ERC900, ApproveAndCallFallBack { // Global config, configurable by governor address public governor; // TODO: consider using aOS' ACL uint64 public jurorCooldownTerms; - uint256 public jurorActivationDust; + uint256 public jurorMinStake; uint256 public maxAppeals = 5; FeeStructure[] public feeStructures; @@ -124,7 +124,7 @@ contract Court is ERC900, ApproveAndCallFallBack { 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"; - string internal constant ERROR_TOKENS_BELOW_DUST = "COURT_TOKENS_BELOW_DUST"; + string internal constant ERROR_TOKENS_BELOW_MIN_STAKE = "COURT_TOKENS_BELOW_MIN_STAKE"; string internal constant ERROR_INVALID_ACTIVATION_TERM = "COURT_INVALID_ACTIVATION_TERM"; string internal constant ERROR_INVALID_DEACTIVATION_TERM = "COURT_INVALID_DEACTIVATION_TERM"; string internal constant ERROR_JUROR_TOKENS_AT_STAKE = "COURT_JUROR_TOKENS_AT_STAKE"; @@ -209,7 +209,7 @@ contract Court is ERC900, ApproveAndCallFallBack { * @param _governanceShare Share in ‱ of fees that are paid to the governor. * @param _governor Address of the governor contract. * @param _firstTermStartTime Timestamp in seconds when the court will open (to give time for juror onboarding) - * @param _jurorActivationDust Minimum amount of juror tokens that can be activated + * @param _jurorMinStake Minimum amount of juror tokens that can be activated * @param _jurorCooldownTerms Number of terms before a juror tokens can be withdrawn after deactivation () */ constructor( @@ -222,12 +222,12 @@ contract Court is ERC900, ApproveAndCallFallBack { uint16 _governanceShare, address _governor, uint64 _firstTermStartTime, - uint256 _jurorActivationDust, + uint256 _jurorMinStake, uint64 _jurorCooldownTerms ) public { termDuration = _termDuration; jurorToken = _jurorToken; - jurorActivationDust = _jurorActivationDust; + jurorMinStake = _jurorMinStake; governor = _governor; jurorCooldownTerms = _jurorCooldownTerms; @@ -640,7 +640,10 @@ contract Court is ERC900, ApproveAndCallFallBack { } function totalStakedFor(address _addr) public view returns (uint256) { - return accounts[_addr].balances[jurorToken]; + uint256 sumTreeId = accounts[_addr].sumTreeId; + uint256 activeTokens = sumTreeId > 0 ? tree.getItem(sumTreeId) : 0; + + return accounts[_addr].balances[jurorToken] + activeTokens; } function totalStaked() external view returns (uint256) { @@ -741,6 +744,10 @@ contract Court is ERC900, ApproveAndCallFallBack { jurorsByTreeId[sumTreeId] = _jurorAddress; } + function _applyTreeUpdate(UpdateQueueItem memory updateItem) internal { + sumTree.update(updateItem.sumTreeId, updateItem.delta, updateItem.positive); + } + function _stake(address _from, address _to, uint256 _amount) internal { require(_amount > 0, ERROR_ZERO_TRANSFER); require(jurorToken.safeTransferFrom(_from, this, _amount), ERROR_DEPOSIT_FAILED); diff --git a/contracts/lib/HexSumTree.sol b/contracts/lib/HexSumTree.sol index 61af37e0..81e07db3 100644 --- a/contracts/lib/HexSumTree.sol +++ b/contracts/lib/HexSumTree.sol @@ -22,6 +22,7 @@ library HexSumTree { string private constant ERROR_SORTITION_OUT_OF_BOUNDS = "SUM_TREE_SORTITION_OUT_OF_BOUNDS"; string private constant ERROR_NEW_KEY_NOT_ADJACENT = "SUM_TREE_NEW_KEY_NOT_ADJACENT"; string private constant ERROR_UPDATE_OVERFLOW = "SUM_TREE_UPDATE_OVERFLOW"; + string private constant ERROR_INEXISTENT_ITEM = "SUM_TREE_INEXISTENT_ITEM"; function init(Tree storage self) internal { self.rootDepth = INSERTION_DEPTH + 1; @@ -32,7 +33,9 @@ library HexSumTree { uint256 key = self.nextKey; self.nextKey = nextKey(key); - _set(self, key, value); + if (value > 0) { + _set(self, key, value); + } return key; } @@ -42,6 +45,15 @@ library HexSumTree { _set(self, key, value); } + function update(Tree storage self, uint256 key, uint256 delta, bool positive) internal { + require(key < self.nextKey, ERROR_INEXISTENT_ITEM); + + uint256 oldValue = self.nodes[INSERTION_DEPTH][key]; + self.nodes[INSERTION_DEPTH][key] = positive ? oldValue + delta : oldValue - delta; + + updateSums(self, key, delta, positive); + } + function sortition(Tree storage self, uint256 value) internal view returns (uint256 key) { require(totalSum(self) > value, ERROR_SORTITION_OUT_OF_BOUNDS); @@ -112,7 +124,7 @@ library HexSumTree { // Invariant: this point should never be reached } - function updateSums(Tree storage self, uint256 key, uint256 delta, bool sum) private { + function updateSums(Tree storage self, uint256 key, uint256 delta, bool positive) private { uint256 newRootDepth = sharedPrefix(self.rootDepth, key); if (self.rootDepth != newRootDepth) { @@ -127,10 +139,10 @@ library HexSumTree { ancestorKey = ancestorKey & mask; // Invariant: this will never underflow. - self.nodes[i][ancestorKey] = sum ? self.nodes[i][ancestorKey] + delta : self.nodes[i][ancestorKey] - delta; + self.nodes[i][ancestorKey] = positive ? self.nodes[i][ancestorKey] + delta : self.nodes[i][ancestorKey] - delta; } // it's only needed to check the last one, as the sum increases going up through the tree - require(!sum || self.nodes[self.rootDepth][ancestorKey] >= delta, ERROR_UPDATE_OVERFLOW); + require(!positive || self.nodes[self.rootDepth][ancestorKey] >= delta, ERROR_UPDATE_OVERFLOW); } function totalSum(Tree storage self) internal view returns (uint256) { @@ -141,6 +153,10 @@ library HexSumTree { return self.nodes[depth][key]; } + function getItem(Tree storage self, uint256 key) internal view returns (uint256) { + return self.nodes[INSERTION_DEPTH][key]; + } + function nextKey(uint256 fromKey) private pure returns (uint256) { return fromKey + 1; } diff --git a/contracts/test/CourtMock.sol b/contracts/test/CourtMock.sol index 8ae57d01..a9501eed 100644 --- a/contracts/test/CourtMock.sol +++ b/contracts/test/CourtMock.sol @@ -17,7 +17,7 @@ contract CourtMock is Court { uint16 _governanceShare, address _governor, uint64 _firstTermStartTime, - uint256 _jurorActivationDust, + uint256 _jurorMinStake, uint64 _jurorCooldownTerms ) Court( _termDuration, @@ -29,7 +29,7 @@ contract CourtMock is Court { _governanceShare, _governor, _firstTermStartTime, - _jurorActivationDust, + _jurorMinStake, _jurorCooldownTerms ) public {} diff --git a/test/court-lifecycle.js b/test/court-lifecycle.js index 9b82f132..45c26693 100644 --- a/test/court-lifecycle.js +++ b/test/court-lifecycle.js @@ -31,7 +31,7 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { const termDuration = 10 const firstTermStart = 5 - const jurorActivationDust = 100 + const jurorMinStake = 100 const cooldown = 10 const startBlock = 1000 @@ -63,7 +63,7 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { 0, governor, firstTermStart, - jurorActivationDust, + jurorMinStake, cooldown ) await this.court.mock_setBlockNumber(startBlock) From dd687db3fba11c3d1b05b8846bcef1796a588ee0 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Mon, 18 Mar 2019 13:31:14 +0100 Subject: [PATCH 14/27] Remove ingress queue in favor of updates --- contracts/Court.sol | 79 +++++++++++++++++++++++------------------ test/court-lifecycle.js | 2 +- 2 files changed, 46 insertions(+), 35 deletions(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index 93cd2765..fdeb3c2a 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -22,13 +22,19 @@ contract Court is ERC900, ApproveAndCallFallBack { PastJuror } + struct AccountUpdate { + bool positive; // TODO: optimize gas + uint256 delta; + } + struct Account { mapping (address => uint256) balances; // token addr -> balance AccountState state; // whether the account is not a juror, a current juror or a past juror uint64 fromTerm; // first term in which the juror can be drawn uint64 toTerm; // last term in which the juror can be drawn - uint64 pendingDisputes; // disputes in which the juror was drawn which haven't resolved + uint256 tokensAtStake; // disputes in which the juror was drawn which haven't resolved uint256 sumTreeId; // key in the sum tree used for sortition + AccountUpdate update; // next account update } // TODO: Rename to TermConfig @@ -47,9 +53,8 @@ contract Court is ERC900, ApproveAndCallFallBack { uint64 feeStructureId; // fee structure for this term (index in feeStructures array) uint64 randomnessBN; // block number for entropy bytes32 randomness; // entropy from randomnessBN block hash - address[] ingressQueue; // jurors that will be added to the juror tree + address[] updateQueue; // jurors whose stake needs to be updated address[] egressQueue; // jurors that will be removed from to the juror tree - address[] updatesQueue; // jurors whose stake has been updated } struct JurorVote { @@ -102,6 +107,7 @@ contract Court is ERC900, ApproveAndCallFallBack { address public governor; // TODO: consider using aOS' ACL uint64 public jurorCooldownTerms; uint256 public jurorMinStake; + uint256 public penalization; // ‱ of jurorMinStake that can be slashed (1/10,000) uint256 public maxAppeals = 5; FeeStructure[] public feeStructures; @@ -552,18 +558,27 @@ contract Court is ERC900, ApproveAndCallFallBack { function activate(uint64 _fromTerm, uint64 _toTerm) external ensureTerm { address jurorAddress = msg.sender; Account storage account = accounts[jurorAddress]; + uint256 balance = account.balances[jurorToken]; require(_fromTerm > term, ERROR_INVALID_ACTIVATION_TERM); require(_toTerm > _fromTerm, ERROR_INVALID_DEACTIVATION_TERM); require(account.state == AccountState.NotJuror, ERROR_INVALID_ACCOUNT_STATE); - require(account.balances[jurorToken] >= jurorActivationDust, ERROR_TOKENS_BELOW_DUST); + require(balance >= jurorMinStake, ERROR_TOKENS_BELOW_MIN_STAKE); + + uint256 sumTreeId = account.sumTreeId; + if (sumTreeId == 0) { + sumTreeId = sumTree.insert(0); + accounts[jurorAddress].sumTreeId = sumTreeId; + jurorsByTreeId[sumTreeId] = jurorAddress; + } if (term == ZERO_TERM && _fromTerm == ZERO_TERM + 1) { // allow direct juror onboardings before term 1 starts (no disputes depend on term 0) - _insertJurorToSumTree(jurorAddress); + sumTree.update(sumTreeId, balance, true); } else { // TODO: check queue size limit - terms[_fromTerm].ingressQueue.push(jurorAddress); + account.update = AccountUpdate({ delta: balance, positive: true }); + terms[_fromTerm].updateQueue.push(jurorAddress); } if (_toTerm != MANUAL_DEACTIVATION) { @@ -574,6 +589,7 @@ contract Court is ERC900, ApproveAndCallFallBack { account.fromTerm = _fromTerm; account.toTerm = _toTerm; account.state = AccountState.Juror; + account.balances[jurorToken] = 0; // tokens are either pending the update or in the tree emit JurorActivate(jurorAddress, _fromTerm, _toTerm); } @@ -591,7 +607,10 @@ contract Court is ERC900, ApproveAndCallFallBack { // Juror didn't actually become activated if (term < account.fromTerm && term != ZERO_TERM) { - terms[account.fromTerm].ingressQueue.deleteItem(jurorAddress); + terms[account.fromTerm].updateQueue.deleteItem(jurorAddress); + assert(account.update.positive); // If the juror didn't activate, its update can only be positive + account.balances[jurorToken] += account.update.delta; + delete account.update; } if (account.toTerm != MANUAL_DEACTIVATION) { @@ -641,9 +660,11 @@ contract Court is ERC900, ApproveAndCallFallBack { function totalStakedFor(address _addr) public view returns (uint256) { uint256 sumTreeId = accounts[_addr].sumTreeId; - uint256 activeTokens = sumTreeId > 0 ? tree.getItem(sumTreeId) : 0; + uint256 activeTokens = sumTreeId > 0 ? sumTree.getItem(sumTreeId) : 0; + AccountUpdate storage update = accounts[_addr].update; + uint256 pendingTokens = update.positive ? update.delta : -update.delta; - return accounts[_addr].balances[jurorToken] + activeTokens; + return accounts[_addr].balances[jurorToken] + activeTokens + pendingTokens; } function totalStaked() external view returns (uint256) { @@ -706,48 +727,38 @@ contract Court is ERC900, ApproveAndCallFallBack { } function _processJurorQueues(Term storage _incomingTerm) internal { - uint256 ingressLength = _incomingTerm.ingressQueue.length; uint256 egressLength = _incomingTerm.egressQueue.length; - uint256 updatesLength = _incomingTerm.updatesQueue.length; + uint256 updatesLength = _incomingTerm.updateQueue.length; + + for (uint256 i = 0; i < updatesLength; i++) { + address jurorUpdate = _incomingTerm.updateQueue[i]; + AccountUpdate storage update = accounts[jurorUpdate].update; - // Insert cost = 40k + tree insertion - for (uint256 i = 0; i < ingressLength; i++) { - _insertJurorToSumTree(_incomingTerm.ingressQueue[i]); + if (update.delta > 0) { + sumTree.update(accounts[jurorUpdate].sumTreeId, update.delta, update.positive); + delete accounts[jurorUpdate].update; + } } for (uint256 j = 0; j < egressLength; j++) { address jurorEgress = _incomingTerm.egressQueue[j]; - if (accounts[jurorEgress].sumTreeId != 0) { - sumTree.set(accounts[jurorEgress].sumTreeId, 0); + uint256 sumTreeId = accounts[jurorEgress].sumTreeId; + if (sumTreeId != 0) { + uint256 treeBalance = sumTree.getItem(sumTreeId); + accounts[jurorEgress].balances[jurorToken] += treeBalance; + sumTree.set(sumTreeId, 0); delete accounts[jurorEgress].sumTreeId; } } - for (uint256 k = 0; k < updatesLength; k++) { - address jurorUpdate = _incomingTerm.updatesQueue[k]; - sumTree.set(accounts[jurorUpdate].sumTreeId, totalStakedFor(jurorUpdate)); - } - if (ingressLength > 0) { - delete _incomingTerm.ingressQueue; - } if (egressLength > 0) { delete _incomingTerm.egressQueue; } if (updatesLength > 0) { - delete _incomingTerm.updatesQueue; + delete _incomingTerm.updateQueue; } } - function _insertJurorToSumTree(address _jurorAddress) internal { - uint256 sumTreeId = sumTree.insert(totalStakedFor(_jurorAddress)); - accounts[_jurorAddress].sumTreeId = sumTreeId; - jurorsByTreeId[sumTreeId] = _jurorAddress; - } - - function _applyTreeUpdate(UpdateQueueItem memory updateItem) internal { - sumTree.update(updateItem.sumTreeId, updateItem.delta, updateItem.positive); - } - function _stake(address _from, address _to, uint256 _amount) internal { require(_amount > 0, ERROR_ZERO_TRANSFER); require(jurorToken.safeTransferFrom(_from, this, _amount), ERROR_DEPOSIT_FAILED); diff --git a/test/court-lifecycle.js b/test/court-lifecycle.js index 45c26693..a82145bc 100644 --- a/test/court-lifecycle.js +++ b/test/court-lifecycle.js @@ -127,7 +127,7 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { it('reverts if activating balance is below dust', async () => { await this.court.mock_setTime(firstTermStart - 1) - await assertRevert(this.court.activate(1, 10, { from: poor }), 'COURT_TOKENS_BELOW_DUST') + await assertRevert(this.court.activate(1, 10, { from: poor }), 'COURT_TOKENS_BELOW_MIN_STAKE') }) }) From 1f6adaedc395f6496e4d601c5dcf387877d81495 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Tue, 19 Mar 2019 21:21:04 +0100 Subject: [PATCH 15/27] Implement CourtConfig and track juror amount of tokens at stake --- contracts/Court.sol | 232 ++++++++++++++++------------ contracts/lib/HexSumTree.sol | 20 +-- contracts/test/CourtMock.sol | 19 ++- contracts/test/HexSumTreePublic.sol | 3 +- contracts/test/TestFactory.sol | 5 +- test/court-lifecycle.js | 22 ++- 6 files changed, 173 insertions(+), 128 deletions(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index fdeb3c2a..5cc72c81 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -33,24 +33,29 @@ contract Court is ERC900, ApproveAndCallFallBack { uint64 fromTerm; // first term in which the juror can be drawn uint64 toTerm; // last term in which the juror can be drawn uint256 tokensAtStake; // disputes in which the juror was drawn which haven't resolved + uint64 pendingDisputes; // TODO: remove uint256 sumTreeId; // key in the sum tree used for sortition AccountUpdate update; // next account update } - // TODO: Rename to TermConfig - struct FeeStructure { + struct CourtConfig { + // Fee structure ERC20 feeToken; - uint16 governanceShare; // ‱ of fees going to the governor (1/10,000) - uint256 jurorFee; // per juror, total dispute fee = jurorFee * jurors drawn - uint256 heartbeatFee; // per dispute, total heartbeat fee = heartbeatFee * disputes/appeals in term - uint256 draftFee; // per dispute - // TODO: add commit/reveal/appeal durations + uint16 governanceFeeShare; // ‱ of fees going to the governor (1/10,000) + uint256 jurorFee; // per juror, total dispute fee = jurorFee * jurors drawn + uint256 heartbeatFee; // per dispute, total heartbeat fee = heartbeatFee * disputes/appeals in term + uint256 draftFee; // per dispute + // Dispute config + uint64 commitTerms; + uint64 revealTerms; + uint64 appealTerms; + uint16 penaltyPct; } struct Term { uint64 startTime; // timestamp when the term started - uint64 dependingDrafts; // disputes or appeals pegged to this term for randomness - uint64 feeStructureId; // fee structure for this term (index in feeStructures array) + uint64 dependingDrafts; // disputes or appeals pegged to this term for randomness + uint64 courtConfigId; // fee structure for this term (index in courtConfigs array) uint64 randomnessBN; // block number for entropy bytes32 randomness; // entropy from randomnessBN block hash address[] updateQueue; // jurors whose stake needs to be updated @@ -105,15 +110,13 @@ contract Court is ERC900, ApproveAndCallFallBack { // Global config, configurable by governor address public governor; // TODO: consider using aOS' ACL - uint64 public jurorCooldownTerms; - uint256 public jurorMinStake; - uint256 public penalization; // ‱ of jurorMinStake that can be slashed (1/10,000) + uint256 public jurorMinStake; // TODO: consider adding it to the conf uint256 public maxAppeals = 5; - FeeStructure[] public feeStructures; + CourtConfig[] public courtConfigs; // Court state uint64 public term; - uint64 public feeChangeTerm; + uint64 public configChangeTerm; mapping (address => Account) public accounts; mapping (uint256 => address) public jurorsByTreeId; mapping (uint64 => Term) public terms; @@ -149,21 +152,18 @@ contract Court is ERC900, ApproveAndCallFallBack { string internal constant ERROR_INVALID_VOTE = "COURT_INVALID_VOTE"; string internal constant ERROR_INVALID_RULING_OPTIONS = "COURT_INVALID_RULING_OPTIONS"; string internal constant ERROR_FAILURE_COMMITMENT_CHECK = "COURT_FAILURE_COMMITMENT_CHECK"; + string internal constant ERROR_CONFIG_PERIOD_ZERO_TERMS = "COURT_CONFIG_PERIOD_ZERO_TERMS"; uint64 internal constant ZERO_TERM = 0; // invalid term that doesn't accept disputes uint64 public constant MANUAL_DEACTIVATION = uint64(-1); uint64 internal constant MODIFIER_ALLOWED_TERM_TRANSITIONS = 1; bytes4 private constant ARBITRABLE_INTERFACE_ID = 0xabababab; // TODO: interface id - uint16 internal constant GOVERNANCE_FEE_DIVISOR = 10000; // ‱ + uint16 internal constant PCT_BASE = 10000; // ‱ uint8 public constant MIN_RULING_OPTIONS = 2; uint8 public constant MAX_RULING_OPTIONS = 254; - // TODO: Move into term configuration (currently fee schedule) - uint64 public constant COMMIT_TERMS = 72; - uint64 public constant REVEAL_TERMS = 24; - event NewTerm(uint64 term, address indexed heartbeatSender); - event NewFeeStructure(uint64 fromTerm, uint64 feeStructureId); + event NewCourtConfig(uint64 fromTerm, uint64 courtConfigId); event TokenBalanceChange(address indexed token, address indexed owner, uint256 amount, bool positive); event JurorActivate(address indexed juror, uint64 fromTerm, uint64 toTerm); event JurorDeactivate(address indexed juror, uint64 lastTerm); @@ -212,11 +212,14 @@ contract Court is ERC900, ApproveAndCallFallBack { * @param _jurorFee The amount of _feeToken that is paid per juror per dispute * @param _heartbeatFee The amount of _feeToken per dispute to cover maintenance costs. * @param _draftFee The amount of _feeToken per juror to cover the drafting cost. - * @param _governanceShare Share in ‱ of fees that are paid to the governor. + * @param _governanceFeeShare Share in ‱ of fees that are paid to the governor. * @param _governor Address of the governor contract. * @param _firstTermStartTime Timestamp in seconds when the court will open (to give time for juror onboarding) * @param _jurorMinStake Minimum amount of juror tokens that can be activated - * @param _jurorCooldownTerms Number of terms before a juror tokens can be withdrawn after deactivation () + * @param _commitTerms Number of terms that the vote commit period lasts in an adjudication round + * @param _revealTerms Number of terms that the vote reveal period lasts in an adjudication round + * @param _appealTerms Number of terms during which a court ruling can be appealed + * @param _penaltyPct ‱ of jurorMinStake that can be slashed (1/10,000) */ constructor( uint64 _termDuration, @@ -225,26 +228,32 @@ contract Court is ERC900, ApproveAndCallFallBack { uint256 _jurorFee, uint256 _heartbeatFee, uint256 _draftFee, - uint16 _governanceShare, + uint16 _governanceFeeShare, address _governor, uint64 _firstTermStartTime, uint256 _jurorMinStake, - uint64 _jurorCooldownTerms + uint64 _commitTerms, + uint64 _revealTerms, + uint64 _appealTerms, + uint16 _penaltyPct ) public { termDuration = _termDuration; jurorToken = _jurorToken; jurorMinStake = _jurorMinStake; governor = _governor; - jurorCooldownTerms = _jurorCooldownTerms; - feeStructures.length = 1; // leave index 0 empty - _setFeeStructure( + courtConfigs.length = 1; // leave index 0 empty + _setCourtConfig( ZERO_TERM, _feeToken, _jurorFee, _heartbeatFee, _draftFee, - _governanceShare + _governanceFeeShare, + _commitTerms, + _revealTerms, + _appealTerms, + _penaltyPct ); terms[ZERO_TERM].startTime = _firstTermStartTime - _termDuration; @@ -260,10 +269,10 @@ contract Court is ERC900, ApproveAndCallFallBack { address heartbeatSender = msg.sender; // Set fee structure for term - if (nextTerm.feeStructureId == 0) { - nextTerm.feeStructureId = prevTerm.feeStructureId; + if (nextTerm.courtConfigId == 0) { + nextTerm.courtConfigId = prevTerm.courtConfigId; } else { - feeChangeTerm = ZERO_TERM; // fee structure changed in this term + configChangeTerm = ZERO_TERM; // fee structure changed in this term } // TODO: skip period if you can @@ -273,11 +282,11 @@ contract Court is ERC900, ApproveAndCallFallBack { nextTerm.randomnessBN = blockNumber() + 1; // randomness source set to next block (unknown when heartbeat happens) _processJurorQueues(nextTerm); - FeeStructure storage feeStructure = feeStructures[nextTerm.feeStructureId]; - uint256 totalFee = nextTerm.dependingDrafts * feeStructure.heartbeatFee; + CourtConfig storage courtConfig = courtConfigs[nextTerm.courtConfigId]; + uint256 totalFee = nextTerm.dependingDrafts * courtConfig.heartbeatFee; if (totalFee > 0) { - _payFees(feeStructure.feeToken, heartbeatSender, totalFee, feeStructure.governanceShare); + _payFees(courtConfig.feeToken, heartbeatSender, totalFee, courtConfig.governanceFeeShare); } term += 1; @@ -342,8 +351,8 @@ contract Court is ERC900, ApproveAndCallFallBack { terms[round.draftTerm].dependingDrafts -= 1; // refund fees - (ERC20 feeToken, uint256 feeAmount, uint16 governanceShare) = feeForJurorDraft(round.draftTerm, round.jurorNumber); - _payFees(feeToken, round.triggeredBy, feeAmount, governanceShare); + (ERC20 feeToken, uint256 feeAmount, uint16 governanceFeeShare) = feeForJurorDraft(round.draftTerm, round.jurorNumber); + _payFees(feeToken, round.triggeredBy, feeAmount, governanceFeeShare); emit DisputeStateChanged(_disputeId, dispute.state); } @@ -353,9 +362,9 @@ contract Court is ERC900, ApproveAndCallFallBack { ensureTerm { Dispute storage dispute = disputes[_disputeId]; - uint256 roundId = dispute.rounds.length - 1; - AdjudicationRound storage round = dispute.rounds[roundId]; + AdjudicationRound storage round = dispute.rounds[dispute.rounds.length - 1]; Term storage draftTerm = terms[term]; + CourtConfig storage config = courtConfigs[draftTerm.courtConfigId]; // safe to use directly as it is the current term // TODO: Work on recovery if draft doesn't occur in the term it was supposed to // it should be scheduled for a future draft and require to pay the heartbeat fee for the term @@ -367,25 +376,32 @@ contract Court is ERC900, ApproveAndCallFallBack { draftTerm.randomness = block.blockhash(draftTerm.randomnessBN); } - uint256[] memory jurorKeys = treeRandomSearch(round.jurorNumber, draftTerm.randomness, _disputeId); - require(jurorKeys.length == round.jurorNumber, "expected more jurors yo"); - - for (uint256 i = 0; i < jurorKeys.length; i++) { - address juror = jurorsByTreeId[jurorKeys[i]]; - - accounts[juror].pendingDisputes += 1; - - JurorVote memory vote; - vote.juror = juror; - round.votes.push(vote); - + uint256 maxPenalty = _pct4(jurorMinStake, config.penaltyPct); + uint256 jurorNumber = round.jurorNumber; + uint256 skippedJurors = 0; + round.votes.length = jurorNumber; + + for (uint256 i = 0; i < jurorNumber; i++) { + (uint256 jurorKey, uint256 stake) = treeSearch(draftTerm.randomness, _disputeId, i + skippedJurors); + address juror = jurorsByTreeId[jurorKey]; + + // Account storage jurorAccount = accounts[juror]; // Hitting stack too deep + uint256 newAtStake = accounts[juror].tokensAtStake + maxPenalty; + if (stake >= newAtStake) { + accounts[juror].tokensAtStake += newAtStake; + } else { + // SECURITY: This has a chance of bricking the round depending on the state of the court + skippedJurors++; + i--; + continue; + } + round.votes[i].juror = juror; emit JurorDrafted(_disputeId, juror, i); } dispute.state = DisputeState.Adjudicating; - FeeStructure storage fees = feeStructureForTerm(term); - _payFees(fees.feeToken, msg.sender, fees.draftFee * round.jurorNumber, fees.governanceShare); + _payFees(config.feeToken, msg.sender, config.draftFee * jurorNumber, config.governanceFeeShare); emit DisputeStateChanged(_disputeId, dispute.state); } @@ -493,10 +509,15 @@ contract Court is ERC900, ApproveAndCallFallBack { function checkAdjudicationState(uint256 _disputeId, uint256 _roundId, AdjudicationState _state) internal { AdjudicationRound storage round = disputes[_disputeId].rounds[_roundId]; + uint64 configId = terms[round.draftTerm].courtConfigId; + CourtConfig storage config = courtConfigs[uint256(configId)]; + + uint64 commitTerms = config.commitTerms; + uint64 revealTerms = config.revealTerms; // fromTerm is inclusive, toTerm is exclusive - uint256 fromTerm = _state == AdjudicationState.Commit ? round.draftTerm : round.draftTerm + COMMIT_TERMS; - uint256 toTerm = fromTerm + (_state == AdjudicationState.Commit ? COMMIT_TERMS : REVEAL_TERMS); + uint256 fromTerm = _state == AdjudicationState.Commit ? round.draftTerm : round.draftTerm + commitTerms; + uint256 toTerm = fromTerm + (_state == AdjudicationState.Commit ? commitTerms : revealTerms); require(term >= fromTerm && term < toTerm, ERROR_INVALID_ADJUDICATION_STATE); } @@ -515,43 +536,51 @@ contract Court is ERC900, ApproveAndCallFallBack { return keccak256(abi.encodePacked(_ruling, _salt)); } - function treeRandomSearch(uint64 _jurorNumber, bytes32 _termRandomness, uint256 _disputeId) internal returns (uint256[] memory) { - bytes32 seed = keccak256(abi.encodePacked(_termRandomness, _disputeId)); - return sumTree.randomSortition(_jurorNumber, uint256(seed)); + function treeSearch(bytes32 _termRandomness, uint256 _disputeId, uint256 _iteration) internal returns (uint256 key, uint256 value) { + bytes32 seed = keccak256(abi.encodePacked(_termRandomness, _disputeId, _iteration)); + // TODO: optimize by caching tree.totalSum(), and perform a `tree.unsafeSortition(seed % totalSum)` (unimplemented) + return sumTree.randomSortition(uint256(seed)); } /** * @dev Assumes term is up to date */ - function feeForJurorDraft(uint64 _draftTerm, uint64 _jurorNumber) public view returns (ERC20 feeToken, uint256 feeAmount, uint16 governanceShare) { - FeeStructure storage fees = feeStructureForTerm(_draftTerm); + function feeForJurorDraft(uint64 _draftTerm, uint64 _jurorNumber) public view returns (ERC20 feeToken, uint256 feeAmount, uint16 governanceFeeShare) { + CourtConfig storage fees = courtConfigForTerm(_draftTerm); feeToken = fees.feeToken; - governanceShare = fees.governanceShare; + governanceFeeShare = fees.governanceFeeShare; feeAmount = fees.heartbeatFee + _jurorNumber * (fees.jurorFee + fees.draftFee); } - function feeStructureForTerm(uint64 _term) internal view returns (FeeStructure storage) { + function courtConfigForTerm(uint64 _term) internal view returns (CourtConfig storage) { uint64 feeTerm; if (_term <= term) { feeTerm = _term; // for past terms, use the fee structure of the specific term - } else if (feeChangeTerm <= _term) { - feeTerm = feeChangeTerm; // if fees are changing before the draft, use the incoming fee schedule + } else if (configChangeTerm <= _term) { + feeTerm = configChangeTerm; // if fees are changing before the draft, use the incoming fee schedule } else { feeTerm = term; // if no changes are scheduled, use the current term fee schedule (which CANNOT change for this term) } - uint256 feeStructureId = uint256(terms[feeTerm].feeStructureId); - return feeStructures[feeStructureId]; + uint256 courtConfigId = uint256(terms[feeTerm].courtConfigId); + return courtConfigs[courtConfigId]; } - function _payFees(ERC20 _feeToken, address _to, uint256 _amount, uint16 _governanceShare) internal { - if (_amount == 0) return; - _assignTokens(_feeToken, _to, _amount * uint256(GOVERNANCE_FEE_DIVISOR - _governanceShare) / GOVERNANCE_FEE_DIVISOR); + function _payFees(ERC20 _feeToken, address _to, uint256 _amount, uint16 _governanceFeeShare) internal { + if (_amount == 0) { + return; + } + + uint256 governanceFee = _pct4(_amount, _governanceFeeShare); + _assignTokens(_feeToken, _to, _amount - governanceFee); + + if (governanceFee == 0) { + return; + } - if (_governanceShare == 0) return; - _assignTokens(_feeToken, governor, _amount * uint256(_governanceShare) / GOVERNANCE_FEE_DIVISOR); + _assignTokens(_feeToken, governor, governanceFee); } // TODO: should we charge heartbeat fees to jurors? @@ -695,7 +724,7 @@ contract Court is ERC900, ApproveAndCallFallBack { if (_token == jurorToken) { if (account.state == AccountState.Juror) { - require(isJurorBalanceUnlocked(addr), ERROR_JUROR_TOKENS_AT_STAKE); + require(_amount <= unlockedBalanceOf(addr), ERROR_JUROR_TOKENS_AT_STAKE); account.state = AccountState.PastJuror; } @@ -710,20 +739,8 @@ contract Court is ERC900, ApproveAndCallFallBack { function unlockedBalanceOf(address _addr) public view returns (uint256) { Account storage account = accounts[_addr]; - if (account.state == AccountState.Juror) { - if (!isJurorBalanceUnlocked(_addr)) { - return 0; - } - } - return account.balances[jurorToken]; - } - - function sortition(uint256 v) public view returns (address) { - return jurorsByTreeId[sumTree.sortition(v)]; - } - - function treeTotalSum() public view returns (uint256) { - return sumTree.totalSum(); + // TODO: safe math + return account.balances[jurorToken] - account.tokensAtStake; } function _processJurorQueues(Term storage _incomingTerm) internal { @@ -774,37 +791,49 @@ contract Court is ERC900, ApproveAndCallFallBack { emit TokenBalanceChange(_token, _to, _amount, true); } - function _setFeeStructure( + function _setCourtConfig( uint64 _fromTerm, ERC20 _feeToken, uint256 _jurorFee, uint256 _heartbeatFee, uint256 _draftFee, - uint16 _governanceShare + uint16 _governanceFeeShare, + uint64 _commitTerms, + uint64 _revealTerms, + uint64 _appealTerms, + uint16 _penaltyPct ) internal { - // TODO: Require fee changes happening at least X terms in the future + // TODO: Require config changes happening at least X terms in the future // Where X is the amount of terms in the future a dispute can be scheduled to be drafted at - require(feeChangeTerm > term || term == ZERO_TERM, ERROR_PAST_TERM_FEE_CHANGE); - require(_governanceShare <= GOVERNANCE_FEE_DIVISOR, ERROR_GOVENANCE_FEE_TOO_HIGH); + require(configChangeTerm > term || term == ZERO_TERM, ERROR_PAST_TERM_FEE_CHANGE); + require(_governanceFeeShare <= PCT_BASE, ERROR_GOVENANCE_FEE_TOO_HIGH); + + require(_commitTerms > 0, ERROR_CONFIG_PERIOD_ZERO_TERMS); + require(_revealTerms > 0, ERROR_CONFIG_PERIOD_ZERO_TERMS); + require(_appealTerms > 0, ERROR_CONFIG_PERIOD_ZERO_TERMS); - if (feeChangeTerm != ZERO_TERM) { - terms[feeChangeTerm].feeStructureId = 0; // reset previously set fee structure change + if (configChangeTerm != ZERO_TERM) { + terms[configChangeTerm].courtConfigId = 0; // reset previously set fee structure change } - FeeStructure memory feeStructure = FeeStructure({ + CourtConfig memory courtConfig = CourtConfig({ feeToken: _feeToken, - governanceShare: _governanceShare, + governanceFeeShare: _governanceFeeShare, jurorFee: _jurorFee, heartbeatFee: _heartbeatFee, - draftFee: _draftFee + draftFee: _draftFee, + commitTerms: _commitTerms, + revealTerms: _revealTerms, + appealTerms: _appealTerms, + penaltyPct: _penaltyPct }); - uint64 feeStructureId = uint64(feeStructures.push(feeStructure) - 1); - terms[feeChangeTerm].feeStructureId = feeStructureId; - feeChangeTerm = _fromTerm; + uint64 courtConfigId = uint64(courtConfigs.push(courtConfig) - 1); + terms[configChangeTerm].courtConfigId = courtConfigId; + configChangeTerm = _fromTerm; - emit NewFeeStructure(_fromTerm, feeStructureId); + emit NewCourtConfig(_fromTerm, courtConfigId); } function time() internal view returns (uint64) { @@ -815,9 +844,8 @@ contract Court is ERC900, ApproveAndCallFallBack { return uint64(block.number); } - function isJurorBalanceUnlocked(address _jurorAddress) internal view returns (bool) { - Account storage account = accounts[_jurorAddress]; - return term > account.toTerm + jurorCooldownTerms && account.pendingDisputes == 0; + function _pct4(uint256 _number, uint16 _pct) internal pure returns (uint256) { + return _number * uint256(_pct) / uint256(PCT_BASE); } } diff --git a/contracts/lib/HexSumTree.sol b/contracts/lib/HexSumTree.sol index 81e07db3..5dec7b35 100644 --- a/contracts/lib/HexSumTree.sol +++ b/contracts/lib/HexSumTree.sol @@ -54,24 +54,14 @@ library HexSumTree { updateSums(self, key, delta, positive); } - function sortition(Tree storage self, uint256 value) internal view returns (uint256 key) { + function sortition(Tree storage self, uint256 value) internal view returns (uint256 key, uint256 nodeValue) { require(totalSum(self) > value, ERROR_SORTITION_OUT_OF_BOUNDS); return _sortition(self, value, BASE_KEY, self.rootDepth); } - function randomSortition(Tree storage self, uint256 n, uint256 seed) internal view returns (uint256[] memory keys) { - keys = new uint256[](n); - - // cache in the stack. TODO: verify these don't go to memory - uint256 sum = totalSum(self); - uint256 rootDepth = self.rootDepth; - uint256 baseKey = BASE_KEY; - - for (uint256 i = 0; i < n; i++) { - uint256 random = (seed * i) % sum; // intended overflow - keys[i] = _sortition(self, random, baseKey, rootDepth); - } + function randomSortition(Tree storage self, uint256 seed) internal view returns (uint256 key, uint256 nodeValue) { + return _sortition(self, seed % totalSum(self), BASE_KEY, self.rootDepth); } function _set(Tree storage self, uint256 key, uint256 value) private { @@ -85,7 +75,7 @@ library HexSumTree { } } - function _sortition(Tree storage self, uint256 value, uint256 node, uint256 depth) private view returns (uint256 key) { + function _sortition(Tree storage self, uint256 value, uint256 node, uint256 depth) private view returns (uint256 key, uint256 nodeValue) { uint256 checkedValue = 0; // Can optimize by having checkedValue = value - remainingValue uint256 checkingLevel = depth - 1; @@ -118,7 +108,7 @@ library HexSumTree { if (checkedValue + nodeSum <= value) { // not reached yet, move to next child checkedValue += nodeSum; } else { // value reached - return checkingNode; + return (checkingNode, nodeSum); } } // Invariant: this point should never be reached diff --git a/contracts/test/CourtMock.sol b/contracts/test/CourtMock.sol index a9501eed..0fe363f5 100644 --- a/contracts/test/CourtMock.sol +++ b/contracts/test/CourtMock.sol @@ -18,7 +18,10 @@ contract CourtMock is Court { address _governor, uint64 _firstTermStartTime, uint256 _jurorMinStake, - uint64 _jurorCooldownTerms + uint64 _commitTerms, + uint64 _revealTerms, + uint64 _appealTerms, + uint16 _penaltyPct ) Court( _termDuration, _jurorToken, @@ -30,7 +33,10 @@ contract CourtMock is Court { _governor, _firstTermStartTime, _jurorMinStake, - _jurorCooldownTerms + _commitTerms, + _revealTerms, + _appealTerms, + _penaltyPct ) public {} function mock_setTime(uint64 time) external { @@ -49,6 +55,15 @@ contract CourtMock is Court { mockBn += inc; } + function sortition(uint256 v) public view returns (address) { + var (k, ) = sumTree.sortition(v); + return jurorsByTreeId[k]; + } + + function treeTotalSum() public view returns (uint256) { + return sumTree.totalSum(); + } + function time() internal view returns (uint64) { return mockTime; } diff --git a/contracts/test/HexSumTreePublic.sol b/contracts/test/HexSumTreePublic.sol index 12b2a1d4..5dc052fb 100644 --- a/contracts/test/HexSumTreePublic.sol +++ b/contracts/test/HexSumTreePublic.sol @@ -41,7 +41,8 @@ contract HexSumTreePublic { } function sortition(uint256 v) public view returns (uint256) { - return uint256(tree.sortition(v)); + var (k,) = tree.sortition(v); + return uint256(k); } function get(uint256 l, uint256 key) public view returns (uint256) { diff --git a/contracts/test/TestFactory.sol b/contracts/test/TestFactory.sol index 96d6e2cc..57a40dff 100644 --- a/contracts/test/TestFactory.sol +++ b/contracts/test/TestFactory.sol @@ -41,7 +41,10 @@ contract CourtFactory is Factory { address(this), uint64(block.timestamp + 60 * 60), 1, - 1 + 1, + 1, + 1, + 100 ); emit Deployed(address(court)); diff --git a/test/court-lifecycle.js b/test/court-lifecycle.js index a82145bc..1acfec2c 100644 --- a/test/court-lifecycle.js +++ b/test/court-lifecycle.js @@ -34,6 +34,10 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { const jurorMinStake = 100 const cooldown = 10 const startBlock = 1000 + const commitTerms = 1 + const revealTerms = 1 + const appealTerms = 1 + const penaltyPct = 100 // 100‱ = 1% const initialBalance = 1e6 const richStake = 1000 @@ -41,7 +45,7 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { const juror2Stake = 300 const NEW_TERM_EVENT = 'NewTerm' - const NEW_FEE_STRUCTURE_EVENT = 'NewFeeStructure' + const NEW_COURT_CONFIG_EVENT = 'NewCourtConfig' before(async () => { this.tokenFactory = await TokenFactory.new() @@ -64,7 +68,10 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { governor, firstTermStart, jurorMinStake, - cooldown + commitTerms, + revealTerms, + appealTerms, + penaltyPct ) await this.court.mock_setBlockNumber(startBlock) @@ -108,13 +115,13 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { const [ startTime, dependingDraws, - feeStructureId, + courtConfigId, randomnessBn ] = await this.court.terms(1) await assertEqualBN(startTime, firstTermStart, 'first term start') await assertEqualBN(dependingDraws, 0, 'depending draws') - await assertEqualBN(feeStructureId, 1, 'fee structure id') + await assertEqualBN(courtConfigId, 1, 'court config id') await assertEqualBN(randomnessBn, startBlock + 1, 'randomeness bn') }) @@ -153,13 +160,13 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { const [ startTime, dependingDraws, - feeStructureId, + courtConfigId, randomnessBn ] = await this.court.terms(term) await assertEqualBN(startTime, firstTermStart + (term - 1) * termDuration, 'term start') await assertEqualBN(dependingDraws, 0, 'depending draws') - await assertEqualBN(feeStructureId, 1, 'fee structure id') + await assertEqualBN(courtConfigId, 1, 'court config id') await assertEqualBN(randomnessBn, startBlock + 1, 'randomeness bn') }) @@ -193,7 +200,8 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { it('juror can manually deactivate') - it('juror can withdraw after cooldown', async () => { + // TODO: refactor to use at stake tokens + it.skip('juror can withdraw after cooldown', async () => { await this.court.activate(term + 1, term + 2, { from: juror1 }) await passTerms(1) await assertEqualBN(this.court.treeTotalSum(), juror1Stake, 'juror added to tree') From 6a5737d3efe03c363c93b190e7e788748f79f917 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Tue, 19 Mar 2019 21:21:44 +0100 Subject: [PATCH 16/27] Start testing dispute lifecycle --- test/court-disputes.js | 116 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 test/court-disputes.js diff --git a/test/court-disputes.js b/test/court-disputes.js new file mode 100644 index 00000000..4c0bb5d0 --- /dev/null +++ b/test/court-disputes.js @@ -0,0 +1,116 @@ +const assertRevert = require('./helpers/assert-revert') +const { promisify } = require('util') + +const TokenFactory = artifacts.require('TokenFactory') +const CourtMock = artifacts.require('CourtMock') + +const MINIME = 'MiniMeToken' + +const getLog = (receipt, logName, argName) => { + const log = receipt.logs.find(({ event }) => event == logName) + return log ? log.args[argName] : null +} + +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: Disputes', ([ poor, rich, governor, juror1, juror2, arbitrable ]) => { + const NO_DATA = '' + const ZERO_ADDRESS = '0x' + '00'.repeat(20) + + const termDuration = 10 + const firstTermStart = 1 + const jurorMinStake = 100 + const startBlock = 1000 + const commitTerms = 1 + const revealTerms = 1 + const appealTerms = 1 + const penaltyPct = 100 // 100‱ = 1% + + const initialBalance = 1e6 + const richStake = 1000 + const juror1Stake = 1000 + const juror2Stake = 500 + + const NEW_TERM_EVENT = 'NewTerm' + const NEW_COURT_CONFIG_EVENT = 'NewCourtConfig' + + 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) + assertEqualBN(this.anj.balanceOf(rich), initialBalance, 'rich balance') + assertEqualBN(this.anj.balanceOf(poor), 0, 'poor balance') + + this.court = await CourtMock.new( + termDuration, + this.anj.address, + ZERO_ADDRESS, + 0, + 0, + 0, + 0, + governor, + firstTermStart, + jurorMinStake, + commitTerms, + revealTerms, + appealTerms, + penaltyPct + ) + 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.treeTotalSum(), 0, 'empty sum tree') + + await this.anj.approveAndCall(this.court.address, richStake, NO_DATA, { from: rich }) + await this.anj.approve(this.court.address, juror1Stake, { from: rich }) + await this.court.stakeFor(juror1, juror1Stake, NO_DATA, { from: rich }) + await this.anj.approve(this.court.address, juror2Stake, { from: rich }) + await this.court.stakeFor(juror2, juror2Stake, NO_DATA, { from: rich }) + + await assertEqualBN(this.court.totalStakedFor(rich), richStake, 'rich stake') + await assertEqualBN(this.court.totalStakedFor(juror1), juror1Stake, 'juror1 stake') + await assertEqualBN(this.court.totalStakedFor(juror2), juror2Stake, 'juror2 stake') + }) + + + context('activating jurors', () => { + 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') + } + + beforeEach(async () => { + await this.court.activate(1, 10000, { from: juror1 }) + await this.court.activate(1, 10000, { from: juror2 }) + await passTerms(1) + }) + + it('creates dispute and drafts jurors', async () => { + const jurors = 1 + const term = 3 + const rulings = 2 + await this.court.createDispute(arbitrable, rulings, jurors, term) + await passTerms(2) + await this.court.draftAdjudicationRound(0) + console.log(await this.court.getJurorVote(0, 0, 0)) + }) + }) +}) From 46ceb8f6fe2eba8bc193887841e3efac83652d70 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Wed, 20 Mar 2019 22:08:10 +0100 Subject: [PATCH 17/27] Test commit and reveal happy case + minor improvements --- contracts/Court.sol | 25 ++++++---- contracts/test/CourtMock.sol | 14 ++++++ package.json | 5 +- test/court-disputes.js | 95 +++++++++++++++++++++++++++++++----- test/court-lifecycle.js | 3 +- 5 files changed, 117 insertions(+), 25 deletions(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index 5cc72c81..cc5848ae 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -9,12 +9,14 @@ import "./standards/erc900/ERC900.sol"; import { ApproveAndCallFallBack } from "@aragon/apps-shared-minime/contracts/MiniMeToken.sol"; import "@aragon/os/contracts/lib/token/ERC20.sol"; import "@aragon/os/contracts/common/SafeERC20.sol"; +import "@aragon/os/contracts/lib/math/SafeMath.sol"; contract Court is ERC900, ApproveAndCallFallBack { using HexSumTree for HexSumTree.Tree; using ArrayUtils for address[]; using SafeERC20 for ERC20; + using SafeMath for uint256; enum AccountState { NotJuror, @@ -160,7 +162,7 @@ contract Court is ERC900, ApproveAndCallFallBack { bytes4 private constant ARBITRABLE_INTERFACE_ID = 0xabababab; // TODO: interface id uint16 internal constant PCT_BASE = 10000; // ‱ uint8 public constant MIN_RULING_OPTIONS = 2; - uint8 public constant MAX_RULING_OPTIONS = 254; + uint8 public constant MAX_RULING_OPTIONS = MIN_RULING_OPTIONS; event NewTerm(uint64 term, address indexed heartbeatSender); event NewCourtConfig(uint64 fromTerm, uint64 courtConfigId); @@ -198,7 +200,6 @@ contract Court is ERC900, ApproveAndCallFallBack { address _juror, AdjudicationState _state ) { - checkDisputeState(_disputeId, _roundId); checkAdjudicationState(_disputeId, _roundId, _state); require(_getJurorVote(_disputeId, _roundId, _draftId).juror == _juror, ERROR_INVALID_JUROR); @@ -305,7 +306,8 @@ contract Court is ERC900, ApproveAndCallFallBack { // 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 interface + // TODO: ERC165 check that _subject conforms to the Arbitrable interface + // TODO: Consider requiring that only the contract being arbitred can create a dispute require(_possibleRulings >= MIN_RULING_OPTIONS && _possibleRulings <= MAX_RULING_OPTIONS, ERROR_INVALID_RULING_OPTIONS); @@ -497,7 +499,14 @@ contract Court is ERC900, ApproveAndCallFallBack { require(jurorVote.ruling == uint8(Ruling.Missing), ERROR_ALREADY_VOTED); } - function checkDisputeState(uint256 _disputeId, uint256 _roundId) internal { + function getWinningRuling(uint256 _disputeId, uint256 _roundId) public view returns (uint8 ruling, uint256 rulingVotes) { + AdjudicationRound storage round = disputes[_disputeId].rounds[_roundId]; + + ruling = round.winningRuling; + rulingVotes = round.rulingVotes[ruling]; + } + + function checkAdjudicationState(uint256 _disputeId, uint256 _roundId, AdjudicationState _state) internal { Dispute storage dispute = disputes[_disputeId]; if (dispute.state == DisputeState.PreDraft) { draftAdjudicationRound(_disputeId); @@ -505,9 +514,6 @@ contract Court is ERC900, ApproveAndCallFallBack { require(dispute.state == DisputeState.Adjudicating, ERROR_INVALID_DISPUTE_STATE); require(_roundId == dispute.rounds.length - 1, ERROR_INVALID_ADJUDICATION_ROUND); - } - - function checkAdjudicationState(uint256 _disputeId, uint256 _roundId, AdjudicationState _state) internal { AdjudicationRound storage round = disputes[_disputeId].rounds[_roundId]; uint64 configId = terms[round.draftTerm].courtConfigId; CourtConfig storage config = courtConfigs[uint256(configId)]; @@ -536,7 +542,7 @@ contract Court is ERC900, ApproveAndCallFallBack { return keccak256(abi.encodePacked(_ruling, _salt)); } - function treeSearch(bytes32 _termRandomness, uint256 _disputeId, uint256 _iteration) internal returns (uint256 key, uint256 value) { + function treeSearch(bytes32 _termRandomness, uint256 _disputeId, uint256 _iteration) internal view returns (uint256 key, uint256 value) { bytes32 seed = keccak256(abi.encodePacked(_termRandomness, _disputeId, _iteration)); // TODO: optimize by caching tree.totalSum(), and perform a `tree.unsafeSortition(seed % totalSum)` (unimplemented) return sumTree.randomSortition(uint256(seed)); @@ -739,8 +745,7 @@ contract Court is ERC900, ApproveAndCallFallBack { function unlockedBalanceOf(address _addr) public view returns (uint256) { Account storage account = accounts[_addr]; - // TODO: safe math - return account.balances[jurorToken] - account.tokensAtStake; + return account.balances[jurorToken].sub(account.tokensAtStake); } function _processJurorQueues(Term storage _incomingTerm) internal { diff --git a/contracts/test/CourtMock.sol b/contracts/test/CourtMock.sol index 0fe363f5..67717602 100644 --- a/contracts/test/CourtMock.sol +++ b/contracts/test/CourtMock.sol @@ -6,6 +6,7 @@ import "../Court.sol"; contract CourtMock is Court { uint64 internal mockTime = 0; uint64 internal mockBn = 0; + bool internal treeSearchHijacked = false; constructor( uint64 _termDuration, @@ -55,6 +56,19 @@ contract CourtMock is Court { mockBn += inc; } + function mock_hijackTreeSearch() external { + treeSearchHijacked = true; + } + + function treeSearch(bytes32 _termRandomness, uint256 _disputeId, uint256 _iteration) internal view returns (uint256 key, uint256 value) { + if (!treeSearchHijacked) { + return super.treeSearch(_termRandomness, _disputeId, _iteration); + } + + key = _iteration; + return (key, sumTree.getItem(key)); + } + function sortition(uint256 v) public view returns (address) { var (k, ) = sumTree.sortition(v); return jurorsByTreeId[k]; diff --git a/package.json b/package.json index 43ff4a88..cd85a1ea 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,11 @@ "@aragon/apps-shared-migrations": "^1.0.0", "@aragon/test-helpers": "^1.0.1", "ganache-cli": "^6.1.0", - "truffle": "^4.1.14" + "truffle": "^4.1.14", + "web3-utils": "^1.0.0-beta.50" }, "dependencies": { "@aragon/apps-shared-minime": "^1.0.1", "@aragon/os": "^4.1.0-rc.1" } -} \ No newline at end of file +} diff --git a/test/court-disputes.js b/test/court-disputes.js index 4c0bb5d0..fe261b7a 100644 --- a/test/court-disputes.js +++ b/test/court-disputes.js @@ -1,5 +1,6 @@ const assertRevert = require('./helpers/assert-revert') const { promisify } = require('util') +const { soliditySha3 } = require('web3-utils') const TokenFactory = artifacts.require('TokenFactory') const CourtMock = artifacts.require('CourtMock') @@ -24,7 +25,7 @@ const assertLogs = async (receiptPromise, ...logNames) => { } } -contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, arbitrable ]) => { +contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arbitrable ]) => { const NO_DATA = '' const ZERO_ADDRESS = '0x' + '00'.repeat(20) @@ -40,10 +41,22 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, arbitrable const initialBalance = 1e6 const richStake = 1000 const juror1Stake = 1000 - const juror2Stake = 500 + const juror2Stake = 600 + const juror3Stake = 500 - const NEW_TERM_EVENT = 'NewTerm' - const NEW_COURT_CONFIG_EVENT = 'NewCourtConfig' + 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 SALT = soliditySha3('passw0rd') + + const encryptVote = (ruling, salt = SALT) => + soliditySha3( + { t: 'uint8', v: ruling }, + { t: 'bytes32', v: salt } + ) before(async () => { this.tokenFactory = await TokenFactory.new() @@ -78,16 +91,24 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, arbitrable await assertEqualBN(this.court.treeTotalSum(), 0, 'empty sum tree') await this.anj.approveAndCall(this.court.address, richStake, NO_DATA, { from: rich }) + await this.anj.approve(this.court.address, juror1Stake, { from: rich }) await this.court.stakeFor(juror1, juror1Stake, NO_DATA, { from: rich }) await this.anj.approve(this.court.address, juror2Stake, { from: rich }) await this.court.stakeFor(juror2, juror2Stake, NO_DATA, { from: rich }) + await this.anj.approve(this.court.address, juror3Stake, { from: rich }) + await this.court.stakeFor(juror3, juror3Stake, NO_DATA, { from: rich }) await assertEqualBN(this.court.totalStakedFor(rich), richStake, 'rich stake') await assertEqualBN(this.court.totalStakedFor(juror1), juror1Stake, 'juror1 stake') await assertEqualBN(this.court.totalStakedFor(juror2), juror2Stake, 'juror2 stake') + await assertEqualBN(this.court.totalStakedFor(juror3), juror3Stake, 'juror3 stake') }) + it('can encrypt votes', async () => { + const ruling = 10 + assert.equal(await this.court.encryptVote(ruling, SALT), encryptVote(ruling)) + }) context('activating jurors', () => { const passTerms = async terms => { @@ -98,19 +119,69 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, arbitrable } beforeEach(async () => { - await this.court.activate(1, 10000, { from: juror1 }) - await this.court.activate(1, 10000, { from: juror2 }) + const activateTerm = 1 + const deactivateTerm = 10000 + for (const juror of [juror1, juror2, juror3]) { + await this.court.activate(activateTerm, deactivateTerm, { from: juror }) + } await passTerms(1) }) - it('creates dispute and drafts jurors', async () => { - const jurors = 1 + context('on dispute', () => { + const jurors = 3 const term = 3 const rulings = 2 - await this.court.createDispute(arbitrable, rulings, jurors, term) - await passTerms(2) - await this.court.draftAdjudicationRound(0) - console.log(await this.court.getJurorVote(0, 0, 0)) + + const disputeId = 0 // TODO: Get from NewDispute event + + beforeEach(async () => { + await assertLogs(this.court.createDispute(arbitrable, rulings, jurors, term), NEW_DISPUTE_EVENT) + await passTerms(2) + }) + + context('with hijacked juror selection', () => { + const roundId = 0 + + beforeEach(async () => { + await this.court.mock_hijackTreeSearch() + await assertLogs(this.court.draftAdjudicationRound(roundId), JUROR_DRAFTED_EVENT, DISPUTE_STATE_CHANGED_EVENT) + + const expectedJurors = [juror1, juror2, juror3] + + for (const [ draftId, juror ] of expectedJurors.entries()) { + const [ jurorAddr, ruling ] = await this.court.getJurorVote(disputeId, roundId, draftId) + + assert.equal(jurorAddr, juror, `juror #${draftId} address`) + assert.equal(ruling, 0, `juror #${draftId} vote`) + } + + assertRevert(this.court.getJurorVote(0, 0, jurors)) // out of bounds + }) + + const commitVotes = async votes => { + for (const [draftId, [juror, vote]] of votes.entries()) { + const receiptPromise = this.court.commitVote(disputeId, roundId, draftId, encryptVote(vote), { from: juror }) + await assertLogs(receiptPromise, VOTE_COMMITTED_EVENT) + } + } + + const revealVotes = async votes => { + for (const [ draftId, [ juror, vote ]] of votes.entries()) { + const receiptPromise = this.court.revealVote(disputeId, roundId, draftId, vote, SALT, { from: juror }) + await assertLogs(receiptPromise, VOTE_REVEALED_EVENT) + } + } + + it('jurors can commit and reveal votes', async () => { + await commitVotes([[juror1, 1], [juror2, 1], [juror3, 2]]) + await passTerms(1) + await revealVotes([[juror1, 1], [juror2, 1], [juror3, 2]]) + const [ ruling, rulingVotes ] = await this.court.getWinningRuling(disputeId, roundId) + + assertEqualBN(ruling, 1, 'winning ruling') + assertEqualBN(rulingVotes, 2, 'winning ruling votes') + }) + }) }) }) }) diff --git a/test/court-lifecycle.js b/test/court-lifecycle.js index 1acfec2c..dd0c763c 100644 --- a/test/court-lifecycle.js +++ b/test/court-lifecycle.js @@ -98,8 +98,9 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { res(rec) })) + // TODO: This is actually measuring the deployment cost for CourtMock and not Court const { gasUsed } = await getReceipt(this.court.transactionHash) - assert.isBelow(gasUsed, BLOCK_GAS_LIMIT, 'should be deployable to under the gas limit') + assert.isBelow(gasUsed, BLOCK_GAS_LIMIT, 'CourtMock should be deployable to under the gas limit') }) context('before first term', () => { From 146f788faf8f332554f2e34ad19870f1102b61f3 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Thu, 21 Mar 2019 19:17:05 +0100 Subject: [PATCH 18/27] Implement appeals, handle updates after exit term --- contracts/Court.sol | 221 ++++++++++++++++++++++------------- contracts/test/CourtMock.sol | 2 +- test/court-disputes.js | 146 ++++++++++++++++++----- 3 files changed, 262 insertions(+), 107 deletions(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index cc5848ae..e136a51f 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -25,19 +25,19 @@ contract Court is ERC900, ApproveAndCallFallBack { } struct AccountUpdate { + uint64 term; bool positive; // TODO: optimize gas uint256 delta; } struct Account { mapping (address => uint256) balances; // token addr -> balance - AccountState state; // whether the account is not a juror, a current juror or a past juror - uint64 fromTerm; // first term in which the juror can be drawn - uint64 toTerm; // last term in which the juror can be drawn - uint256 tokensAtStake; // disputes in which the juror was drawn which haven't resolved - uint64 pendingDisputes; // TODO: remove - uint256 sumTreeId; // key in the sum tree used for sortition - AccountUpdate update; // next account update + AccountState state; // whether the account is not a juror, a current juror or a past juror + uint64 fromTerm; // first term in which the juror can be drawn + uint64 toTerm; // last term in which the juror can be drawn + 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 + AccountUpdate update; // next account update } struct CourtConfig { @@ -88,8 +88,6 @@ contract Court is ERC900, ApproveAndCallFallBack { enum DisputeState { PreDraft, Adjudicating, - Appealable, // TODO: do we need to store this state? - Executable, // TODO: do we need to store this state? Executed, Dismissed } @@ -102,8 +100,11 @@ contract Court is ERC900, ApproveAndCallFallBack { } enum AdjudicationState { + Invalid, Commit, - Reveal + Reveal, + Appealable, + Ended } // State constants which are set in the constructor and can't change @@ -155,6 +156,7 @@ contract Court is ERC900, ApproveAndCallFallBack { string internal constant ERROR_INVALID_RULING_OPTIONS = "COURT_INVALID_RULING_OPTIONS"; string internal constant ERROR_FAILURE_COMMITMENT_CHECK = "COURT_FAILURE_COMMITMENT_CHECK"; string internal constant ERROR_CONFIG_PERIOD_ZERO_TERMS = "COURT_CONFIG_PERIOD_ZERO_TERMS"; + string internal constant ERROR_CANT_DISMISS_APPEAL = "COURT_CANT_DISMISS_APPEAL"; uint64 internal constant ZERO_TERM = 0; // invalid term that doesn't accept disputes uint64 public constant MANUAL_DEACTIVATION = uint64(-1); @@ -167,8 +169,8 @@ contract Court is ERC900, ApproveAndCallFallBack { event NewTerm(uint64 term, address indexed heartbeatSender); event NewCourtConfig(uint64 fromTerm, uint64 courtConfigId); event TokenBalanceChange(address indexed token, address indexed owner, uint256 amount, bool positive); - event JurorActivate(address indexed juror, uint64 fromTerm, uint64 toTerm); - event JurorDeactivate(address indexed juror, uint64 lastTerm); + event JurorActivated(address indexed juror, uint64 fromTerm, uint64 toTerm); + event JurorDeactivated(address indexed juror, uint64 lastTerm); event JurorDrafted(uint256 indexed disputeId, address indexed juror, uint256 draftId); event DisputeStateChanged(uint256 indexed disputeId, DisputeState indexed state); event NewDispute(uint256 indexed disputeId, address indexed subject, uint64 indexed draftTerm, uint64 jurorNumber); @@ -176,6 +178,7 @@ contract Court is ERC900, ApproveAndCallFallBack { event VoteCommitted(uint256 indexed disputeId, uint256 indexed roundId, address indexed juror, bytes32 commitment); event VoteRevealed(uint256 indexed disputeId, uint256 indexed roundId, address indexed juror, uint8 ruling); event VoteLeaked(uint256 indexed disputeId, uint256 indexed roundId, address indexed juror, address leaker); + event RulingAppealed(uint256 indexed disputeId, uint256 indexed roundId, uint64 indexed draftTerm, uint256 jurorNumber); modifier only(address _addr) { require(msg.sender == _addr, ERROR_INVALID_ADDR); @@ -200,7 +203,7 @@ contract Court is ERC900, ApproveAndCallFallBack { address _juror, AdjudicationState _state ) { - checkAdjudicationState(_disputeId, _roundId, _state); + _checkAdjudicationState(_disputeId, _roundId, _state); require(_getJurorVote(_disputeId, _roundId, _draftId).juror == _juror, ERROR_INVALID_JUROR); _; @@ -265,8 +268,9 @@ contract Court is ERC900, ApproveAndCallFallBack { function heartbeat(uint64 _termTransitions) public { require(canTransitionTerm(), ERROR_UNFINISHED_TERM); - Term storage prevTerm = terms[term]; - Term storage nextTerm = terms[term + 1]; + uint64 prevTermId = term; + Term storage prevTerm = terms[prevTermId]; + Term storage nextTerm = terms[prevTermId + 1]; address heartbeatSender = msg.sender; // Set fee structure for term @@ -281,7 +285,7 @@ contract Court is ERC900, ApproveAndCallFallBack { // Set the start time of the term (ensures equally long terms, regardless of heartbeats) nextTerm.startTime = prevTerm.startTime + termDuration; nextTerm.randomnessBN = blockNumber() + 1; // randomness source set to next block (unknown when heartbeat happens) - _processJurorQueues(nextTerm); + _processJurorQueues(prevTermId, nextTerm); CourtConfig storage courtConfig = courtConfigs[nextTerm.courtConfigId]; uint256 totalFee = nextTerm.dependingDrafts * courtConfig.heartbeatFee; @@ -311,26 +315,14 @@ contract Court is ERC900, ApproveAndCallFallBack { require(_possibleRulings >= MIN_RULING_OPTIONS && _possibleRulings <= MAX_RULING_OPTIONS, ERROR_INVALID_RULING_OPTIONS); - (ERC20 feeToken, uint256 feeAmount,) = feeForJurorDraft(_draftTerm, _jurorNumber); - if (feeAmount > 0) { - require(feeToken.safeTransferFrom(msg.sender, this, feeAmount), ERROR_DEPOSIT_FAILED); - } - uint256 disputeId = disputes.length; disputes.length = disputeId + 1; Dispute storage dispute = disputes[disputeId]; dispute.subject = _subject; - dispute.state = DisputeState.PreDraft; - dispute.rounds.length = 1; dispute.possibleRulings = _possibleRulings; - AdjudicationRound storage round = dispute.rounds[0]; - round.draftTerm = _draftTerm; - round.jurorNumber = _jurorNumber; - round.triggeredBy = msg.sender; - - terms[_draftTerm].dependingDrafts += 1; + _newAdjudicationRound(dispute, _jurorNumber, _draftTerm); emit NewDispute(disputeId, _subject, _draftTerm, _jurorNumber); @@ -347,8 +339,9 @@ contract Court is ERC900, ApproveAndCallFallBack { require(round.triggeredBy == msg.sender, ERROR_ENTITY_CANT_DISMISS); require(dispute.state == DisputeState.PreDraft && round.draftTerm > term, ERROR_CANT_DISMISS_AFTER_DRAFT); + require(roundId == 0, ERROR_CANT_DISMISS_APPEAL); - dispute.state = roundId == 0 ? DisputeState.Dismissed : DisputeState.Appealable; + dispute.state = DisputeState.Dismissed; terms[round.draftTerm].dependingDrafts -= 1; @@ -388,9 +381,9 @@ contract Court is ERC900, ApproveAndCallFallBack { address juror = jurorsByTreeId[jurorKey]; // Account storage jurorAccount = accounts[juror]; // Hitting stack too deep - uint256 newAtStake = accounts[juror].tokensAtStake + maxPenalty; + uint256 newAtStake = accounts[juror].atStakeTokens + maxPenalty; if (stake >= newAtStake) { - accounts[juror].tokensAtStake += newAtStake; + accounts[juror].atStakeTokens += newAtStake; } else { // SECURITY: This has a chance of bricking the round depending on the state of the court skippedJurors++; @@ -476,6 +469,38 @@ contract Court is ERC900, ApproveAndCallFallBack { emit VoteRevealed(_disputeId, _roundId, msg.sender, _ruling); } + function appealRuling(uint256 _disputeId, uint256 _roundId) external ensureTerm { + _checkAdjudicationState(_disputeId, _roundId, AdjudicationState.Appealable); + + Dispute storage dispute = disputes[_disputeId]; + AdjudicationRound storage currentRound = dispute.rounds[_roundId]; + + uint64 appealJurorNumber = 2 * currentRound.jurorNumber + 1; // J' = 2J + 1 + uint64 appealDraftTerm = term + 1; // Appeals are drafted in the next term + + uint256 roundId = _newAdjudicationRound(dispute, appealJurorNumber, appealDraftTerm); + emit RulingAppealed(_disputeId, roundId, appealDraftTerm, appealJurorNumber); + } + + function _newAdjudicationRound(Dispute storage dispute, uint64 _jurorNumber, uint64 _draftTerm) internal returns (uint256 roundId) { + (ERC20 feeToken, uint256 feeAmount,) = feeForJurorDraft(_draftTerm, _jurorNumber); + if (feeAmount > 0) { + require(feeToken.safeTransferFrom(msg.sender, this, feeAmount), ERROR_DEPOSIT_FAILED); + } + + dispute.state = DisputeState.PreDraft; + + roundId = dispute.rounds.length; + dispute.rounds.length = roundId + 1; + + AdjudicationRound storage round = dispute.rounds[roundId]; + round.draftTerm = _draftTerm; + round.jurorNumber = _jurorNumber; + round.triggeredBy = msg.sender; + + terms[_draftTerm].dependingDrafts += 1; + } + function updateTally(uint256 _disputeId, uint256 _roundId, uint8 _ruling) internal { AdjudicationRound storage round = disputes[_disputeId].rounds[_roundId]; @@ -499,33 +524,49 @@ contract Court is ERC900, ApproveAndCallFallBack { require(jurorVote.ruling == uint8(Ruling.Missing), ERROR_ALREADY_VOTED); } - function getWinningRuling(uint256 _disputeId, uint256 _roundId) public view returns (uint8 ruling, uint256 rulingVotes) { - AdjudicationRound storage round = disputes[_disputeId].rounds[_roundId]; + function getWinningRuling(uint256 _disputeId) public view returns (uint8 ruling, uint256 rulingVotes) { + Dispute storage dispute = disputes[_disputeId]; + AdjudicationRound storage round = dispute.rounds[dispute.rounds.length - 1]; ruling = round.winningRuling; rulingVotes = round.rulingVotes[ruling]; } - function checkAdjudicationState(uint256 _disputeId, uint256 _roundId, AdjudicationState _state) internal { + function _checkAdjudicationState(uint256 _disputeId, uint256 _roundId, AdjudicationState _state) internal { Dispute storage dispute = disputes[_disputeId]; - if (dispute.state == DisputeState.PreDraft) { + DisputeState disputeState = dispute.state; + if (disputeState == DisputeState.PreDraft) { draftAdjudicationRound(_disputeId); } - require(dispute.state == DisputeState.Adjudicating, ERROR_INVALID_DISPUTE_STATE); + require(disputeState == DisputeState.Adjudicating, ERROR_INVALID_DISPUTE_STATE); require(_roundId == dispute.rounds.length - 1, ERROR_INVALID_ADJUDICATION_ROUND); - AdjudicationRound storage round = disputes[_disputeId].rounds[_roundId]; - uint64 configId = terms[round.draftTerm].courtConfigId; - CourtConfig storage config = courtConfigs[uint256(configId)]; + require(adjudicationStateAtTerm(_disputeId, _roundId, term) == _state, ERROR_INVALID_ADJUDICATION_STATE); + } - uint64 commitTerms = config.commitTerms; - uint64 revealTerms = config.revealTerms; + function adjudicationStateAtTerm(uint256 _disputeId, uint256 _roundId, uint64 _term) internal returns (AdjudicationState) { + Dispute storage dispute = disputes[_disputeId]; + AdjudicationRound storage round = disputes[_disputeId].rounds[_roundId]; - // fromTerm is inclusive, toTerm is exclusive - uint256 fromTerm = _state == AdjudicationState.Commit ? round.draftTerm : round.draftTerm + commitTerms; - uint256 toTerm = fromTerm + (_state == AdjudicationState.Commit ? commitTerms : revealTerms); + uint64 draftTerm = round.draftTerm; + uint64 configId = terms[draftTerm].courtConfigId; + CourtConfig storage config = courtConfigs[uint256(configId)]; - require(term >= fromTerm && term < toTerm, ERROR_INVALID_ADJUDICATION_STATE); + uint64 revealStart = draftTerm + config.commitTerms; + uint64 appealStart = revealStart + config.revealTerms; + uint64 appealEnd = appealStart + config.appealTerms; + + if (_term < draftTerm) { + return AdjudicationState.Invalid; + } else if (_term < revealStart) { + return AdjudicationState.Commit; + } else if (_term < appealStart) { + return AdjudicationState.Reveal; + } else if (_term < appealEnd) { + return AdjudicationState.Appealable; + } else { + return AdjudicationState.Ended; + } } function getJurorVote(uint256 _disputeId, uint256 _roundId, uint256 _draftId) public view returns (address juror, uint8 ruling) { @@ -612,7 +653,7 @@ contract Court is ERC900, ApproveAndCallFallBack { sumTree.update(sumTreeId, balance, true); } else { // TODO: check queue size limit - account.update = AccountUpdate({ delta: balance, positive: true }); + account.update = AccountUpdate({ delta: balance, positive: true, term: _fromTerm }); terms[_fromTerm].updateQueue.push(jurorAddress); } @@ -626,7 +667,7 @@ contract Court is ERC900, ApproveAndCallFallBack { account.state = AccountState.Juror; account.balances[jurorToken] = 0; // tokens are either pending the update or in the tree - emit JurorActivate(jurorAddress, _fromTerm, _toTerm); + emit JurorActivated(jurorAddress, _fromTerm, _toTerm); } // TODO: activate more tokens @@ -655,7 +696,7 @@ contract Court is ERC900, ApproveAndCallFallBack { terms[_lastTerm].egressQueue.push(jurorAddress); account.toTerm = _lastTerm; - emit JurorDeactivate(jurorAddress, _lastTerm); + emit JurorDeactivated(jurorAddress, _lastTerm); } function canTransitionTerm() public view returns (bool) { @@ -694,12 +735,13 @@ contract Court is ERC900, ApproveAndCallFallBack { } function totalStakedFor(address _addr) public view returns (uint256) { - uint256 sumTreeId = accounts[_addr].sumTreeId; + Account storage account = accounts[_addr]; + uint256 sumTreeId = account.sumTreeId; uint256 activeTokens = sumTreeId > 0 ? sumTree.getItem(sumTreeId) : 0; - AccountUpdate storage update = accounts[_addr].update; + AccountUpdate storage update = account.update; uint256 pendingTokens = update.positive ? update.delta : -update.delta; - return accounts[_addr].balances[jurorToken] + activeTokens + pendingTokens; + return account.balances[jurorToken] + activeTokens + pendingTokens; } function totalStaked() external view returns (uint256) { @@ -714,9 +756,7 @@ contract Court is ERC900, ApproveAndCallFallBack { return false; } - /** @dev Withdraw tokens. Note that we can't withdraw the tokens which are still atStake. - * Jurors can't withdraw their tokens if they have deposited some during this term. - * This is to prevent jurors from withdrawing tokens they could lose. + /** * @param _token Token to withdraw * @param _amount The amount to withdraw. */ @@ -745,38 +785,63 @@ contract Court is ERC900, ApproveAndCallFallBack { function unlockedBalanceOf(address _addr) public view returns (uint256) { Account storage account = accounts[_addr]; - return account.balances[jurorToken].sub(account.tokensAtStake); + return account.balances[jurorToken].sub(account.atStakeTokens); } - function _processJurorQueues(Term storage _incomingTerm) internal { - uint256 egressLength = _incomingTerm.egressQueue.length; - uint256 updatesLength = _incomingTerm.updateQueue.length; + function _processJurorQueues(uint64 _prevTermId, Term storage _incomingTerm) internal { + // Always process egress before updates + // If a juror update is scheduled for the same term, it will get settled processing its exit + _processEgressQueue(_incomingTerm, _prevTermId); + _processUpdateQueue(_incomingTerm); + } - for (uint256 i = 0; i < updatesLength; i++) { - address jurorUpdate = _incomingTerm.updateQueue[i]; - AccountUpdate storage update = accounts[jurorUpdate].update; + function _processEgressQueue(Term storage _incomingTerm, uint64 _prevTermId) internal { + uint256 length = _incomingTerm.egressQueue.length; - if (update.delta > 0) { - sumTree.update(accounts[jurorUpdate].sumTreeId, update.delta, update.positive); - delete accounts[jurorUpdate].update; - } - } - for (uint256 j = 0; j < egressLength; j++) { - address jurorEgress = _incomingTerm.egressQueue[j]; - - uint256 sumTreeId = accounts[jurorEgress].sumTreeId; - if (sumTreeId != 0) { - uint256 treeBalance = sumTree.getItem(sumTreeId); - accounts[jurorEgress].balances[jurorToken] += treeBalance; - sumTree.set(sumTreeId, 0); - delete accounts[jurorEgress].sumTreeId; + for (uint256 i = 0; i < length; i++) { + address juror = _incomingTerm.egressQueue[i]; + Account storage account = accounts[juror]; + + uint256 sumTreeId = account.sumTreeId; + uint256 treeBalance = sumTree.getItem(sumTreeId); + + // Juror has their update in this term or in the future + // settle immediately and remove from updates queue + AccountUpdate storage futureUpdate = account.update; + if (futureUpdate.term >= _prevTermId) { + if (futureUpdate.positive) { + treeBalance = treeBalance + futureUpdate.delta; + } else { + treeBalance = treeBalance - futureUpdate.delta; + } + + terms[futureUpdate.term].updateQueue.deleteItem(juror); } + + sumTree.set(sumTreeId, 0); + account.balances[jurorToken] += treeBalance; + account.state = AccountState.PastJuror; + delete account.update; + delete account.sumTreeId; } - if (egressLength > 0) { + if (length > 0) { delete _incomingTerm.egressQueue; } - if (updatesLength > 0) { + } + + function _processUpdateQueue(Term storage _incomingTerm) internal { + uint256 length = _incomingTerm.updateQueue.length; + for (uint256 i = 0; i < length; i++) { + Account storage account = accounts[_incomingTerm.updateQueue[i]]; + AccountUpdate storage update = account.update; + + if (update.delta > 0) { + sumTree.update(account.sumTreeId, update.delta, update.positive); + delete account.update; + } + } + if (length > 0) { delete _incomingTerm.updateQueue; } } diff --git a/contracts/test/CourtMock.sol b/contracts/test/CourtMock.sol index 67717602..978ce38d 100644 --- a/contracts/test/CourtMock.sol +++ b/contracts/test/CourtMock.sol @@ -65,7 +65,7 @@ contract CourtMock is Court { return super.treeSearch(_termRandomness, _disputeId, _iteration); } - key = _iteration; + key = _iteration % sumTree.nextKey; // loop return (key, sumTree.getItem(key)); } diff --git a/test/court-disputes.js b/test/court-disputes.js index fe261b7a..5098e1d2 100644 --- a/test/court-disputes.js +++ b/test/court-disputes.js @@ -25,7 +25,7 @@ const assertLogs = async (receiptPromise, ...logNames) => { } } -contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arbitrable ]) => { +contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arbitrable, other ]) => { const NO_DATA = '' const ZERO_ADDRESS = '0x' + '00'.repeat(20) @@ -49,6 +49,7 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arb const DISPUTE_STATE_CHANGED_EVENT = 'DisputeStateChanged' const VOTE_COMMITTED_EVENT = 'VoteCommitted' const VOTE_REVEALED_EVENT = 'VoteRevealed' + const RULING_APPEALED_EVENT = 'RulingAppealed' const SALT = soliditySha3('passw0rd') @@ -85,6 +86,8 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arb penaltyPct ) await this.court.mock_setBlockNumber(startBlock) + // tree searches always return jurors in the order that they were added to the tree + await this.court.mock_hijackTreeSearch() assert.equal(await this.court.token(), this.anj.address, 'court token') assert.equal(await this.court.jurorToken(), this.anj.address, 'court juror token') @@ -110,7 +113,7 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arb assert.equal(await this.court.encryptVote(ruling, SALT), encryptVote(ruling)) }) - context('activating jurors', () => { + context.only('activating jurors', () => { const passTerms = async terms => { await this.court.mock_timeTravel(terms * termDuration) await this.court.heartbeat(terms) @@ -124,7 +127,7 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arb for (const juror of [juror1, juror2, juror3]) { await this.court.activate(activateTerm, deactivateTerm, { from: juror }) } - await passTerms(1) + await passTerms(1) // term = 1 }) context('on dispute', () => { @@ -133,23 +136,37 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arb const rulings = 2 const disputeId = 0 // TODO: Get from NewDispute event + const firstRoundId = 0 beforeEach(async () => { await assertLogs(this.court.createDispute(arbitrable, rulings, jurors, term), NEW_DISPUTE_EVENT) - await passTerms(2) }) - context('with hijacked juror selection', () => { - const roundId = 0 + it('fails to draft outside of the draft term', async () => { + await passTerms(1) // term = 2 + await assertRevert(this.court.draftAdjudicationRound(firstRoundId), 'COURT_NOT_DRAFT_TERM') + await passTerms(2) // term = 4 + await assertRevert(this.court.draftAdjudicationRound(firstRoundId), 'COURT_NOT_DRAFT_TERM') + }) + + context('on juror draft (hijacked)', () => { + const commitVotes = async votes => { + for (const [draftId, [juror, vote]] of votes.entries()) { + const receiptPromise = this.court.commitVote(disputeId, firstRoundId, draftId, encryptVote(vote), { from: juror }) + await assertLogs(receiptPromise, VOTE_COMMITTED_EVENT) + } + } beforeEach(async () => { - await this.court.mock_hijackTreeSearch() - await assertLogs(this.court.draftAdjudicationRound(roundId), JUROR_DRAFTED_EVENT, DISPUTE_STATE_CHANGED_EVENT) + await passTerms(2) // term = 3 + await assertLogs(this.court.draftAdjudicationRound(firstRoundId), JUROR_DRAFTED_EVENT, DISPUTE_STATE_CHANGED_EVENT) + }) + it('selects expected jurors', async () => { const expectedJurors = [juror1, juror2, juror3] for (const [ draftId, juror ] of expectedJurors.entries()) { - const [ jurorAddr, ruling ] = await this.court.getJurorVote(disputeId, roundId, draftId) + const [ jurorAddr, ruling ] = await this.court.getJurorVote(disputeId, firstRoundId, draftId) assert.equal(jurorAddr, juror, `juror #${draftId} address`) assert.equal(ruling, 0, `juror #${draftId} vote`) @@ -158,28 +175,101 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arb assertRevert(this.court.getJurorVote(0, 0, jurors)) // out of bounds }) - const commitVotes = async votes => { - for (const [draftId, [juror, vote]] of votes.entries()) { - const receiptPromise = this.court.commitVote(disputeId, roundId, draftId, encryptVote(vote), { from: juror }) - await assertLogs(receiptPromise, VOTE_COMMITTED_EVENT) - } - } + it('fails to draft a second time', async () => { + await assertRevert(this.court.draftAdjudicationRound(firstRoundId), 'COURT_ROUND_ALREADY_DRAFTED') + }) - const revealVotes = async votes => { - for (const [ draftId, [ juror, vote ]] of votes.entries()) { - const receiptPromise = this.court.revealVote(disputeId, roundId, draftId, vote, SALT, { from: juror }) - await assertLogs(receiptPromise, VOTE_REVEALED_EVENT) - } - } + context('jurors commit', () => { + const votes = [[juror1, 2], [juror2, 1], [juror3, 1]] + const round1Ruling = 1 + const round1WinningVotes = 2 - it('jurors can commit and reveal votes', async () => { - await commitVotes([[juror1, 1], [juror2, 1], [juror3, 2]]) - await passTerms(1) - await revealVotes([[juror1, 1], [juror2, 1], [juror3, 2]]) - const [ ruling, rulingVotes ] = await this.court.getWinningRuling(disputeId, roundId) + const revealVotes = async votes => { + for (const [ draftId, [ juror, vote ]] of votes.entries()) { + const receiptPromise = this.court.revealVote(disputeId, firstRoundId, draftId, vote, SALT, { from: juror }) + await assertLogs(receiptPromise, VOTE_REVEALED_EVENT) + } + } - assertEqualBN(ruling, 1, 'winning ruling') - assertEqualBN(rulingVotes, 2, 'winning ruling votes') + beforeEach(async () => { + await commitVotes(votes) + }) + + it('fails to reveal during commit period', async () => { + const draftId = 0 + const [ juror, vote ] = votes[draftId] + const receiptPromise = this.court.revealVote(disputeId, firstRoundId, draftId, vote, SALT, { from: juror }) + assertRevert(receiptPromise, 'COURT_INVALID_ADJUDICATION_STATE') + }) + + it('fails to reveal if salt is incorrect', async () => { + await passTerms(1) // term = 4 + const draftId = 0 + const [ juror, vote ] = votes[draftId] + const badSalt = soliditySha3('not the salt') + const receiptPromise = this.court.revealVote(disputeId, firstRoundId, draftId, vote, badSalt, { from: juror }) + assertRevert(receiptPromise, 'COURT_FAILURE_COMMITMENT_CHECK') + }) + + it('fails to reveal if already revealed', async () => { + await passTerms(1) // term = 4 + const draftId = 0 + const [ juror, vote ] = votes[draftId] + await this.court.revealVote(disputeId, firstRoundId, draftId, vote, SALT, { from: juror }) // reveal once + const receiptPromise = this.court.revealVote(disputeId, firstRoundId, draftId, vote, SALT, { from: juror }) + assertRevert(receiptPromise, 'COURT_ALREADY_VOTED') // fails to reveal twice + }) + + it("fails to reveal if sender isn't the drafted juror", async () => { + await passTerms(1) // term = 4 + const draftId = 0 + const [, vote ] = votes[draftId] + const receiptPromise = this.court.revealVote(disputeId, firstRoundId, draftId, vote, SALT, { from: other }) + assertRevert(receiptPromise, 'COURT_INVALID_JUROR') + }) + + context('jurors reveal', () => { + beforeEach(async () => { + await passTerms(1) // term = 4 + await revealVotes(votes) + }) + + it('stored votes', async () => { + for (const [ draftId, [ juror, vote ]] of votes.entries()) { + const [, ruling ] = await this.court.getJurorVote(disputeId, firstRoundId, draftId) + + assert.equal(ruling, vote, `juror #${draftId} revealed vote ${vote}`) + } + }) + + it('has correct ruling result', async () => { + const [ ruling, rulingVotes ] = await this.court.getWinningRuling(disputeId) + + assertEqualBN(ruling, round1Ruling, 'winning ruling') + assertEqualBN(rulingVotes, round1WinningVotes, 'winning ruling votes') + }) + + it('fails to appeal during reveal period', async () => { + await assertRevert(this.court.appealRuling(disputeId, firstRoundId), 'COURT_INVALID_ADJUDICATION_STATE') + }) + + it('fails to appeal incorrect round', async () => { + await passTerms(1) // term = 5 + await assertRevert(this.court.appealRuling(disputeId, firstRoundId + 1), 'COURT_INVALID_ADJUDICATION_ROUND') + }) + + context('on appeal', () => { + beforeEach(async () => { + await passTerms(1) // term = 5 + await assertLogs(this.court.appealRuling(disputeId, firstRoundId), RULING_APPEALED_EVENT) + }) + + it('drafts jurors', async () => { + await passTerms(1) // term = 6 + await assertLogs(this.court.draftAdjudicationRound(firstRoundId), JUROR_DRAFTED_EVENT, DISPUTE_STATE_CHANGED_EVENT) + }) + }) + }) }) }) }) From 5896c122b81173ac7d43fc2286715acac9a92e83 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Fri, 22 Mar 2019 17:22:04 +0100 Subject: [PATCH 19/27] Round slashing settlement --- contracts/Court.sol | 165 ++++++++++++++++++++++++++------- contracts/test/CourtMock.sol | 14 ++- contracts/test/TestFactory.sol | 12 ++- test/court-disputes.js | 12 +-- test/court-lifecycle.js | 7 +- 5 files changed, 151 insertions(+), 59 deletions(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index e136a51f..7973afa5 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -44,9 +44,10 @@ contract Court is ERC900, ApproveAndCallFallBack { // Fee structure ERC20 feeToken; uint16 governanceFeeShare; // ‱ of fees going to the governor (1/10,000) - uint256 jurorFee; // per juror, total dispute fee = jurorFee * jurors drawn + 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 dispute + 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; @@ -67,6 +68,7 @@ contract Court is ERC900, ApproveAndCallFallBack { struct JurorVote { bytes32 commitment; uint8 ruling; + bool rewarded; address juror; } @@ -77,6 +79,8 @@ contract Court is ERC900, ApproveAndCallFallBack { uint64 draftTerm; uint64 jurorNumber; address triggeredBy; + bool settledPenalties; + uint256 slashedTokens; } enum Ruling { @@ -114,7 +118,6 @@ contract Court is ERC900, ApproveAndCallFallBack { // Global config, configurable by governor address public governor; // TODO: consider using aOS' ACL uint256 public jurorMinStake; // TODO: consider adding it to the conf - uint256 public maxAppeals = 5; CourtConfig[] public courtConfigs; // Court state @@ -157,6 +160,8 @@ contract Court is ERC900, ApproveAndCallFallBack { string internal constant ERROR_FAILURE_COMMITMENT_CHECK = "COURT_FAILURE_COMMITMENT_CHECK"; string internal constant ERROR_CONFIG_PERIOD_ZERO_TERMS = "COURT_CONFIG_PERIOD_ZERO_TERMS"; string internal constant ERROR_CANT_DISMISS_APPEAL = "COURT_CANT_DISMISS_APPEAL"; + string internal constant ERROR_PREV_ROUND_NOT_SETTLED = "COURT_PREV_ROUND_NOT_SETTLED"; + string internal constant ERROR_ROUND_ALREADY_SETTLED = "COURT_ROUND_ALREADY_SETTLED"; uint64 internal constant ZERO_TERM = 0; // invalid term that doesn't accept disputes uint64 public constant MANUAL_DEACTIVATION = uint64(-1); @@ -179,6 +184,7 @@ contract Court is ERC900, ApproveAndCallFallBack { event VoteRevealed(uint256 indexed disputeId, uint256 indexed roundId, address indexed juror, uint8 ruling); event VoteLeaked(uint256 indexed disputeId, uint256 indexed roundId, address indexed juror, address leaker); event RulingAppealed(uint256 indexed disputeId, uint256 indexed roundId, uint64 indexed draftTerm, uint256 jurorNumber); + event RulingExecuted(uint256 indexed disputeId, uint8 indexed ruling); modifier only(address _addr) { require(msg.sender == _addr, ERROR_INVALID_ADDR); @@ -216,13 +222,12 @@ contract Court is ERC900, ApproveAndCallFallBack { * @param _jurorFee The amount of _feeToken that is paid per juror per dispute * @param _heartbeatFee The amount of _feeToken per dispute to cover maintenance costs. * @param _draftFee The amount of _feeToken per juror to cover the drafting cost. + * @param _settleFee The amount of _feeToken per juror to cover round settlement cost. * @param _governanceFeeShare Share in ‱ of fees that are paid to the governor. * @param _governor Address of the governor contract. * @param _firstTermStartTime Timestamp in seconds when the court will open (to give time for juror onboarding) * @param _jurorMinStake Minimum amount of juror tokens that can be activated - * @param _commitTerms Number of terms that the vote commit period lasts in an adjudication round - * @param _revealTerms Number of terms that the vote reveal period lasts in an adjudication round - * @param _appealTerms Number of terms during which a court ruling can be appealed + * @param _roundStateDurations Number of terms that the different states a dispute round last * @param _penaltyPct ‱ of jurorMinStake that can be slashed (1/10,000) */ constructor( @@ -232,13 +237,12 @@ contract Court is ERC900, ApproveAndCallFallBack { uint256 _jurorFee, uint256 _heartbeatFee, uint256 _draftFee, + uint256 _settleFee, uint16 _governanceFeeShare, address _governor, uint64 _firstTermStartTime, uint256 _jurorMinStake, - uint64 _commitTerms, - uint64 _revealTerms, - uint64 _appealTerms, + uint64[3] _roundStateDurations, uint16 _penaltyPct ) public { termDuration = _termDuration; @@ -253,10 +257,9 @@ contract Court is ERC900, ApproveAndCallFallBack { _jurorFee, _heartbeatFee, _draftFee, + _settleFee, _governanceFeeShare, - _commitTerms, - _revealTerms, - _appealTerms, + _roundStateDurations, _penaltyPct ); terms[ZERO_TERM].startTime = _firstTermStartTime - _termDuration; @@ -322,6 +325,7 @@ contract Court is ERC900, ApproveAndCallFallBack { dispute.subject = _subject; dispute.possibleRulings = _possibleRulings; + // _newAdjudicationRound charges fees for starting the round _newAdjudicationRound(dispute, _jurorNumber, _draftTerm); emit NewDispute(disputeId, _subject, _draftTerm, _jurorNumber); @@ -377,7 +381,7 @@ contract Court is ERC900, ApproveAndCallFallBack { round.votes.length = jurorNumber; for (uint256 i = 0; i < jurorNumber; i++) { - (uint256 jurorKey, uint256 stake) = treeSearch(draftTerm.randomness, _disputeId, i + skippedJurors); + (uint256 jurorKey, uint256 stake) = _treeSearch(draftTerm.randomness, _disputeId, i + skippedJurors); address juror = jurorsByTreeId[jurorKey]; // Account storage jurorAccount = accounts[juror]; // Hitting stack too deep @@ -431,7 +435,7 @@ contract Court is ERC900, ApproveAndCallFallBack { ensureTerm ensureDrafted(_disputeId, _roundId, _draftId, _juror, AdjudicationState.Commit) { - checkVote(_disputeId, _roundId, _draftId, _leakedRuling, _salt); + _checkVote(_disputeId, _roundId, _draftId, _leakedRuling, _salt); uint8 ruling = uint8(Ruling.RefusedRuling); JurorVote storage vote = _getJurorVote(_disputeId, _roundId, _draftId); @@ -439,7 +443,7 @@ contract Court is ERC900, ApproveAndCallFallBack { // TODO: slash juror - updateTally(_disputeId, _roundId, ruling); + _updateTally(_disputeId, _roundId, ruling); emit VoteLeaked(_disputeId, _roundId, _juror, msg.sender); emit VoteRevealed(_disputeId, _roundId, _juror, ruling); @@ -456,7 +460,7 @@ contract Court is ERC900, ApproveAndCallFallBack { ensureTerm ensureDrafted(_disputeId, _roundId, _draftId, msg.sender, AdjudicationState.Reveal) { - checkVote(_disputeId, _roundId, _draftId, _ruling, _salt); + _checkVote(_disputeId, _roundId, _draftId, _ruling, _salt); Dispute storage dispute = disputes[_disputeId]; JurorVote storage vote = _getJurorVote(_disputeId, _roundId, _draftId); @@ -464,12 +468,14 @@ contract Court is ERC900, ApproveAndCallFallBack { require(_ruling > uint8(Ruling.Missing) && _ruling <= dispute.possibleRulings + 1, ERROR_INVALID_VOTE); vote.ruling = _ruling; - updateTally(_disputeId, _roundId, _ruling); + _updateTally(_disputeId, _roundId, _ruling); emit VoteRevealed(_disputeId, _roundId, msg.sender, _ruling); } function appealRuling(uint256 _disputeId, uint256 _roundId) external ensureTerm { + // TODO: Implement appeals limit + // TODO: Implement final appeal _checkAdjudicationState(_disputeId, _roundId, AdjudicationState.Appealable); Dispute storage dispute = disputes[_disputeId]; @@ -478,10 +484,98 @@ contract Court is ERC900, ApproveAndCallFallBack { uint64 appealJurorNumber = 2 * currentRound.jurorNumber + 1; // J' = 2J + 1 uint64 appealDraftTerm = term + 1; // Appeals are drafted in the next term + // _newAdjudicationRound charges fees for starting the round uint256 roundId = _newAdjudicationRound(dispute, appealJurorNumber, appealDraftTerm); emit RulingAppealed(_disputeId, roundId, appealDraftTerm, appealJurorNumber); } + 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 ruling = getWinningRuling(_disputeId); + + dispute.subject.rule(_disputeId, uint256(ruling)); + + emit RulingExecuted(_disputeId, ruling); + } + + // settle round just executes penalties, jurors should manually claim their rewards + function settleRoundSlashing(uint256 _disputeId, uint256 _roundId) external ensureTerm { + Dispute storage dispute = disputes[_disputeId]; + AdjudicationRound storage round = dispute.rounds[_roundId]; + + // 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 + require(_roundId == 0 || dispute.rounds[_roundId - 1].settledPenalties, ERROR_PREV_ROUND_NOT_SETTLED); + require(!round.settledPenalties, ERROR_ROUND_ALREADY_SETTLED); + + if (dispute.state != DisputeState.Executed) { + _checkAdjudicationState(_disputeId, dispute.rounds.length - 1, AdjudicationState.Ended); + } else { + revert(ERROR_INVALID_DISPUTE_STATE); + } + + uint8 ruling = getWinningRuling(_disputeId); + CourtConfig storage config = courtConfigs[terms[round.draftTerm].courtConfigId]; // safe to use directly as it is the current term + uint256 penalty = _pct4(jurorMinStake, config.penaltyPct); + + uint256 slashedTokens = 0; + uint256 votesLength = round.votes.length; + uint64 slashingUpdateTerm = term + 1; // TODO: check update queue size + + for (uint256 i = 0; i < votesLength; i++) { + JurorVote storage vote = round.votes[i]; + Account storage account = accounts[vote.juror]; + account.atStakeTokens -= penalty; + + // If the juror didn't vote for the final ruling + if (vote.ruling != ruling) { + slashedTokens += penalty; + + if (account.state == AccountState.PastJuror) { + // Slash from balance if the account already deactivated + account.balances[jurorToken] -= penalty; + } else { + assert(account.state == AccountState.Juror); + _editUpdate(account.update, penalty, false); // edit their update for slashing + + // Unless the juror already had an update scheduled for the term that they will be slashed at + if (account.update.term <= slashingUpdateTerm) { + if (account.update.term != 0) { + // Juror had a pending + terms[account.update.term].updateQueue.deleteItem(vote.juror); + } + + account.update.term = slashingUpdateTerm; + terms[slashingUpdateTerm].updateQueue.push(vote.juror); + } + } + } + } + + round.slashedTokens = slashedTokens; + round.settledPenalties = true; + + _payFees(config.feeToken, msg.sender, config.settleFee * round.jurorNumber, config.governanceFeeShare); + } + + function _editUpdate(AccountUpdate storage update, uint256 _delta, bool _positive) internal { + (update.delta, update.positive) = _signedSum(update.delta, update.positive, _delta, _positive); + } + + /** + * @dev Sum nA + nB which can be positive or negative denoted by pA and pB + */ + function _signedSum(uint256 nA, bool pA, uint256 nB, bool pB) internal pure returns (uint256 nC, bool pC) { + nC = nA + (pA == pB ? nB : -nB); + pC = pB ? nC >= nA : nA >= nC; + nC = pA == pC ? nC : -nC; + } + function _newAdjudicationRound(Dispute storage dispute, uint64 _jurorNumber, uint64 _draftTerm) internal returns (uint256 roundId) { (ERC20 feeToken, uint256 feeAmount,) = feeForJurorDraft(_draftTerm, _jurorNumber); if (feeAmount > 0) { @@ -501,7 +595,7 @@ contract Court is ERC900, ApproveAndCallFallBack { terms[_draftTerm].dependingDrafts += 1; } - function updateTally(uint256 _disputeId, uint256 _roundId, uint8 _ruling) internal { + function _updateTally(uint256 _disputeId, uint256 _roundId, uint8 _ruling) internal { AdjudicationRound storage round = disputes[_disputeId].rounds[_roundId]; uint256 rulingVotes = round.rulingVotes[_ruling] + 1; @@ -517,19 +611,21 @@ contract Court is ERC900, ApproveAndCallFallBack { } } - function checkVote(uint256 _disputeId, uint256 _roundId, uint256 _draftId, uint8 _ruling, bytes32 _salt) internal { + function _checkVote(uint256 _disputeId, uint256 _roundId, uint256 _draftId, uint8 _ruling, bytes32 _salt) internal { JurorVote storage jurorVote = _getJurorVote(_disputeId, _roundId, _draftId); require(jurorVote.commitment == encryptVote(_ruling, _salt), ERROR_FAILURE_COMMITMENT_CHECK); require(jurorVote.ruling == uint8(Ruling.Missing), ERROR_ALREADY_VOTED); } - function getWinningRuling(uint256 _disputeId) public view returns (uint8 ruling, uint256 rulingVotes) { + function getWinningRuling(uint256 _disputeId) public view returns (uint8 ruling) { Dispute storage dispute = disputes[_disputeId]; AdjudicationRound storage round = dispute.rounds[dispute.rounds.length - 1]; ruling = round.winningRuling; - rulingVotes = round.rulingVotes[ruling]; + if (Ruling(ruling) == Ruling.Missing) { + ruling = uint8(Ruling.RefusedRuling); + } } function _checkAdjudicationState(uint256 _disputeId, uint256 _roundId, AdjudicationState _state) internal { @@ -541,11 +637,10 @@ contract Court is ERC900, ApproveAndCallFallBack { require(disputeState == DisputeState.Adjudicating, ERROR_INVALID_DISPUTE_STATE); require(_roundId == dispute.rounds.length - 1, ERROR_INVALID_ADJUDICATION_ROUND); - require(adjudicationStateAtTerm(_disputeId, _roundId, term) == _state, ERROR_INVALID_ADJUDICATION_STATE); + require(_adjudicationStateAtTerm(_disputeId, _roundId, term) == _state, ERROR_INVALID_ADJUDICATION_STATE); } - function adjudicationStateAtTerm(uint256 _disputeId, uint256 _roundId, uint64 _term) internal returns (AdjudicationState) { - Dispute storage dispute = disputes[_disputeId]; + function _adjudicationStateAtTerm(uint256 _disputeId, uint256 _roundId, uint64 _term) internal view returns (AdjudicationState) { AdjudicationRound storage round = disputes[_disputeId].rounds[_roundId]; uint64 draftTerm = round.draftTerm; @@ -583,7 +678,7 @@ contract Court is ERC900, ApproveAndCallFallBack { return keccak256(abi.encodePacked(_ruling, _salt)); } - function treeSearch(bytes32 _termRandomness, uint256 _disputeId, uint256 _iteration) internal view returns (uint256 key, uint256 value) { + function _treeSearch(bytes32 _termRandomness, uint256 _disputeId, uint256 _iteration) internal view returns (uint256 key, uint256 value) { bytes32 seed = keccak256(abi.encodePacked(_termRandomness, _disputeId, _iteration)); // TODO: optimize by caching tree.totalSum(), and perform a `tree.unsafeSortition(seed % totalSum)` (unimplemented) return sumTree.randomSortition(uint256(seed)); @@ -597,7 +692,7 @@ contract Court is ERC900, ApproveAndCallFallBack { feeToken = fees.feeToken; governanceFeeShare = fees.governanceFeeShare; - feeAmount = fees.heartbeatFee + _jurorNumber * (fees.jurorFee + fees.draftFee); + feeAmount = fees.heartbeatFee + _jurorNumber * (fees.jurorFee + fees.draftFee + fees.settleFee); } function courtConfigForTerm(uint64 _term) internal view returns (CourtConfig storage) { @@ -867,10 +962,9 @@ contract Court is ERC900, ApproveAndCallFallBack { uint256 _jurorFee, uint256 _heartbeatFee, uint256 _draftFee, + uint256 _settleFee, uint16 _governanceFeeShare, - uint64 _commitTerms, - uint64 _revealTerms, - uint64 _appealTerms, + uint64[3] _roundStateDurations, uint16 _penaltyPct ) internal { // TODO: Require config changes happening at least X terms in the future @@ -879,9 +973,9 @@ contract Court is ERC900, ApproveAndCallFallBack { require(configChangeTerm > term || term == ZERO_TERM, ERROR_PAST_TERM_FEE_CHANGE); require(_governanceFeeShare <= PCT_BASE, ERROR_GOVENANCE_FEE_TOO_HIGH); - require(_commitTerms > 0, ERROR_CONFIG_PERIOD_ZERO_TERMS); - require(_revealTerms > 0, ERROR_CONFIG_PERIOD_ZERO_TERMS); - require(_appealTerms > 0, ERROR_CONFIG_PERIOD_ZERO_TERMS); + for (uint i = 0; i < _roundStateDurations.length; i++) { + require(_roundStateDurations[i] > 0, ERROR_CONFIG_PERIOD_ZERO_TERMS); + } if (configChangeTerm != ZERO_TERM) { terms[configChangeTerm].courtConfigId = 0; // reset previously set fee structure change @@ -893,9 +987,10 @@ contract Court is ERC900, ApproveAndCallFallBack { jurorFee: _jurorFee, heartbeatFee: _heartbeatFee, draftFee: _draftFee, - commitTerms: _commitTerms, - revealTerms: _revealTerms, - appealTerms: _appealTerms, + settleFee: _settleFee, + commitTerms: _roundStateDurations[0], + revealTerms: _roundStateDurations[1], + appealTerms: _roundStateDurations[2], penaltyPct: _penaltyPct }); diff --git a/contracts/test/CourtMock.sol b/contracts/test/CourtMock.sol index 978ce38d..2b11fdfe 100644 --- a/contracts/test/CourtMock.sol +++ b/contracts/test/CourtMock.sol @@ -15,13 +15,12 @@ contract CourtMock is Court { uint256 _jurorFee, uint256 _heartbeatFee, uint256 _draftFee, + uint256 _settleFee, uint16 _governanceShare, address _governor, uint64 _firstTermStartTime, uint256 _jurorMinStake, - uint64 _commitTerms, - uint64 _revealTerms, - uint64 _appealTerms, + uint64[3] _roundStateDurations, uint16 _penaltyPct ) Court( _termDuration, @@ -30,13 +29,12 @@ contract CourtMock is Court { _jurorFee, _heartbeatFee, _draftFee, + _settleFee, _governanceShare, _governor, _firstTermStartTime, _jurorMinStake, - _commitTerms, - _revealTerms, - _appealTerms, + _roundStateDurations, _penaltyPct ) public {} @@ -60,9 +58,9 @@ contract CourtMock is Court { treeSearchHijacked = true; } - function treeSearch(bytes32 _termRandomness, uint256 _disputeId, uint256 _iteration) internal view returns (uint256 key, uint256 value) { + function _treeSearch(bytes32 _termRandomness, uint256 _disputeId, uint256 _iteration) internal view returns (uint256 key, uint256 value) { if (!treeSearchHijacked) { - return super.treeSearch(_termRandomness, _disputeId, _iteration); + return super._treeSearch(_termRandomness, _disputeId, _iteration); } key = _iteration % sumTree.nextKey; // loop diff --git a/contracts/test/TestFactory.sol b/contracts/test/TestFactory.sol index 57a40dff..de7fcfdf 100644 --- a/contracts/test/TestFactory.sol +++ b/contracts/test/TestFactory.sol @@ -28,8 +28,13 @@ contract TokenFactory is Factory { } } -contract CourtFactory is Factory { +contract CourtFactory is Factory { function newCourtStaking(ERC20 anj) external { + uint64[3] memory roundStateDurations; + + roundStateDurations[0] = 1; + roundStateDurations[1] = 1; + roundStateDurations[2] = 1; Court court = new Court( 60 * 60, // 1h anj, @@ -38,12 +43,11 @@ contract CourtFactory is Factory { 0, 0, 0, + 0, address(this), uint64(block.timestamp + 60 * 60), 1, - 1, - 1, - 1, + roundStateDurations, 100 ); diff --git a/test/court-disputes.js b/test/court-disputes.js index 5098e1d2..f2f3b40e 100644 --- a/test/court-disputes.js +++ b/test/court-disputes.js @@ -72,7 +72,8 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arb this.court = await CourtMock.new( termDuration, this.anj.address, - ZERO_ADDRESS, + ZERO_ADDRESS, // no fees + 0, 0, 0, 0, @@ -80,9 +81,7 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arb governor, firstTermStart, jurorMinStake, - commitTerms, - revealTerms, - appealTerms, + [ commitTerms, appealTerms, revealTerms ], penaltyPct ) await this.court.mock_setBlockNumber(startBlock) @@ -243,10 +242,7 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arb }) it('has correct ruling result', async () => { - const [ ruling, rulingVotes ] = await this.court.getWinningRuling(disputeId) - - assertEqualBN(ruling, round1Ruling, 'winning ruling') - assertEqualBN(rulingVotes, round1WinningVotes, 'winning ruling votes') + assertEqualBN(this.court.getWinningRuling(disputeId), round1Ruling, 'winning ruling') }) it('fails to appeal during reveal period', async () => { diff --git a/test/court-lifecycle.js b/test/court-lifecycle.js index dd0c763c..5af38677 100644 --- a/test/court-lifecycle.js +++ b/test/court-lifecycle.js @@ -60,7 +60,8 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { this.court = await CourtMock.new( termDuration, this.anj.address, - ZERO_ADDRESS, + ZERO_ADDRESS, // no fees + 0, 0, 0, 0, @@ -68,9 +69,7 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { governor, firstTermStart, jurorMinStake, - commitTerms, - revealTerms, - appealTerms, + [ commitTerms, appealTerms, revealTerms ], penaltyPct ) await this.court.mock_setBlockNumber(startBlock) From 9d5d795c687ee01020cb04d08acffca5175f9516 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Fri, 22 Mar 2019 18:29:28 +0100 Subject: [PATCH 20/27] Reward settle and refund fees in settlement if no coherent jurors --- contracts/Court.sol | 69 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index 7973afa5..f8183463 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -162,6 +162,9 @@ contract Court is ERC900, ApproveAndCallFallBack { string internal constant ERROR_CANT_DISMISS_APPEAL = "COURT_CANT_DISMISS_APPEAL"; string internal constant ERROR_PREV_ROUND_NOT_SETTLED = "COURT_PREV_ROUND_NOT_SETTLED"; string internal constant ERROR_ROUND_ALREADY_SETTLED = "COURT_ROUND_ALREADY_SETTLED"; + string internal constant ERROR_ROUND_NOT_SETTLED = "COURT_ROUND_NOT_SETTLED"; + string internal constant ERROR_JUROR_ALREADY_REWARDED = "COURT_JUROR_ALREADY_REWARDED"; + string internal constant ERROR_JUROR_NOT_COHERENT = "COURT_JUROR_NOT_COHERENT"; uint64 internal constant ZERO_TERM = 0; // invalid term that doesn't accept disputes uint64 public constant MANUAL_DEACTIVATION = uint64(-1); @@ -170,6 +173,7 @@ contract Court is ERC900, ApproveAndCallFallBack { uint16 internal constant PCT_BASE = 10000; // ‱ uint8 public constant MIN_RULING_OPTIONS = 2; uint8 public constant MAX_RULING_OPTIONS = MIN_RULING_OPTIONS; + address internal constant BURN_ACCOUNT = 0xdead; event NewTerm(uint64 term, address indexed heartbeatSender); event NewCourtConfig(uint64 fromTerm, uint64 courtConfigId); @@ -180,11 +184,13 @@ contract Court is ERC900, ApproveAndCallFallBack { event DisputeStateChanged(uint256 indexed disputeId, DisputeState indexed state); event NewDispute(uint256 indexed disputeId, address indexed subject, uint64 indexed draftTerm, uint64 jurorNumber); event TokenWithdrawal(address indexed token, address indexed account, uint256 amount); - event VoteCommitted(uint256 indexed disputeId, uint256 indexed roundId, address indexed juror, bytes32 commitment); - event VoteRevealed(uint256 indexed disputeId, uint256 indexed roundId, address indexed juror, uint8 ruling); - event VoteLeaked(uint256 indexed disputeId, uint256 indexed roundId, address indexed juror, address leaker); + event VoteCommitted(uint256 indexed disputeId, uint256 indexed roundId, address indexed juror, uint256 draftId, bytes32 commitment); + event VoteRevealed(uint256 indexed disputeId, uint256 indexed roundId, address indexed juror, uint256 draftId, uint8 ruling); + event VoteLeaked(uint256 indexed disputeId, uint256 indexed roundId, address indexed juror, uint256 draftId, address leaker); event RulingAppealed(uint256 indexed disputeId, uint256 indexed roundId, uint64 indexed draftTerm, uint256 jurorNumber); event RulingExecuted(uint256 indexed disputeId, uint8 indexed ruling); + event RoundSlashingSettled(uint256 indexed disputeId, uint256 indexed roundId, uint256 slashedTokens); + event RewardSettled(uint256 indexed disputeId, uint256 indexed roundId, address indexed juror, uint256 draftId); modifier only(address _addr) { require(msg.sender == _addr, ERROR_INVALID_ADDR); @@ -420,7 +426,7 @@ contract Court is ERC900, ApproveAndCallFallBack { vote.commitment = _commitment; - emit VoteCommitted(_disputeId, _roundId, msg.sender, _commitment); + emit VoteCommitted(_disputeId, _roundId, msg.sender, _draftId, _commitment); } function leakVote( @@ -445,8 +451,8 @@ contract Court is ERC900, ApproveAndCallFallBack { _updateTally(_disputeId, _roundId, ruling); - emit VoteLeaked(_disputeId, _roundId, _juror, msg.sender); - emit VoteRevealed(_disputeId, _roundId, _juror, ruling); + emit VoteLeaked(_disputeId, _roundId, _juror, _draftId, msg.sender); + emit VoteRevealed(_disputeId, _roundId, _juror, _draftId, ruling); } function revealVote( @@ -470,7 +476,7 @@ contract Court is ERC900, ApproveAndCallFallBack { vote.ruling = _ruling; _updateTally(_disputeId, _roundId, _ruling); - emit VoteRevealed(_disputeId, _roundId, msg.sender, _ruling); + emit VoteRevealed(_disputeId, _roundId, msg.sender, _draftId, _ruling); } function appealRuling(uint256 _disputeId, uint256 _roundId) external ensureTerm { @@ -538,7 +544,7 @@ contract Court is ERC900, ApproveAndCallFallBack { if (account.state == AccountState.PastJuror) { // Slash from balance if the account already deactivated - account.balances[jurorToken] -= penalty; + _removeTokens(jurorToken, vote.juror, penalty); } else { assert(account.state == AccountState.Juror); _editUpdate(account.update, penalty, false); // edit their update for slashing @@ -546,7 +552,7 @@ contract Court is ERC900, ApproveAndCallFallBack { // Unless the juror already had an update scheduled for the term that they will be slashed at if (account.update.term <= slashingUpdateTerm) { if (account.update.term != 0) { - // Juror had a pending + // Juror had a pending update already terms[account.update.term].updateQueue.deleteItem(vote.juror); } @@ -560,7 +566,42 @@ contract Court is ERC900, ApproveAndCallFallBack { round.slashedTokens = slashedTokens; round.settledPenalties = true; + // No juror was coherent in the round + if (round.rulingVotes[ruling] == 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); + + emit RoundSlashingSettled(_disputeId, _roundId, slashedTokens); + } + + function settleReward(uint256 _disputeId, uint256 _roundId, uint256 _draftId) external ensureTerm { + Dispute storage dispute = disputes[_disputeId]; + AdjudicationRound storage round = dispute.rounds[_roundId]; + JurorVote storage vote = round.votes[_draftId]; + CourtConfig storage config = courtConfigs[terms[round.draftTerm].courtConfigId]; // safe to use directly as it is the current term + uint8 ruling = getWinningRuling(_disputeId); + + require(round.settledPenalties, ERROR_ROUND_NOT_SETTLED); + require(vote.ruling == ruling, ERROR_JUROR_NOT_COHERENT); + require(!vote.rewarded, ERROR_JUROR_ALREADY_REWARDED); + vote.rewarded = true; + + address juror = vote.juror; + uint256 coherentJurors = round.rulingVotes[ruling]; + uint256 slashedTokens = round.slashedTokens; + + if (slashedTokens > 0) { + _assignTokens(jurorToken, juror, slashedTokens / coherentJurors); + } + + uint256 totalFees = config.jurorFee * round.jurorNumber; + _payFees(config.feeToken, juror, totalFees / coherentJurors, config.governanceFeeShare); + + emit RewardSettled(_disputeId, _roundId, juror, _draftId); } function _editUpdate(AccountUpdate storage update, uint256 _delta, bool _positive) internal { @@ -568,7 +609,7 @@ contract Court is ERC900, ApproveAndCallFallBack { } /** - * @dev Sum nA + nB which can be positive or negative denoted by pA and pB + * @dev Sum nA + nB which can be positive or negative denoted by pA and pB */ function _signedSum(uint256 nA, bool pA, uint256 nB, bool pB) internal pure returns (uint256 nC, bool pC) { nC = nA + (pA == pB ? nB : -nB); @@ -872,7 +913,7 @@ contract Court is ERC900, ApproveAndCallFallBack { emit Unstaked(addr, _amount, totalStakedFor(addr), ""); } - account.balances[_token] -= _amount; + _removeTokens(_token, addr, _amount); require(_token.safeTransfer(addr, _amount), ERROR_TOKEN_TRANSFER_FAILED); emit TokenWithdrawal(_token, addr, _amount); @@ -956,6 +997,12 @@ contract Court is ERC900, ApproveAndCallFallBack { emit TokenBalanceChange(_token, _to, _amount, true); } + function _removeTokens(ERC20 _token, address _from, uint256 _amount) internal { + accounts[_from].balances[_token] -= _amount; + + emit TokenBalanceChange(_token, _from, _amount, false); + } + function _setCourtConfig( uint64 _fromTerm, ERC20 _feeToken, From 19e962cc4ce36028a9325e901bea7d8b7bbc2d01 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Fri, 22 Mar 2019 21:33:02 +0100 Subject: [PATCH 21/27] Bypass OOG error and basic tests for slashing and rewards --- contracts/Court.sol | 6 +++--- test/court-disputes.js | 32 ++++++++++++++++++++++++++++++-- truffle-config.js | 9 +++++++-- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index f8183463..3a8933a2 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -584,12 +584,12 @@ contract Court is ERC900, ApproveAndCallFallBack { JurorVote storage vote = round.votes[_draftId]; CourtConfig storage config = courtConfigs[terms[round.draftTerm].courtConfigId]; // safe to use directly as it is the current term uint8 ruling = getWinningRuling(_disputeId); - + require(round.settledPenalties, ERROR_ROUND_NOT_SETTLED); require(vote.ruling == ruling, ERROR_JUROR_NOT_COHERENT); require(!vote.rewarded, ERROR_JUROR_ALREADY_REWARDED); vote.rewarded = true; - + address juror = vote.juror; uint256 coherentJurors = round.rulingVotes[ruling]; uint256 slashedTokens = round.slashedTokens; @@ -609,7 +609,7 @@ contract Court is ERC900, ApproveAndCallFallBack { } /** - * @dev Sum nA + nB which can be positive or negative denoted by pA and pB + * @dev Sum nA + nB which can be positive or negative denoted by pA and pB */ function _signedSum(uint256 nA, bool pA, uint256 nB, bool pB) internal pure returns (uint256 nC, bool pC) { nC = nA + (pA == pB ? nB : -nB); diff --git a/test/court-disputes.js b/test/court-disputes.js index f2f3b40e..e5c3ceba 100644 --- a/test/court-disputes.js +++ b/test/court-disputes.js @@ -31,7 +31,7 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arb const termDuration = 10 const firstTermStart = 1 - const jurorMinStake = 100 + const jurorMinStake = 400 const startBlock = 1000 const commitTerms = 1 const revealTerms = 1 @@ -50,6 +50,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 ROUND_SLASHING_SETTLED_EVENT = 'RoundSlashingSettled' const SALT = soliditySha3('passw0rd') @@ -59,6 +60,8 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arb { t: 'bytes32', v: salt } ) + const pct4 = (n, p) => n * p / 1e4 + before(async () => { this.tokenFactory = await TokenFactory.new() }) @@ -112,7 +115,7 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arb assert.equal(await this.court.encryptVote(ruling, SALT), encryptVote(ruling)) }) - context.only('activating jurors', () => { + context('activating jurors', () => { const passTerms = async terms => { await this.court.mock_timeTravel(terms * termDuration) await this.court.heartbeat(terms) @@ -254,6 +257,31 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arb await assertRevert(this.court.appealRuling(disputeId, firstRoundId + 1), 'COURT_INVALID_ADJUDICATION_ROUND') }) + context('settling round', () => { + const slashed = pct4(jurorMinStake, penaltyPct) + + beforeEach(async () => { + await passTerms(2) // term = 6 + await assertLogs(this.court.settleRoundSlashing(disputeId, firstRoundId), ROUND_SLASHING_SETTLED_EVENT) + }) + + it('slashed incoherent juror', async () => { + await assertEqualBN(this.court.totalStakedFor(juror1), juror1Stake - slashed, 'juror1 slashed') + }) + + it('coherent jurors can claim reward', async () => { + const reward = slashed / 2 + + await assertEqualBN(this.court.totalStakedFor(juror2), juror2Stake, 'juror2 pre-reward') + await assertLogs(this.court.settleReward(disputeId, firstRoundId, 1)) + await assertEqualBN(this.court.totalStakedFor(juror2), juror2Stake + reward, 'juror2 post-reward') + + await assertEqualBN(this.court.totalStakedFor(juror3), juror3Stake, 'juror3 pre-reward') + await assertLogs(this.court.settleReward(disputeId, firstRoundId, 2)) + await assertEqualBN(this.court.totalStakedFor(juror3), juror3Stake + reward, 'juror3 post-reward') + }) + }) + context('on appeal', () => { beforeEach(async () => { await passTerms(1) // term = 5 diff --git a/truffle-config.js b/truffle-config.js index c107e6cc..f27faf6c 100644 --- a/truffle-config.js +++ b/truffle-config.js @@ -1,12 +1,17 @@ const aOSConfig = require("@aragon/os/truffle-config") -module.exports = { +let config = { ...aOSConfig, solc: { optimizer: { enabled: true, - runs: 1000 // could be increased depending on the final size of Court.sol + runs: 1 // could be increased depending on the final size of Court.sol + // Disabling the optimizer or setting a higher runs value causes CourtMock deployments to out of gas for any gas amount }, }, } + +config.networks.rpc.gas = 10e6 + +module.exports = config \ No newline at end of file From 8327a1f4a60cd60e315ca8f3d43c23ce8759622d Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Fri, 22 Mar 2019 21:43:35 +0100 Subject: [PATCH 22/27] Fix heartbeat transitioning one extra term --- contracts/Court.sol | 2 +- test/court-lifecycle.js | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index 3a8933a2..d4097f1e 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -306,7 +306,7 @@ contract Court is ERC900, ApproveAndCallFallBack { term += 1; emit NewTerm(term, heartbeatSender); - if (_termTransitions > 0 && canTransitionTerm()) { + if (_termTransitions > 1 && canTransitionTerm()) { heartbeat(_termTransitions - 1); } } diff --git a/test/court-lifecycle.js b/test/court-lifecycle.js index 5af38677..f32302b9 100644 --- a/test/court-lifecycle.js +++ b/test/court-lifecycle.js @@ -136,6 +136,12 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { await this.court.mock_setTime(firstTermStart - 1) await assertRevert(this.court.activate(1, 10, { from: poor }), 'COURT_TOKENS_BELOW_MIN_STAKE') }) + + it("doesn't perform more transitions than requested", async () => { + await this.court.mock_setTime(firstTermStart + termDuration * 100) + await this.court.heartbeat(3) + await assertEqualBN(this.court.term(), 3, 'current term') + }) }) context('on regular court terms', () => { From 323f362bc3d24a73952fb022e7dd31bf2146c8f7 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Fri, 22 Mar 2019 22:28:40 +0100 Subject: [PATCH 23/27] Properly order functions and notice strings --- contracts/Court.sol | 447 ++++++++++++++++++--------------- contracts/test/CourtMock.sol | 8 +- contracts/test/TestFactory.sol | 27 -- test/court-disputes.js | 2 +- test/court-lifecycle.js | 30 +-- test/court-staking.js | 22 +- 6 files changed, 287 insertions(+), 249 deletions(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index d4097f1e..551fa476 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -221,20 +221,20 @@ contract Court is ERC900, ApproveAndCallFallBack { _; } - /** @dev Constructor. - * @param _termDuration Duration in seconds per term (recommended 1 hour) - * @param _jurorToken The address of the juror work token contract. - * @param _feeToken The address of the token contract that is used to pay for fees. - * @param _jurorFee The amount of _feeToken that is paid per juror per dispute - * @param _heartbeatFee The amount of _feeToken per dispute to cover maintenance costs. - * @param _draftFee The amount of _feeToken per juror to cover the drafting cost. - * @param _settleFee The amount of _feeToken per juror to cover round settlement cost. - * @param _governanceFeeShare Share in ‱ of fees that are paid to the governor. - * @param _governor Address of the governor contract. - * @param _firstTermStartTime Timestamp in seconds when the court will open (to give time for juror onboarding) - * @param _jurorMinStake Minimum amount of juror tokens that can be activated - * @param _roundStateDurations Number of terms that the different states a dispute round last - * @param _penaltyPct ‱ of jurorMinStake that can be slashed (1/10,000) + /** + * @param _termDuration Duration in seconds per term (recommended 1 hour) + * @param _jurorToken The address of the juror work token contract. + * @param _feeToken The address of the token contract that is used to pay for fees. + * @param _jurorFee The amount of _feeToken that is paid per juror per dispute + * @param _heartbeatFee The amount of _feeToken per dispute to cover maintenance costs. + * @param _draftFee The amount of _feeToken per juror to cover the drafting cost. + * @param _settleFee The amount of _feeToken per juror to cover round settlement cost. + * @param _governanceFeeShare Share in ‱ of fees that are paid to the governor. + * @param _governor Address of the governor contract. + * @param _firstTermStartTime Timestamp in seconds when the court will open (to give time for juror onboarding) + * @param _jurorMinStake Minimum amount of juror tokens that can be activated + * @param _roundStateDurations Number of terms that the different states a dispute round last + * @param _penaltyPct ‱ of jurorMinStake that can be slashed (1/10,000) */ constructor( uint64 _termDuration, @@ -274,6 +274,9 @@ contract Court is ERC900, ApproveAndCallFallBack { assert(sumTree.insert(0) == 0); // first tree item is an empty juror } + /** + * @notice Send a heartbeat to the Court to transition up to `_termTransitions` + */ function heartbeat(uint64 _termTransitions) public { require(canTransitionTerm(), ERROR_UNFINISHED_TERM); @@ -293,7 +296,7 @@ contract Court is ERC900, ApproveAndCallFallBack { // Set the start time of the term (ensures equally long terms, regardless of heartbeats) nextTerm.startTime = prevTerm.startTime + termDuration; - nextTerm.randomnessBN = blockNumber() + 1; // randomness source set to next block (unknown when heartbeat happens) + nextTerm.randomnessBN = _blockNumber() + 1; // randomness source set to next block (unknown when heartbeat happens) _processJurorQueues(prevTermId, nextTerm); CourtConfig storage courtConfig = courtConfigs[nextTerm.courtConfigId]; @@ -311,6 +314,131 @@ contract Court is ERC900, ApproveAndCallFallBack { } } + /** + * @notice Stake `@tokenAmount(self.jurorToken(), _amount)` to the Court + */ + function stake(uint256 _amount, bytes) external { + _stake(msg.sender, msg.sender, _amount); + } + + /** + * @notice Stake `@tokenAmount(self.jurorToken(), _amount)` for `_to` to the Court + */ + function stakeFor(address _to, uint256 _amount, bytes) external { + _stake(msg.sender, _to, _amount); + } + + /** + * @notice Unstake `@tokenAmount(self.jurorToken(), _amount)` for `_to` from the Court + */ + function unstake(uint256 _amount, bytes) external { + return withdraw(jurorToken, _amount); // withdraw() ensures the correct term + } + + /** + * @notice Withdraw `@tokenAmount(_token, _amount)` from the Court + */ + function withdraw(ERC20 _token, uint256 _amount) public ensureTerm { + require(_amount > 0, ERROR_ZERO_TRANSFER); + + address addr = msg.sender; + Account storage account = accounts[addr]; + uint256 balance = account.balances[_token]; + require(balance >= _amount, ERROR_BALANCE_TOO_LOW); + + if (_token == jurorToken) { + if (account.state == AccountState.Juror) { + require(_amount <= unlockedBalanceOf(addr), ERROR_JUROR_TOKENS_AT_STAKE); + account.state = AccountState.PastJuror; + } + + emit Unstaked(addr, _amount, totalStakedFor(addr), ""); + } + + _removeTokens(_token, addr, _amount); + require(_token.safeTransfer(addr, _amount), ERROR_TOKEN_TRANSFER_FAILED); + + emit TokenWithdrawal(_token, addr, _amount); + } + + /** + * @notice Become an active juror on term `_fromTerm` until term `_toTerm` + */ + function activate(uint64 _fromTerm, uint64 _toTerm) external ensureTerm { + // TODO: Charge activation fee to juror + + address jurorAddress = msg.sender; + Account storage account = accounts[jurorAddress]; + uint256 balance = account.balances[jurorToken]; + + require(_fromTerm > term, ERROR_INVALID_ACTIVATION_TERM); + require(_toTerm > _fromTerm, ERROR_INVALID_DEACTIVATION_TERM); + require(account.state == AccountState.NotJuror, ERROR_INVALID_ACCOUNT_STATE); + require(balance >= jurorMinStake, ERROR_TOKENS_BELOW_MIN_STAKE); + + uint256 sumTreeId = account.sumTreeId; + if (sumTreeId == 0) { + sumTreeId = sumTree.insert(0); + accounts[jurorAddress].sumTreeId = sumTreeId; + jurorsByTreeId[sumTreeId] = jurorAddress; + } + + if (term == ZERO_TERM && _fromTerm == ZERO_TERM + 1) { + // allow direct juror onboardings before term 1 starts (no disputes depend on term 0) + sumTree.update(sumTreeId, balance, true); + } else { + // TODO: check queue size limit + account.update = AccountUpdate({ delta: balance, positive: true, term: _fromTerm }); + terms[_fromTerm].updateQueue.push(jurorAddress); + } + + if (_toTerm != MANUAL_DEACTIVATION) { + // TODO: check queue size limit + terms[_toTerm].egressQueue.push(jurorAddress); + } + + account.fromTerm = _fromTerm; + account.toTerm = _toTerm; + account.state = AccountState.Juror; + account.balances[jurorToken] = 0; // tokens are either pending the update or in the tree + + emit JurorActivated(jurorAddress, _fromTerm, _toTerm); + } + + // TODO: Activate more tokens as a juror + + /** + * @notice Stop being an active juror on term `_lastTerm` + */ + function deactivate(uint64 _lastTerm) external ensureTerm { + address jurorAddress = msg.sender; + Account storage account = accounts[jurorAddress]; + + require(account.state == AccountState.Juror, ERROR_INVALID_ACCOUNT_STATE); + require(_lastTerm > term, ERROR_INVALID_DEACTIVATION_TERM); + + // Juror didn't actually become activated + if (term < account.fromTerm && term != ZERO_TERM) { + terms[account.fromTerm].updateQueue.deleteItem(jurorAddress); + assert(account.update.positive); // If the juror didn't activate, its update can only be positive + account.balances[jurorToken] += account.update.delta; + delete account.update; + } + + if (account.toTerm != MANUAL_DEACTIVATION) { + terms[account.toTerm].egressQueue.deleteItem(jurorAddress); + } + + // TODO: check queue size limit + terms[_lastTerm].egressQueue.push(jurorAddress); + account.toTerm = _lastTerm; + + emit JurorDeactivated(jurorAddress, _lastTerm); + } + + /** + * @notice Create a dispute over `_subject` with `_possibleRulings` possible rulings, drafting `_jurorNumber` jurors in term `_draftTerm` + */ function createDispute(IArbitrable _subject, uint8 _possibleRulings, uint64 _jurorNumber, uint64 _draftTerm) external ensureTerm @@ -339,6 +467,9 @@ contract Court is ERC900, ApproveAndCallFallBack { return disputeId; } + /** + * @notice Dismissing dispute #`_disputeId` + */ function dismissDispute(uint256 _disputeId) external ensureTerm @@ -362,6 +493,9 @@ contract Court is ERC900, ApproveAndCallFallBack { emit DisputeStateChanged(_disputeId, dispute.state); } + /** + * @notice Draft jurors for the next round of dispute #`_disputeId` + */ function draftAdjudicationRound(uint256 _disputeId) public ensureTerm @@ -411,6 +545,9 @@ contract Court is ERC900, ApproveAndCallFallBack { emit DisputeStateChanged(_disputeId, dispute.state); } + /** + * @notice Commit juror vote for dispute #`_disputeId` (round #`_roundId`) + */ function commitVote( uint256 _disputeId, uint256 _roundId, @@ -429,6 +566,9 @@ contract Court is ERC900, ApproveAndCallFallBack { emit VoteCommitted(_disputeId, _roundId, msg.sender, _draftId, _commitment); } + /** + * @notice Leak vote for `_juror` in dispute #`_disputeId` (round #`_roundId`) + */ function leakVote( uint256 _disputeId, uint256 _roundId, @@ -455,6 +595,9 @@ contract Court is ERC900, ApproveAndCallFallBack { emit VoteRevealed(_disputeId, _roundId, _juror, _draftId, ruling); } + /** + * @notice Reveal juror `_ruling` vote in dispute #`_disputeId` (round #`_roundId`) + */ function revealVote( uint256 _disputeId, uint256 _roundId, @@ -479,6 +622,9 @@ contract Court is ERC900, ApproveAndCallFallBack { emit VoteRevealed(_disputeId, _roundId, msg.sender, _draftId, _ruling); } + /** + * @notice Appeal round #`_roundId` ruling in dispute #`_disputeId` + */ function appealRuling(uint256 _disputeId, uint256 _roundId) external ensureTerm { // TODO: Implement appeals limit // TODO: Implement final appeal @@ -495,6 +641,9 @@ contract Court is ERC900, ApproveAndCallFallBack { emit RulingAppealed(_disputeId, roundId, appealDraftTerm, appealJurorNumber); } + /** + * @notice Execute the final ruling of dispute #`_disputeId` + */ function executeRuling(uint256 _disputeId, uint256 _roundId) external ensureTerm { // checks that dispute is in adjudication state _checkAdjudicationState(_disputeId, _roundId, AdjudicationState.Ended); @@ -509,7 +658,10 @@ contract Court is ERC900, ApproveAndCallFallBack { emit RulingExecuted(_disputeId, ruling); } - // settle round just executes penalties, jurors should manually claim their rewards + /** + * @notice Execute the final ruling of dispute #`_disputeId` + * @dev Just executes penalties, jurors must manually claim their rewards + */ function settleRoundSlashing(uint256 _disputeId, uint256 _roundId) external ensureTerm { Dispute storage dispute = disputes[_disputeId]; AdjudicationRound storage round = dispute.rounds[_roundId]; @@ -578,6 +730,10 @@ contract Court is ERC900, ApproveAndCallFallBack { emit RoundSlashingSettled(_disputeId, _roundId, slashedTokens); } + /** + * @notice Claim juror reward for round #`_roundId` of dispute #`_disputeId` + * @dev Just executes penalties, jurors must manually claim their rewards + */ function settleReward(uint256 _disputeId, uint256 _roundId, uint256 _draftId) external ensureTerm { Dispute storage dispute = disputes[_disputeId]; AdjudicationRound storage round = dispute.rounds[_roundId]; @@ -604,6 +760,81 @@ contract Court is ERC900, ApproveAndCallFallBack { emit RewardSettled(_disputeId, _roundId, juror, _draftId); } + function canTransitionTerm() public view returns (bool) { + return neededTermTransitions() >= 1; + } + + function neededTermTransitions() public view returns (uint64) { + return (_time() - terms[term].startTime) / termDuration; + } + + function getJurorVote(uint256 _disputeId, uint256 _roundId, uint256 _draftId) public view returns (address juror, uint8 ruling) { + JurorVote storage jurorVote = _getJurorVote(_disputeId, _roundId, _draftId); + + return (jurorVote.juror, jurorVote.ruling); + } + + function encryptVote(uint8 _ruling, bytes32 _salt) public view returns (bytes32) { + return keccak256(abi.encodePacked(_ruling, _salt)); + } + + /** + * @dev Assumes term is up to date + */ + function feeForJurorDraft(uint64 _draftTerm, uint64 _jurorNumber) public view returns (ERC20 feeToken, uint256 feeAmount, uint16 governanceFeeShare) { + CourtConfig storage fees = courtConfigForTerm(_draftTerm); + + feeToken = fees.feeToken; + governanceFeeShare = fees.governanceFeeShare; + feeAmount = fees.heartbeatFee + _jurorNumber * (fees.jurorFee + fees.draftFee + fees.settleFee); + } + + /** + * @dev Callback of approveAndCall, allows staking directly with a transaction to the token contract. + * @param _from The address making the transfer. + * @param _amount Amount of tokens to transfer to Kleros (in basic units). + * @param _token Token address + */ + function receiveApproval(address _from, uint256 _amount, address _token, bytes) + public + only(_token) + { + if (_token == address(jurorToken)) { + _stake(_from, _from, _amount); + // TODO: Activate depending on data + } + } + + function totalStaked() external view returns (uint256) { + return jurorToken.balanceOf(this); + } + + function token() external view returns (address) { + return address(jurorToken); + } + + function supportsHistory() external pure returns (bool) { + return false; + } + + function totalStakedFor(address _addr) public view returns (uint256) { + Account storage account = accounts[_addr]; + uint256 sumTreeId = account.sumTreeId; + uint256 activeTokens = sumTreeId > 0 ? sumTree.getItem(sumTreeId) : 0; + AccountUpdate storage update = account.update; + uint256 pendingTokens = update.positive ? update.delta : -update.delta; + + return account.balances[jurorToken] + activeTokens + pendingTokens; + } + + /** + * @dev Assumes that it is always called ensuring the term + */ + function unlockedBalanceOf(address _addr) public view returns (uint256) { + Account storage account = accounts[_addr]; + return account.balances[jurorToken].sub(account.atStakeTokens); + } + function _editUpdate(AccountUpdate storage update, uint256 _delta, bool _positive) internal { (update.delta, update.positive) = _signedSum(update.delta, update.positive, _delta, _positive); } @@ -705,37 +936,16 @@ contract Court is ERC900, ApproveAndCallFallBack { } } - function getJurorVote(uint256 _disputeId, uint256 _roundId, uint256 _draftId) public view returns (address juror, uint8 ruling) { - JurorVote storage jurorVote = _getJurorVote(_disputeId, _roundId, _draftId); - - return (jurorVote.juror, jurorVote.ruling); - } - function _getJurorVote(uint256 _disputeId, uint256 _roundId, uint256 _draftId) internal view returns (JurorVote storage) { return disputes[_disputeId].rounds[_roundId].votes[_draftId]; } - function encryptVote(uint8 _ruling, bytes32 _salt) public view returns (bytes32) { - return keccak256(abi.encodePacked(_ruling, _salt)); - } - function _treeSearch(bytes32 _termRandomness, uint256 _disputeId, uint256 _iteration) internal view returns (uint256 key, uint256 value) { bytes32 seed = keccak256(abi.encodePacked(_termRandomness, _disputeId, _iteration)); // TODO: optimize by caching tree.totalSum(), and perform a `tree.unsafeSortition(seed % totalSum)` (unimplemented) return sumTree.randomSortition(uint256(seed)); } - /** - * @dev Assumes term is up to date - */ - function feeForJurorDraft(uint64 _draftTerm, uint64 _jurorNumber) public view returns (ERC20 feeToken, uint256 feeAmount, uint16 governanceFeeShare) { - CourtConfig storage fees = courtConfigForTerm(_draftTerm); - - feeToken = fees.feeToken; - governanceFeeShare = fees.governanceFeeShare; - feeAmount = fees.heartbeatFee + _jurorNumber * (fees.jurorFee + fees.draftFee + fees.settleFee); - } - function courtConfigForTerm(uint64 _term) internal view returns (CourtConfig storage) { uint64 feeTerm; @@ -766,164 +976,6 @@ contract Court is ERC900, ApproveAndCallFallBack { _assignTokens(_feeToken, governor, governanceFee); } - // TODO: should we charge heartbeat fees to jurors? - function activate(uint64 _fromTerm, uint64 _toTerm) external ensureTerm { - address jurorAddress = msg.sender; - Account storage account = accounts[jurorAddress]; - uint256 balance = account.balances[jurorToken]; - - require(_fromTerm > term, ERROR_INVALID_ACTIVATION_TERM); - require(_toTerm > _fromTerm, ERROR_INVALID_DEACTIVATION_TERM); - require(account.state == AccountState.NotJuror, ERROR_INVALID_ACCOUNT_STATE); - require(balance >= jurorMinStake, ERROR_TOKENS_BELOW_MIN_STAKE); - - uint256 sumTreeId = account.sumTreeId; - if (sumTreeId == 0) { - sumTreeId = sumTree.insert(0); - accounts[jurorAddress].sumTreeId = sumTreeId; - jurorsByTreeId[sumTreeId] = jurorAddress; - } - - if (term == ZERO_TERM && _fromTerm == ZERO_TERM + 1) { - // allow direct juror onboardings before term 1 starts (no disputes depend on term 0) - sumTree.update(sumTreeId, balance, true); - } else { - // TODO: check queue size limit - account.update = AccountUpdate({ delta: balance, positive: true, term: _fromTerm }); - terms[_fromTerm].updateQueue.push(jurorAddress); - } - - if (_toTerm != MANUAL_DEACTIVATION) { - // TODO: check queue size limit - terms[_toTerm].egressQueue.push(jurorAddress); - } - - account.fromTerm = _fromTerm; - account.toTerm = _toTerm; - account.state = AccountState.Juror; - account.balances[jurorToken] = 0; // tokens are either pending the update or in the tree - - emit JurorActivated(jurorAddress, _fromTerm, _toTerm); - } - - // TODO: activate more tokens - - // this can't called if the juror is deactivated on the schedule specified when calling activate - // can be called many times to modify the deactivation date - function deactivate(uint64 _lastTerm) external ensureTerm { - address jurorAddress = msg.sender; - Account storage account = accounts[jurorAddress]; - - require(account.state == AccountState.Juror, ERROR_INVALID_ACCOUNT_STATE); - require(_lastTerm > term, ERROR_INVALID_DEACTIVATION_TERM); - - // Juror didn't actually become activated - if (term < account.fromTerm && term != ZERO_TERM) { - terms[account.fromTerm].updateQueue.deleteItem(jurorAddress); - assert(account.update.positive); // If the juror didn't activate, its update can only be positive - account.balances[jurorToken] += account.update.delta; - delete account.update; - } - - if (account.toTerm != MANUAL_DEACTIVATION) { - terms[account.toTerm].egressQueue.deleteItem(jurorAddress); - } - - terms[_lastTerm].egressQueue.push(jurorAddress); - account.toTerm = _lastTerm; - - emit JurorDeactivated(jurorAddress, _lastTerm); - } - - function canTransitionTerm() public view returns (bool) { - return neededTermTransitions() >= 1; - } - - function neededTermTransitions() public view returns (uint64) { - return (time() - terms[term].startTime) / termDuration; - } - - // ERC900 - - function stake(uint256 _amount, bytes) external { - _stake(msg.sender, msg.sender, _amount); - } - - function stakeFor(address _to, uint256 _amount, bytes) external { - _stake(msg.sender, _to, _amount); - } - - /** @dev Callback of approveAndCall - transfer jurorTokens of a juror in the contract. Should be called by the jurorToken contract. TRUSTED. - * @param _from The address making the transfer. - * @param _amount Amount of tokens to transfer to Kleros (in basic units). - */ - function receiveApproval(address _from, uint256 _amount, address token, bytes) - public - only(token) - { - if (token == address(jurorToken)) { - _stake(_from, _from, _amount); - } - } - - function unstake(uint256 _amount, bytes) external { - return withdraw(jurorToken, _amount); - } - - function totalStakedFor(address _addr) public view returns (uint256) { - Account storage account = accounts[_addr]; - uint256 sumTreeId = account.sumTreeId; - uint256 activeTokens = sumTreeId > 0 ? sumTree.getItem(sumTreeId) : 0; - AccountUpdate storage update = account.update; - uint256 pendingTokens = update.positive ? update.delta : -update.delta; - - return account.balances[jurorToken] + activeTokens + pendingTokens; - } - - function totalStaked() external view returns (uint256) { - return jurorToken.balanceOf(this); - } - - function token() external view returns (address) { - return address(jurorToken); - } - - function supportsHistory() external pure returns (bool) { - return false; - } - - /** - * @param _token Token to withdraw - * @param _amount The amount to withdraw. - */ - function withdraw(ERC20 _token, uint256 _amount) public ensureTerm { - require(_amount > 0, ERROR_ZERO_TRANSFER); - - address addr = msg.sender; - Account storage account = accounts[addr]; - uint256 balance = account.balances[_token]; - require(balance >= _amount, ERROR_BALANCE_TOO_LOW); - - if (_token == jurorToken) { - if (account.state == AccountState.Juror) { - require(_amount <= unlockedBalanceOf(addr), ERROR_JUROR_TOKENS_AT_STAKE); - account.state = AccountState.PastJuror; - } - - emit Unstaked(addr, _amount, totalStakedFor(addr), ""); - } - - _removeTokens(_token, addr, _amount); - require(_token.safeTransfer(addr, _amount), ERROR_TOKEN_TRANSFER_FAILED); - - emit TokenWithdrawal(_token, addr, _amount); - } - - function unlockedBalanceOf(address _addr) public view returns (uint256) { - Account storage account = accounts[_addr]; - return account.balances[jurorToken].sub(account.atStakeTokens); - } - function _processJurorQueues(uint64 _prevTermId, Term storage _incomingTerm) internal { // Always process egress before updates // If a juror update is scheduled for the same term, it will get settled processing its exit @@ -1003,6 +1055,7 @@ contract Court is ERC900, ApproveAndCallFallBack { emit TokenBalanceChange(_token, _from, _amount, false); } + // TODO: Expose external function to change config function _setCourtConfig( uint64 _fromTerm, ERC20 _feeToken, @@ -1048,11 +1101,11 @@ contract Court is ERC900, ApproveAndCallFallBack { emit NewCourtConfig(_fromTerm, courtConfigId); } - function time() internal view returns (uint64) { + function _time() internal view returns (uint64) { return uint64(block.timestamp); } - function blockNumber() internal view returns (uint64) { + function _blockNumber() internal view returns (uint64) { return uint64(block.number); } diff --git a/contracts/test/CourtMock.sol b/contracts/test/CourtMock.sol index 2b11fdfe..e4d1d443 100644 --- a/contracts/test/CourtMock.sol +++ b/contracts/test/CourtMock.sol @@ -67,20 +67,20 @@ contract CourtMock is Court { return (key, sumTree.getItem(key)); } - function sortition(uint256 v) public view returns (address) { + function mock_sortition(uint256 v) public view returns (address) { var (k, ) = sumTree.sortition(v); return jurorsByTreeId[k]; } - function treeTotalSum() public view returns (uint256) { + function mock_treeTotalSum() public view returns (uint256) { return sumTree.totalSum(); } - function time() internal view returns (uint64) { + function _time() internal view returns (uint64) { return mockTime; } - function blockNumber() internal view returns (uint64) { + function _blockNumber() internal view returns (uint64) { return mockBn; } } \ No newline at end of file diff --git a/contracts/test/TestFactory.sol b/contracts/test/TestFactory.sol index de7fcfdf..3489846c 100644 --- a/contracts/test/TestFactory.sol +++ b/contracts/test/TestFactory.sol @@ -27,30 +27,3 @@ contract TokenFactory is Factory { emit Deployed(address(token)); } } - -contract CourtFactory is Factory { - function newCourtStaking(ERC20 anj) external { - uint64[3] memory roundStateDurations; - - roundStateDurations[0] = 1; - roundStateDurations[1] = 1; - roundStateDurations[2] = 1; - Court court = new Court( - 60 * 60, // 1h - anj, - ERC20(0), // no fees - 0, - 0, - 0, - 0, - 0, - address(this), - uint64(block.timestamp + 60 * 60), - 1, - roundStateDurations, - 100 - ); - - emit Deployed(address(court)); - } -} diff --git a/test/court-disputes.js b/test/court-disputes.js index e5c3ceba..e3cff953 100644 --- a/test/court-disputes.js +++ b/test/court-disputes.js @@ -93,7 +93,7 @@ contract('Court: Disputes', ([ poor, rich, governor, juror1, juror2, juror3, arb 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.treeTotalSum(), 0, 'empty sum tree') + await assertEqualBN(this.court.mock_treeTotalSum(), 0, 'empty sum tree') await this.anj.approveAndCall(this.court.address, richStake, NO_DATA, { from: rich }) diff --git a/test/court-lifecycle.js b/test/court-lifecycle.js index f32302b9..014769be 100644 --- a/test/court-lifecycle.js +++ b/test/court-lifecycle.js @@ -76,7 +76,7 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { 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.treeTotalSum(), 0, 'empty sum tree') + await assertEqualBN(this.court.mock_treeTotalSum(), 0, 'empty sum tree') await this.anj.approveAndCall(this.court.address, richStake, NO_DATA, { from: rich }) await this.anj.approve(this.court.address, juror1Stake, { from: rich }) @@ -129,7 +129,7 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { await this.court.mock_setTime(firstTermStart - 1) await this.court.activate(1, 10, { from: rich }) - await assertEqualBN(this.court.treeTotalSum(), richStake, 'total tree sum') + await assertEqualBN(this.court.mock_treeTotalSum(), richStake, 'total tree sum') }) it('reverts if activating balance is below dust', async () => { @@ -182,26 +182,26 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { await passTerms(1) - assert.equal(await this.court.sortition(0), juror1, 'sortition start edge juror1') - assert.equal(await this.court.sortition(juror1Stake / 2), juror1, 'sortition juror1') - assert.equal(await this.court.sortition(juror1Stake - 1), juror1, 'sortition juror1 end edge') - assert.equal(await this.court.sortition(juror1Stake), juror2, 'sortition juror2 start edge') - assert.equal(await this.court.sortition(juror1Stake + juror2Stake / 2), juror2, 'sortition juror2') - assert.equal(await this.court.sortition(juror1Stake + juror2Stake - 1), juror2, 'sortition juror2 end edge') + assert.equal(await this.court.mock_sortition(0), juror1, 'sortition start edge juror1') + assert.equal(await this.court.mock_sortition(juror1Stake / 2), juror1, 'sortition juror1') + assert.equal(await this.court.mock_sortition(juror1Stake - 1), juror1, 'sortition juror1 end edge') + assert.equal(await this.court.mock_sortition(juror1Stake), juror2, 'sortition juror2 start edge') + assert.equal(await this.court.mock_sortition(juror1Stake + juror2Stake / 2), juror2, 'sortition juror2') + assert.equal(await this.court.mock_sortition(juror1Stake + juror2Stake - 1), juror2, 'sortition juror2 end edge') - await assertRevert(this.court.sortition(juror1Stake + juror2Stake), 'SUM_TREE_SORTITION_OUT_OF_BOUNDS') - await assertEqualBN(this.court.treeTotalSum(), juror1Stake + juror2Stake, 'both jurors in the tree') + await assertRevert(this.court.mock_sortition(juror1Stake + juror2Stake), 'SUM_TREE_SORTITION_OUT_OF_BOUNDS') + await assertEqualBN(this.court.mock_treeTotalSum(), juror1Stake + juror2Stake, 'both jurors in the tree') }) it('jurors can deactivate', async () => { await this.court.activate(term + 1, term + 2, { from: juror1 }) await this.court.activate(term + 1, term + 3, { from: juror2 }) await passTerms(1) - await assertEqualBN(this.court.treeTotalSum(), juror1Stake + juror2Stake, 'both jurors in the tree') + await assertEqualBN(this.court.mock_treeTotalSum(), juror1Stake + juror2Stake, 'both jurors in the tree') await passTerms(1) - await assertEqualBN(this.court.treeTotalSum(), juror2Stake, 'only juror2 in tree') + await assertEqualBN(this.court.mock_treeTotalSum(), juror2Stake, 'only juror2 in tree') await passTerms(1) - await assertEqualBN(this.court.treeTotalSum(), 0, 'no jurors in tree') + await assertEqualBN(this.court.mock_treeTotalSum(), 0, 'no jurors in tree') }) it('juror can manually deactivate') @@ -210,9 +210,9 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => { it.skip('juror can withdraw after cooldown', async () => { await this.court.activate(term + 1, term + 2, { from: juror1 }) await passTerms(1) - await assertEqualBN(this.court.treeTotalSum(), juror1Stake, 'juror added to tree') + await assertEqualBN(this.court.mock_treeTotalSum(), juror1Stake, 'juror added to tree') await passTerms(1) - await assertEqualBN(this.court.treeTotalSum(), 0, 'juror removed from to tree') + await assertEqualBN(this.court.mock_treeTotalSum(), 0, 'juror removed from to tree') await assertRevert(this.court.unstake(1, NO_DATA, { from: juror1 }), 'COURT_JUROR_TOKENS_AT_STAKE') diff --git a/test/court-staking.js b/test/court-staking.js index 2a5d322b..6cc305f3 100644 --- a/test/court-staking.js +++ b/test/court-staking.js @@ -1,5 +1,5 @@ const TokenFactory = artifacts.require('TokenFactory') -const CourtFactory = artifacts.require('CourtFactory') +const CourtMock = artifacts.require('CourtMock') const COURT = 'Court' const MINIME = 'MiniMeToken' @@ -16,10 +16,10 @@ const assertEqualBN = async (actualPromise, expected, message) => contract('Court: Staking', ([ pleb, rich ]) => { const INITIAL_BALANCE = 1e6 const NO_DATA = '' + const ZERO_ADDRESS = '0x' + '00'.repeat(20) before(async () => { this.tokenFactory = await TokenFactory.new() - this.courtFactory = await CourtFactory.new() }) beforeEach(async () => { @@ -28,9 +28,21 @@ contract('Court: Staking', ([ pleb, rich ]) => { assertEqualBN(this.anj.balanceOf(rich), INITIAL_BALANCE, 'rich balance') assertEqualBN(this.anj.balanceOf(pleb), 0, 'pleb balance') - this.court = await deployedContract(this.courtFactory.newCourtStaking(this.anj.address), COURT) - assert.equal(await this.court.token(), this.anj.address, 'court token') - assert.equal(await this.court.jurorToken(), this.anj.address, 'court juror token'); + this.court = await CourtMock.new( + 10, + this.anj.address, + ZERO_ADDRESS, // no fees + 0, + 0, + 0, + 0, + 0, + ZERO_ADDRESS, + 1, + 1, + [ 1, 1, 1 ], + 1 + ) }) const assertStaked = async (staker, amount, initialBalance, { recipient, initialStaked = 0 } = {}) => { From 61cfe77bcd2323ca236dba8b6459e26de85cf831 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Sat, 23 Mar 2019 18:21:04 +0100 Subject: [PATCH 24/27] Address review comments --- contracts/Court.sol | 22 +++++++++---------- .../standards/arbitration/Arbitrable.sol | 2 +- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/contracts/Court.sol b/contracts/Court.sol index 551fa476..42445bdd 100644 --- a/contracts/Court.sol +++ b/contracts/Court.sol @@ -150,6 +150,7 @@ contract Court is ERC900, ApproveAndCallFallBack { string internal constant ERROR_CANT_DISMISS_AFTER_DRAFT = "COURT_CANT_DISMISS_AFTER_DRAFT"; string internal constant ERROR_ROUND_ALREADY_DRAFTED = "COURT_ROUND_ALREADY_DRAFTED"; string internal constant ERROR_NOT_DRAFT_TERM = "COURT_NOT_DRAFT_TERM"; + string internal constant ERROR_TERM_RANDOMNESS_UNAVAIL = "COURT_TERM_RANDOMNESS_UNAVAIL"; string internal constant ERROR_INVALID_DISPUTE_STATE = "COURT_INVALID_DISPUTE_STATE"; string internal constant ERROR_INVALID_ADJUDICATION_ROUND = "COURT_INVALID_ADJUDICATION_ROUND"; string internal constant ERROR_INVALID_ADJUDICATION_STATE = "COURT_INVALID_ADJUDICATION_STATE"; @@ -378,8 +379,8 @@ contract Court is ERC900, ApproveAndCallFallBack { uint256 sumTreeId = account.sumTreeId; if (sumTreeId == 0) { - sumTreeId = sumTree.insert(0); - accounts[jurorAddress].sumTreeId = sumTreeId; + sumTreeId = sumTree.insert(0); // Always > 0 (as constructor inserts the first item) + account.sumTreeId = sumTreeId; jurorsByTreeId[sumTreeId] = jurorAddress; } @@ -505,13 +506,11 @@ contract Court is ERC900, ApproveAndCallFallBack { Term storage draftTerm = terms[term]; CourtConfig storage config = courtConfigs[draftTerm.courtConfigId]; // safe to use directly as it is the current term - // TODO: Work on recovery if draft doesn't occur in the term it was supposed to - // it should be scheduled for a future draft and require to pay the heartbeat fee for the term require(round.draftTerm == term, ERROR_NOT_DRAFT_TERM); require(dispute.state == DisputeState.PreDraft, ERROR_ROUND_ALREADY_DRAFTED); + require(draftTerm.randomnessBN >= _blockNumber(), ERROR_TERM_RANDOMNESS_UNAVAIL); if (draftTerm.randomness == bytes32(0)) { - // the blockhash could be 0 if the first dispute draft happens 256 blocks after the term starts draftTerm.randomness = block.blockhash(draftTerm.randomnessBN); } @@ -768,7 +767,7 @@ contract Court is ERC900, ApproveAndCallFallBack { return (_time() - terms[term].startTime) / termDuration; } - function getJurorVote(uint256 _disputeId, uint256 _roundId, uint256 _draftId) public view returns (address juror, uint8 ruling) { + function getJurorVote(uint256 _disputeId, uint256 _roundId, uint256 _draftId) external view returns (address juror, uint8 ruling) { JurorVote storage jurorVote = _getJurorVote(_disputeId, _roundId, _draftId); return (jurorVote.juror, jurorVote.ruling); @@ -782,7 +781,7 @@ contract Court is ERC900, ApproveAndCallFallBack { * @dev Assumes term is up to date */ function feeForJurorDraft(uint64 _draftTerm, uint64 _jurorNumber) public view returns (ERC20 feeToken, uint256 feeAmount, uint16 governanceFeeShare) { - CourtConfig storage fees = courtConfigForTerm(_draftTerm); + CourtConfig storage fees = _courtConfigForTerm(_draftTerm); feeToken = fees.feeToken; governanceFeeShare = fees.governanceFeeShare; @@ -946,7 +945,7 @@ contract Court is ERC900, ApproveAndCallFallBack { return sumTree.randomSortition(uint256(seed)); } - function courtConfigForTerm(uint64 _term) internal view returns (CourtConfig storage) { + function _courtConfigForTerm(uint64 _term) internal view returns (CourtConfig storage) { uint64 feeTerm; if (_term <= term) { @@ -969,11 +968,9 @@ contract Court is ERC900, ApproveAndCallFallBack { uint256 governanceFee = _pct4(_amount, _governanceFeeShare); _assignTokens(_feeToken, _to, _amount - governanceFee); - if (governanceFee == 0) { - return; + if (governanceFee > 0) { + _assignTokens(_feeToken, governor, governanceFee); } - - _assignTokens(_feeToken, governor, governanceFee); } function _processJurorQueues(uint64 _prevTermId, Term storage _incomingTerm) internal { @@ -1025,6 +1022,7 @@ contract Court is ERC900, ApproveAndCallFallBack { AccountUpdate storage update = account.update; if (update.delta > 0) { + // account.sumTreeId always > 0: as only a juror that activates can get in this queue (and gets its sumTreeId) sumTree.update(account.sumTreeId, update.delta, update.positive); delete account.update; } diff --git a/contracts/standards/arbitration/Arbitrable.sol b/contracts/standards/arbitration/Arbitrable.sol index b1f64513..9ba335db 100644 --- a/contracts/standards/arbitration/Arbitrable.sol +++ b/contracts/standards/arbitration/Arbitrable.sol @@ -11,7 +11,7 @@ contract Arbitrable is IArbitrable, ERC165 { bytes4 private constant ARBITRABLE_INTERFACE_ID = 0xabababab; // TODO: interface id string private constant ERROR_NOT_COURT = "ARBITRABLE_NOT_COURT"; - string private constant ERROR_CANNOT_SUBMIT_EVIDENCE = "ARBITRABLE_CANNOT_SUBMIT_EVIDENCE"; + string private constant ERROR_CANNOT_SUBMIT_EVIDENCE = "ARBITRABLE_CANNOT_SUBMIT_EV"; /** @dev Constructor. Choose the arbitrator. * @param _court The address of the court that arbitrates the contract. From c44ba3b75a69c06f53692b005af92bc674acf797 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Mon, 25 Mar 2019 14:25:50 +0100 Subject: [PATCH 25/27] Add readme --- readme.md | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index fdf99bf9..ac93dce1 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,31 @@ -# Aragon Court (Kleros) +# Aragon Court [![Travis branch](https://img.shields.io/travis/aragon/aragon-court/dev.svg?style=for-the-badge)](https://travis-ci.org/aragon/aragon-court/) -**Fork: [kleros/kleros](https://github.com/kleros/kleros)**, see [`kleros` branch](https://github.com/aragon/aragon-court-kleros/tree/kleros) +The Aragon Court is a dispute resolution protocol that runs on Ethereum. It's one of the core components of the [Aragon Network](https://aragon.org/network/). -The Aragon Court for the Aragon Network v1 will be a fork of the [Kleros](https://kleros.io) protocol. +#### 🐲 Project stage: Research and development +The Aragon Court is still in research phase and aspects of the mechanism are still being designed and implemented. The current implementation is rapidly changing and being improved, [see issues](https://github.com/aragon/aragon-court/issues). + +#### 🚨 Security review status: pre-audit +The code in this repo is highly experimental and hasn't undergone a professional security review yet, therefore we cannot recommend using any of the code or deploying the Court at the moment. + +#### 👋 Get started contributing with a [good first issue](https://github.com/aragon/aragon-court/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) +Don't be shy to contribute even the smallest tweak. Everyone will be especially nice and helpful to beginners to help you get started! + +## Description + +**Full description of the mechanism**: [Aragon Forum - Aragon Court v1](TODO) + +The Aragon Court handles subjective disputes that cannot be solved by smart contracts. For this, it employs jurors that need to stake a token to the Court which allows them to get drafted to adjudicate disputes, that can earn them fees. The more tokens a juror has activated, the higher the chance to get drafted and earn more fees. + +The Aragon Court attempts to find what the subjective truth is with a [Schelling game](https://en.wikipedia.org/wiki/Focal_point_(game_theory)). Jurors are asked to vote on the ruling that they think their fellow jurors are more likely to vote on. To incentivize consensus, jurors that don't vote on the consensus ruling have some tokens slashed. Jurors that vote with the consensus ruling are rewarded with ruling fees and juror tokens from the jurors that voted for a minority ruling. + +A design goal of the mechanism is to require very few jurors to adjudicate a dispute and produce a ruling. A small number of jurors is adjudicated by default to a dispute, and their ruling can be appealed in multiple rounds of appeals. + +Even though the Aragon Court could theoretically resolve any type of binary dispute, in its first deployments it will be used to arbitrate **Proposal Agreements.** These agreements require entities creating a proposal in an organization to agree to its specific rules around proposal creation, putting some collateral at stake that could be lost if the Court finds the proposal invalid. + +This first version of the Aragon Court has been heavily inspired by [Kleros protocol](https://github.com/kleros/kleros). + + +## Help shape the Aragon Court +- Discuss in the [Aragon Forum](https://forum.aragon.org/tags/dispute-resolution) +- Join the [#research channel in Aragon Chat](https://aragon.chat/channel/research) \ No newline at end of file From 778ab8b28f93911940152f4925acd57f3ad2f4a1 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Mon, 25 Mar 2019 14:31:05 +0100 Subject: [PATCH 26/27] Improve readme --- readme.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index ac93dce1..d3ce6dd3 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ The Aragon Court is a dispute resolution protocol that runs on Ethereum. It's one of the core components of the [Aragon Network](https://aragon.org/network/). -#### 🐲 Project stage: Research and development +#### 🐲 Project stage: research and development The Aragon Court is still in research phase and aspects of the mechanism are still being designed and implemented. The current implementation is rapidly changing and being improved, [see issues](https://github.com/aragon/aragon-court/issues). #### 🚨 Security review status: pre-audit @@ -11,9 +11,9 @@ The code in this repo is highly experimental and hasn't undergone a professional #### 👋 Get started contributing with a [good first issue](https://github.com/aragon/aragon-court/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) Don't be shy to contribute even the smallest tweak. Everyone will be especially nice and helpful to beginners to help you get started! -## Description +## How does it work -**Full description of the mechanism**: [Aragon Forum - Aragon Court v1](TODO) +**Full description of the mechanism: [Aragon Forum - Aragon Court v1](TODO)** The Aragon Court handles subjective disputes that cannot be solved by smart contracts. For this, it employs jurors that need to stake a token to the Court which allows them to get drafted to adjudicate disputes, that can earn them fees. The more tokens a juror has activated, the higher the chance to get drafted and earn more fees. @@ -28,4 +28,4 @@ This first version of the Aragon Court has been heavily inspired by [Kleros prot ## Help shape the Aragon Court - Discuss in the [Aragon Forum](https://forum.aragon.org/tags/dispute-resolution) -- Join the [#research channel in Aragon Chat](https://aragon.chat/channel/research) \ No newline at end of file +- Join the [#research channel](https://aragon.chat/channel/research) in [Aragon Chat](https://aragon.chat) \ No newline at end of file From 3824223e6b4a64f8ac9189008a70b0025bcd17eb Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Mon, 25 Mar 2019 19:21:59 +0100 Subject: [PATCH 27/27] Add license --- LICENSE | 696 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 696 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f19bea3b --- /dev/null +++ b/LICENSE @@ -0,0 +1,696 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + +---------------------------------------------------------------------------- + +Note: + +Unless otherwise specified, individual files are licensed as GPL-3.0-or-later +and are assumed to include the following text: + + Copyright (C) 2017-2019 Aragon Association + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see .