diff --git a/contracts/src/arbitration/IDisputeKit.sol b/contracts/src/arbitration/IDisputeKit.sol index a1e5eb876..463c561af 100644 --- a/contracts/src/arbitration/IDisputeKit.sol +++ b/contracts/src/arbitration/IDisputeKit.sol @@ -18,6 +18,24 @@ import "./IArbitrator.sol"; * It does not intend to abstract the interactions with the user (such as voting or appeal funding) to allow for implementation-specific parameters. */ interface IDisputeKit { + // ************************************ // + // * Events * // + // ************************************ // + + /** + * @dev Emitted when casting a vote to provide the justification of juror's choice. + * @param _coreDisputeID ID of the dispute in the core contract. + * @param _juror Address of the juror. + * @param _choice The choice juror voted for. + * @param _justification Justification of the choice. + */ + event Justification( + uint256 indexed _coreDisputeID, + address indexed _juror, + uint256 indexed _choice, + string _justification + ); + // ************************************* // // * State Modifiers * // // ************************************* // @@ -58,10 +76,10 @@ interface IDisputeKit { /** @dev Returns the voting data from the most relevant round. * @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit. - * @return winningChoiece The winning choice of this round. + * @return winningChoice The winning choice of this round. * @return tied Whether it's a tie or not. */ - function getLastRoundResult(uint256 _coreDisputeID) external view returns (uint256 winningChoiece, bool tied); + function getLastRoundResult(uint256 _coreDisputeID) external view returns (uint256 winningChoice, bool tied); /** @dev Gets the degree of coherence of a particular voter. This function is called by Kleros Core in order to determine the amount of the reward. * @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit. diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index 349d2928e..bf467b530 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -18,6 +18,7 @@ import {SortitionSumTreeFactory} from "../data-structures/SortitionSumTreeFactor /** * @title KlerosCore * Core arbitrator contract for Kleros v2. + * Note that this contract trusts the token and the dispute kit contracts. */ contract KlerosCore is IArbitrator { using SortitionSumTreeFactory for SortitionSumTreeFactory.SortitionSumTrees; // Use library functions for sortition sum trees. @@ -71,7 +72,7 @@ contract KlerosCore is IArbitrator { } struct Juror { - uint96[] subcourtIDs; // The IDs of subcourts where the juror's stake path ends. A stake path is a path from the forking court to a court the juror directly staked in using `_setStake`. + uint96[] subcourtIDs; // The IDs of subcourts where the juror's stake path ends. A stake path is a path from the general court to a court the juror directly staked in using `_setStake`. mapping(uint96 => uint256) stakedTokens; // The number of tokens the juror has staked in the subcourt in the form `stakedTokens[subcourtID]`. mapping(uint96 => uint256) lockedTokens; // The number of tokens the juror has locked in the subcourt in the form `lockedTokens[subcourtID]`. } @@ -95,7 +96,8 @@ contract KlerosCore is IArbitrator { // * Storage * // // ************************************* // - uint256 public constant FORKING_COURT = 0; // Index of the default court. + uint256 public constant FORKING_COURT = 0; // Index of the forking court. + uint256 public constant GENERAL_COURT = 1; // Index of the default (general) court. uint256 public constant NULL_DISPUTE_KIT = 0; // Null pattern to indicate a top-level DK which has no parent. uint256 public constant DISPUTE_KIT_CLASSIC_INDEX = 1; // Index of the default DK. 0 index is skipped. uint256 public constant MAX_STAKE_PATHS = 4; // The maximum number of stake paths a juror can have. @@ -168,10 +170,10 @@ contract KlerosCore is IArbitrator { * @param _jurorProsecutionModule The address of the juror prosecution module. * @param _disputeKit The address of the default dispute kit. * @param _phaseTimeouts minStakingTime and maxFreezingTime respectively - * @param _hiddenVotes The `hiddenVotes` property value of the forking court. - * @param _courtParameters Numeric parameters of Forking court (minStake, alpha, feeForJuror and jurorsForCourtJump respectively). - * @param _timesPerPeriod The `timesPerPeriod` property value of the forking court. - * @param _sortitionSumTreeK The number of children per node of the forking court's sortition sum tree. + * @param _hiddenVotes The `hiddenVotes` property value of the general court. + * @param _courtParameters Numeric parameters of General court (minStake, alpha, feeForJuror and jurorsForCourtJump respectively). + * @param _timesPerPeriod The `timesPerPeriod` property value of the general court. + * @param _sortitionSumTreeK The number of children per node of the general court's sortition sum tree. */ constructor( address _governor, @@ -204,8 +206,12 @@ contract KlerosCore is IArbitrator { lastPhaseChange = block.timestamp; // Create the Forking court. + courts.push(); + // TODO: fill the properties for Forking court. + + // Create the General court. Court storage court = courts.push(); - court.parent = 0; + court.parent = 1; // TODO: Should the parent for General court be 0 or 1? In the former case the Forking court will become the top court after jumping. court.children = new uint256[](0); court.hiddenVotes = _hiddenVotes; court.minStake = _courtParameters[0]; @@ -215,7 +221,9 @@ contract KlerosCore is IArbitrator { court.timesPerPeriod = _timesPerPeriod; court.supportedDisputeKits[DISPUTE_KIT_CLASSIC_INDEX] = true; - sortitionSumTrees.createTree(bytes32(0), _sortitionSumTreeK); + // TODO: fill the properties for Forking court. + sortitionSumTrees.createTree(bytes32(FORKING_COURT), _sortitionSumTreeK); + sortitionSumTrees.createTree(bytes32(GENERAL_COURT), _sortitionSumTreeK); } // ************************ // @@ -274,16 +282,16 @@ contract KlerosCore is IArbitrator { /** @dev Add a new supported dispute kit module to the court. * @param _disputeKitAddress The address of the dispute kit contract. * @param _parent The ID of the parent dispute kit. It is left empty when root DK is created. - * Note that the root DK must be supported by the forking court. + * Note that the root DK must be supported by the general court. */ function addNewDisputeKit(IDisputeKit _disputeKitAddress, uint256 _parent) external onlyByGovernor { uint256 disputeKitID = disputeKitNodes.length; require(_parent < disputeKitID, "Parent doesn't exist"); uint256 depthLevel; - // Create new tree, which root should be supported by Forking court. + // Create new tree, which root should be supported by General court. if (_parent == NULL_DISPUTE_KIT) { - courts[FORKING_COURT].supportedDisputeKits[disputeKitID] = true; + courts[GENERAL_COURT].supportedDisputeKits[disputeKitID] = true; } else { depthLevel = disputeKitNodes[_parent].depthLevel + 1; // It should be always possible to reach the root from the leaf with the defined number of search iterations. @@ -328,6 +336,7 @@ contract KlerosCore is IArbitrator { "A subcourt cannot be a child of a subcourt with a higher minimum stake." ); require(_supportedDisputeKits.length > 0, "Must support at least one DK"); + require(_parent != FORKING_COURT, "Can't have Forking court as a parent"); uint256 subcourtID = courts.length; Court storage court = courts.push(); @@ -359,7 +368,7 @@ contract KlerosCore is IArbitrator { * @param _minStake The new value for the `minStake` property value. */ function changeSubcourtMinStake(uint96 _subcourtID, uint256 _minStake) external onlyByGovernor { - require(_subcourtID == FORKING_COURT || courts[courts[_subcourtID].parent].minStake <= _minStake); + require(_subcourtID == GENERAL_COURT || courts[courts[_subcourtID].parent].minStake <= _minStake); for (uint256 i = 0; i < courts[_subcourtID].children.length; i++) { require( courts[courts[_subcourtID].children[i]].minStake >= _minStake, @@ -430,8 +439,8 @@ contract KlerosCore is IArbitrator { subcourt.supportedDisputeKits[_disputeKitIDs[i]] = true; } else { require( - !(_subcourtID == FORKING_COURT && disputeKitNodes[_disputeKitIDs[i]].parent == NULL_DISPUTE_KIT), - "Can't remove root DK support from the forking court" + !(_subcourtID == GENERAL_COURT && disputeKitNodes[_disputeKitIDs[i]].parent == NULL_DISPUTE_KIT), + "Can't remove root DK support from the general court" ); subcourt.supportedDisputeKits[_disputeKitIDs[i]] = false; } @@ -674,7 +683,7 @@ contract KlerosCore is IArbitrator { // We didn't find a court that is compatible with DK from this tree, so we jump directly to the top court. // Note that this can only happen when disputeKitID is at its root, and each root DK is supported by the top court by default. if (!courts[newSubcourtID].supportedDisputeKits[newDisputeKitID]) { - newSubcourtID = uint96(FORKING_COURT); + newSubcourtID = uint96(GENERAL_COURT); } if (newSubcourtID != dispute.subcourtID) { @@ -782,7 +791,7 @@ contract KlerosCore is IArbitrator { if (coherentCount == 0) { // No one was coherent. Send the rewards to governor. payable(governor).send(round.totalFeesForJurors); - pinakion.transfer(governor, penaltiesInRoundCache); + safeTransfer(governor, penaltiesInRoundCache); } } } else { @@ -804,12 +813,12 @@ contract KlerosCore is IArbitrator { // Give back the locked tokens in case the juror fully unstaked earlier. if (jurors[account].stakedTokens[dispute.subcourtID] == 0) { uint256 tokenLocked = (round.tokensAtStakePerJuror * degreeOfCoherence) / ALPHA_DIVISOR; - pinakion.transfer(account, tokenLocked); + safeTransfer(account, tokenLocked); } uint256 tokenReward = ((penaltiesInRoundCache / coherentCount) * degreeOfCoherence) / ALPHA_DIVISOR; uint256 ethReward = ((round.totalFeesForJurors / coherentCount) * degreeOfCoherence) / ALPHA_DIVISOR; - pinakion.transfer(account, tokenReward); + safeTransfer(account, tokenReward); payable(account).send(ethReward); emit TokenAndETHShift(account, _disputeID, int256(tokenReward), int256(ethReward)); } @@ -858,8 +867,8 @@ contract KlerosCore is IArbitrator { Court storage court = courts[dispute.subcourtID]; if (round.nbVotes >= court.jurorsForCourtJump) { // Jump to parent subcourt. - if (dispute.subcourtID == FORKING_COURT) { - // Already in the forking court. + if (dispute.subcourtID == GENERAL_COURT) { + // TODO: Handle the forking when appealed in General court. cost = NON_PAYABLE_AMOUNT; // Get the cost of the parent subcourt. } else { cost = courts[court.parent].feeForJuror * ((round.nbVotes * 2) + 1); @@ -1063,8 +1072,7 @@ contract KlerosCore is IArbitrator { uint256 _stake, uint256 _penalty ) internal returns (bool succeeded) { - // Input and transfer checks - if (_subcourtID > courts.length) return false; + if (_subcourtID == FORKING_COURT || _subcourtID > courts.length) return false; Juror storage juror = jurors[_account]; bytes32 stakePathID = accountAndSubcourtIDToStakePathID(_account, _subcourtID); @@ -1091,33 +1099,35 @@ contract KlerosCore is IArbitrator { if (_stake >= currentStake) { transferredAmount = _stake - currentStake; if (transferredAmount > 0) { - // TODO: handle transfer reverts. - if (!pinakion.transferFrom(_account, address(this), transferredAmount)) return false; + if (safeTransferFrom(_account, address(this), transferredAmount)) { + if (currentStake == 0) { + juror.subcourtIDs.push(_subcourtID); + } + } else { + return false; + } } } else if (_stake == 0) { // Keep locked tokens in the contract and release them after dispute is executed. transferredAmount = currentStake - juror.lockedTokens[_subcourtID] - _penalty; if (transferredAmount > 0) { - if (!pinakion.transfer(_account, transferredAmount)) return false; + if (safeTransfer(_account, transferredAmount)) { + for (uint256 i = 0; i < juror.subcourtIDs.length; i++) { + if (juror.subcourtIDs[i] == _subcourtID) { + juror.subcourtIDs[i] = juror.subcourtIDs[juror.subcourtIDs.length - 1]; + juror.subcourtIDs.pop(); + break; + } + } + } else { + return false; + } } } else { transferredAmount = currentStake - _stake - _penalty; if (transferredAmount > 0) { - if (!pinakion.transfer(_account, transferredAmount)) return false; - } - } - - // State update - if (_stake != 0) { - if (currentStake == 0) { - juror.subcourtIDs.push(_subcourtID); - } - } else { - for (uint256 i = 0; i < juror.subcourtIDs.length; i++) { - if (juror.subcourtIDs[i] == _subcourtID) { - juror.subcourtIDs[i] = juror.subcourtIDs[juror.subcourtIDs.length - 1]; - juror.subcourtIDs.pop(); - break; + if (!safeTransfer(_account, transferredAmount)) { + return false; } } } @@ -1131,7 +1141,7 @@ contract KlerosCore is IArbitrator { uint256 currentSubcourtID = _subcourtID; while (!finished) { sortitionSumTrees.set(bytes32(currentSubcourtID), _stake, stakePathID); - if (currentSubcourtID == FORKING_COURT) finished = true; + if (currentSubcourtID == GENERAL_COURT) finished = true; else currentSubcourtID = courts[currentSubcourtID].parent; } @@ -1164,8 +1174,8 @@ contract KlerosCore is IArbitrator { minJurors := mload(add(_extraData, 0x40)) disputeKitID := mload(add(_extraData, 0x60)) } - if (subcourtID >= courts.length) { - subcourtID = uint96(FORKING_COURT); + if (subcourtID == FORKING_COURT || subcourtID >= courts.length) { + subcourtID = uint96(GENERAL_COURT); } if (minJurors == 0) { minJurors = MIN_JURORS; @@ -1174,7 +1184,7 @@ contract KlerosCore is IArbitrator { disputeKitID = DISPUTE_KIT_CLASSIC_INDEX; // 0 index is not used. } } else { - subcourtID = uint96(FORKING_COURT); + subcourtID = uint96(GENERAL_COURT); minJurors = MIN_JURORS; disputeKitID = DISPUTE_KIT_CLASSIC_INDEX; } @@ -1210,4 +1220,33 @@ contract KlerosCore is IArbitrator { stakePathID := mload(ptr) } } + + /** @dev Calls transfer() without reverting. + * @param _to Recepient address. + * @param _value Amount transferred. + * @return Whether transfer succeeded or not. + */ + function safeTransfer(address _to, uint256 _value) internal returns (bool) { + (bool success, bytes memory data) = address(pinakion).call( + abi.encodeWithSelector(IERC20.transfer.selector, _to, _value) + ); + return (success && (data.length == 0 || abi.decode(data, (bool)))); + } + + /** @dev Calls transferFrom() without reverting. + * @param _from Sender address. + * @param _to Recepient address. + * @param _value Amount transferred. + * @return Whether transfer succeeded or not. + */ + function safeTransferFrom( + address _from, + address _to, + uint256 _value + ) internal returns (bool) { + (bool success, bytes memory data) = address(pinakion).call( + abi.encodeWithSelector(IERC20.transferFrom.selector, _from, _to, _value) + ); + return (success && (data.length == 0 || abi.decode(data, (bool)))); + } } diff --git a/contracts/src/arbitration/dispute-kits/BaseDisputeKit.sol b/contracts/src/arbitration/dispute-kits/BaseDisputeKit.sol index 9f56db46b..ce3190a65 100644 --- a/contracts/src/arbitration/dispute-kits/BaseDisputeKit.sol +++ b/contracts/src/arbitration/dispute-kits/BaseDisputeKit.sol @@ -67,8 +67,9 @@ abstract contract BaseDisputeKit is IDisputeKit { } /** @dev Checks that the chosen address satisfies certain conditions for being drawn. - * @param _disputeID ID of the dispute in the core contract. + * @param _coreDisputeID ID of the dispute in the core contract. * @param _juror Chosen address. + * @return Whether the address can be drawn or not. */ - function postDrawCheck(uint256 _disputeID, address _juror) internal virtual returns (bool); + function postDrawCheck(uint256 _coreDisputeID, address _juror) internal virtual returns (bool); } diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol index 3929be284..185b6663b 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol @@ -85,22 +85,22 @@ contract DisputeKitClassic is BaseDisputeKit, IEvidence { // ************************************* // event Contribution( - uint256 indexed _disputeID, - uint256 indexed _round, + uint256 indexed _coreDisputeID, + uint256 indexed _coreRoundID, uint256 _choice, address indexed _contributor, uint256 _amount ); event Withdrawal( - uint256 indexed _disputeID, - uint256 indexed _round, + uint256 indexed _coreDisputeID, + uint256 indexed _coreRoundID, uint256 _choice, address indexed _contributor, uint256 _amount ); - event ChoiceFunded(uint256 indexed _disputeID, uint256 indexed _round, uint256 indexed _choice); + event ChoiceFunded(uint256 indexed _coreDisputeID, uint256 indexed _coreRoundID, uint256 indexed _choice); event NewPhaseDisputeKit(Phase _phase); // ************************************* // @@ -272,7 +272,7 @@ contract DisputeKitClassic is BaseDisputeKit, IEvidence { * `n` is the number of votes. * @param _coreDisputeID The ID of the dispute in Kleros Core. * @param _voteIDs The IDs of the votes. - * @param _commit The commit. + * @param _commit The commit. Note that justification string is a part of the commit. */ function castCommit( uint256 _coreDisputeID, @@ -302,12 +302,14 @@ contract DisputeKitClassic is BaseDisputeKit, IEvidence { * @param _voteIDs The IDs of the votes. * @param _choice The choice. * @param _salt The salt for the commit if the votes were hidden. + * @param _justification Justification of the choice. */ function castVote( uint256 _coreDisputeID, uint256[] calldata _voteIDs, uint256 _choice, - uint256 _salt + uint256 _salt, + string memory _justification ) external notJumped(_coreDisputeID) { require( core.getCurrentPeriod(_coreDisputeID) == KlerosCore.Period.vote, @@ -325,7 +327,8 @@ contract DisputeKitClassic is BaseDisputeKit, IEvidence { for (uint256 i = 0; i < _voteIDs.length; i++) { require(round.votes[_voteIDs[i]].account == msg.sender, "The caller has to own the vote."); require( - !hiddenVotes || round.votes[_voteIDs[i]].commit == keccak256(abi.encodePacked(_choice, _salt)), + !hiddenVotes || + round.votes[_voteIDs[i]].commit == keccak256(abi.encodePacked(_choice, _justification, _salt)), "The commit must match the choice in subcourts with hidden votes." ); require(!round.votes[_voteIDs[i]].voted, "Vote already cast."); @@ -349,6 +352,7 @@ contract DisputeKitClassic is BaseDisputeKit, IEvidence { round.tied = false; } } + emit Justification(_coreDisputeID, msg.sender, _choice, _justification); } /** @dev Manages contributions, and appeals a dispute if at least two choices are fully funded. @@ -376,6 +380,8 @@ contract DisputeKitClassic is BaseDisputeKit, IEvidence { } Round storage round = dispute.rounds[dispute.rounds.length - 1]; + uint256 coreRoundID = core.getNumberOfRounds(_coreDisputeID) - 1; + require(!round.hasPaid[_choice], "Appeal fee is already paid."); uint256 appealCost = core.appealCost(_coreDisputeID); uint256 totalCost = appealCost + (appealCost * multiplier) / ONE_BASIS_POINT; @@ -386,7 +392,7 @@ contract DisputeKitClassic is BaseDisputeKit, IEvidence { contribution = totalCost - round.paidFees[_choice] > msg.value // Overflows and underflows will be managed on the compiler level. ? msg.value : totalCost - round.paidFees[_choice]; - emit Contribution(_coreDisputeID, dispute.rounds.length - 1, _choice, msg.sender, contribution); + emit Contribution(_coreDisputeID, coreRoundID, _choice, msg.sender, contribution); } round.contributions[msg.sender][_choice] += contribution; @@ -395,7 +401,7 @@ contract DisputeKitClassic is BaseDisputeKit, IEvidence { round.feeRewards += round.paidFees[_choice]; round.fundedChoices.push(_choice); round.hasPaid[_choice] = true; - emit ChoiceFunded(_coreDisputeID, dispute.rounds.length - 1, _choice); + emit ChoiceFunded(_coreDisputeID, coreRoundID, _choice); } if (round.fundedChoices.length > 1) { @@ -407,7 +413,7 @@ contract DisputeKitClassic is BaseDisputeKit, IEvidence { dispute.jumped = true; } else { // Don't subtract 1 from length since both round arrays haven't been updated yet. - dispute.coreRoundIDToLocal[core.getNumberOfRounds(_coreDisputeID)] = dispute.rounds.length; + dispute.coreRoundIDToLocal[coreRoundID + 1] = dispute.rounds.length; Round storage newRound = dispute.rounds.push(); newRound.nbVotes = core.getNumberOfVotes(_coreDisputeID); @@ -423,20 +429,20 @@ contract DisputeKitClassic is BaseDisputeKit, IEvidence { /** @dev Allows those contributors who attempted to fund an appeal round to withdraw any reimbursable fees or rewards after the dispute gets resolved. * @param _coreDisputeID Index of the dispute in Kleros Core contract. * @param _beneficiary The address whose rewards to withdraw. - * @param _round The round the caller wants to withdraw from. + * @param _coreRoundID The round in the Kleros Core contract the caller wants to withdraw from. * @param _choice The ruling option that the caller wants to withdraw from. * @return amount The withdrawn amount. */ function withdrawFeesAndRewards( uint256 _coreDisputeID, address payable _beneficiary, - uint256 _round, + uint256 _coreRoundID, uint256 _choice ) external returns (uint256 amount) { require(core.isRuled(_coreDisputeID), "Dispute should be resolved."); Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[_round]; + Round storage round = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; uint256 finalRuling = core.currentRuling(_coreDisputeID); if (!round.hasPaid[_choice]) { @@ -460,7 +466,7 @@ contract DisputeKitClassic is BaseDisputeKit, IEvidence { if (amount != 0) { _beneficiary.send(amount); // Deliberate use of send to prevent reverting fallback. It's the user's responsibility to accept ETH. - emit Withdrawal(_coreDisputeID, _round, _choice, _beneficiary, amount); + emit Withdrawal(_coreDisputeID, _coreRoundID, _choice, _beneficiary, amount); } } @@ -490,7 +496,7 @@ contract DisputeKitClassic is BaseDisputeKit, IEvidence { external view override - returns (uint256 winningChoiece, bool tied) + returns (uint256 winningChoice, bool tied) { Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; Round storage lastRound = dispute.rounds[dispute.rounds.length - 1]; @@ -632,6 +638,11 @@ contract DisputeKitClassic is BaseDisputeKit, IEvidence { // * Internal * // // ************************************* // + /** @dev Checks that the chosen address satisfies certain conditions for being drawn. + * @param _coreDisputeID ID of the dispute in the core contract. + * @param _juror Chosen address. + * @return Whether the address can be drawn or not. + */ function postDrawCheck(uint256 _coreDisputeID, address _juror) internal view override returns (bool) { uint256 subcourtID = core.getSubcourtID(_coreDisputeID); (uint256 lockedAmountPerJuror, , , , , ) = core.getRoundInfo( diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol index 990b50029..a81418639 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol @@ -62,6 +62,7 @@ contract DisputeKitSybilResistant is BaseDisputeKit, IEvidence { uint256 feeRewards; // Sum of reimbursable appeal fees available to the parties that made contributions to the ruling that ultimately wins a dispute. uint256[] fundedChoices; // Stores the choices that are fully funded. uint256 nbVotes; // Maximal number of votes this dispute can get. + mapping(address => bool) alreadyDrawn; // Set to 'true' if the address has already been drawn, so it can't be drawn more than once. } struct Vote { @@ -94,22 +95,22 @@ contract DisputeKitSybilResistant is BaseDisputeKit, IEvidence { // ************************************* // event Contribution( - uint256 indexed _disputeID, - uint256 indexed _round, + uint256 indexed _coreDisputeID, + uint256 indexed _coreRoundID, uint256 _choice, address indexed _contributor, uint256 _amount ); event Withdrawal( - uint256 indexed _disputeID, - uint256 indexed _round, + uint256 indexed _coreDisputeID, + uint256 indexed _coreRoundID, uint256 _choice, address indexed _contributor, uint256 _amount ); - event ChoiceFunded(uint256 indexed _disputeID, uint256 indexed _round, uint256 indexed _choice); + event ChoiceFunded(uint256 indexed _coreDisputeID, uint256 indexed _coreRoundID, uint256 indexed _choice); event NewPhaseDisputeKit(Phase _phase); // ************************************* // @@ -275,11 +276,9 @@ contract DisputeKitSybilResistant is BaseDisputeKit, IEvidence { bytes32 ID = core.getSortitionSumTreeID(key, treeIndex); drawnAddress = stakePathIDToAccount(ID); - // TODO: deduplicate the list of all the drawn humans before moving to the next period !! - if (!proofOfHumanity(drawnAddress)) drawnAddress = address(0); - - if (postDrawCheck(_coreDisputeID, drawnAddress)) { + if (postDrawCheck(_coreDisputeID, drawnAddress) && !round.alreadyDrawn[drawnAddress]) { round.votes.push(Vote({account: drawnAddress, commit: bytes32(0), choice: 0, voted: false})); + round.alreadyDrawn[drawnAddress] = true; if (round.votes.length == round.nbVotes) { disputesWithoutJurors--; } @@ -293,7 +292,7 @@ contract DisputeKitSybilResistant is BaseDisputeKit, IEvidence { * `n` is the number of votes. * @param _coreDisputeID The ID of the dispute in Kleros Core. * @param _voteIDs The IDs of the votes. - * @param _commit The commit. + * @param _commit The commit. Note that justification string is a part of the commit. */ function castCommit( uint256 _coreDisputeID, @@ -323,12 +322,14 @@ contract DisputeKitSybilResistant is BaseDisputeKit, IEvidence { * @param _voteIDs The IDs of the votes. * @param _choice The choice. * @param _salt The salt for the commit if the votes were hidden. + * @param _justification Justification of the choice. */ function castVote( uint256 _coreDisputeID, uint256[] calldata _voteIDs, uint256 _choice, - uint256 _salt + uint256 _salt, + string calldata _justification ) external notJumped(_coreDisputeID) { require( core.getCurrentPeriod(_coreDisputeID) == KlerosCore.Period.vote, @@ -346,7 +347,8 @@ contract DisputeKitSybilResistant is BaseDisputeKit, IEvidence { for (uint256 i = 0; i < _voteIDs.length; i++) { require(round.votes[_voteIDs[i]].account == msg.sender, "The caller has to own the vote."); require( - !hiddenVotes || round.votes[_voteIDs[i]].commit == keccak256(abi.encodePacked(_choice, _salt)), + !hiddenVotes || + round.votes[_voteIDs[i]].commit == keccak256(abi.encodePacked(_choice, _justification, _salt)), "The commit must match the choice in subcourts with hidden votes." ); require(!round.votes[_voteIDs[i]].voted, "Vote already cast."); @@ -370,6 +372,7 @@ contract DisputeKitSybilResistant is BaseDisputeKit, IEvidence { round.tied = false; } } + emit Justification(_coreDisputeID, msg.sender, _choice, _justification); } /** @dev Manages contributions, and appeals a dispute if at least two choices are fully funded. @@ -397,6 +400,8 @@ contract DisputeKitSybilResistant is BaseDisputeKit, IEvidence { } Round storage round = dispute.rounds[dispute.rounds.length - 1]; + uint256 coreRoundID = core.getNumberOfRounds(_coreDisputeID) - 1; + require(!round.hasPaid[_choice], "Appeal fee is already paid."); uint256 appealCost = core.appealCost(_coreDisputeID); uint256 totalCost = appealCost + (appealCost * multiplier) / ONE_BASIS_POINT; @@ -407,7 +412,7 @@ contract DisputeKitSybilResistant is BaseDisputeKit, IEvidence { contribution = totalCost - round.paidFees[_choice] > msg.value // Overflows and underflows will be managed on the compiler level. ? msg.value : totalCost - round.paidFees[_choice]; - emit Contribution(_coreDisputeID, dispute.rounds.length - 1, _choice, msg.sender, contribution); + emit Contribution(_coreDisputeID, coreRoundID, _choice, msg.sender, contribution); } round.contributions[msg.sender][_choice] += contribution; @@ -416,7 +421,7 @@ contract DisputeKitSybilResistant is BaseDisputeKit, IEvidence { round.feeRewards += round.paidFees[_choice]; round.fundedChoices.push(_choice); round.hasPaid[_choice] = true; - emit ChoiceFunded(_coreDisputeID, dispute.rounds.length - 1, _choice); + emit ChoiceFunded(_coreDisputeID, coreRoundID, _choice); } if (round.fundedChoices.length > 1) { @@ -428,7 +433,7 @@ contract DisputeKitSybilResistant is BaseDisputeKit, IEvidence { dispute.jumped = true; } else { // Don't subtract 1 from length since both round arrays haven't been updated yet. - dispute.coreRoundIDToLocal[core.getNumberOfRounds(_coreDisputeID)] = dispute.rounds.length; + dispute.coreRoundIDToLocal[coreRoundID + 1] = dispute.rounds.length; Round storage newRound = dispute.rounds.push(); newRound.nbVotes = core.getNumberOfVotes(_coreDisputeID); @@ -444,20 +449,20 @@ contract DisputeKitSybilResistant is BaseDisputeKit, IEvidence { /** @dev Allows those contributors who attempted to fund an appeal round to withdraw any reimbursable fees or rewards after the dispute gets resolved. * @param _coreDisputeID Index of the dispute in Kleros Core contract. * @param _beneficiary The address whose rewards to withdraw. - * @param _round The round the caller wants to withdraw from. + * @param _coreRoundID The round in the Kleros Core contract the caller wants to withdraw from. * @param _choice The ruling option that the caller wants to withdraw from. * @return amount The withdrawn amount. */ function withdrawFeesAndRewards( uint256 _coreDisputeID, address payable _beneficiary, - uint256 _round, + uint256 _coreRoundID, uint256 _choice ) external returns (uint256 amount) { require(core.isRuled(_coreDisputeID), "Dispute should be resolved."); Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[_round]; + Round storage round = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; uint256 finalRuling = core.currentRuling(_coreDisputeID); if (!round.hasPaid[_choice]) { @@ -481,7 +486,7 @@ contract DisputeKitSybilResistant is BaseDisputeKit, IEvidence { if (amount != 0) { _beneficiary.send(amount); // Deliberate use of send to prevent reverting fallback. It's the user's responsibility to accept ETH. - emit Withdrawal(_coreDisputeID, _round, _choice, _beneficiary, amount); + emit Withdrawal(_coreDisputeID, _coreRoundID, _choice, _beneficiary, amount); } } @@ -511,7 +516,7 @@ contract DisputeKitSybilResistant is BaseDisputeKit, IEvidence { external view override - returns (uint256 winningChoiece, bool tied) + returns (uint256 winningChoice, bool tied) { Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; Round storage lastRound = dispute.rounds[dispute.rounds.length - 1]; @@ -653,6 +658,11 @@ contract DisputeKitSybilResistant is BaseDisputeKit, IEvidence { // * Internal * // // ************************************* // + /** @dev Checks that the chosen address satisfies certain conditions for being drawn. + * @param _coreDisputeID ID of the dispute in the core contract. + * @param _juror Chosen address. + * @return Whether the address can be drawn or not. + */ function postDrawCheck(uint256 _coreDisputeID, address _juror) internal view override returns (bool) { uint256 subcourtID = core.getSubcourtID(_coreDisputeID); (uint256 lockedAmountPerJuror, , , , , ) = core.getRoundInfo( @@ -660,7 +670,11 @@ contract DisputeKitSybilResistant is BaseDisputeKit, IEvidence { core.getNumberOfRounds(_coreDisputeID) - 1 ); (uint256 stakedTokens, uint256 lockedTokens) = core.getJurorBalance(_juror, uint96(subcourtID)); - return stakedTokens >= lockedTokens + lockedAmountPerJuror; + if (stakedTokens < lockedTokens + lockedAmountPerJuror) { + return false; + } else { + return proofOfHumanity(_juror); + } } /** @dev Checks if an address belongs to the Proof of Humanity registry. diff --git a/contracts/test/integration/index.ts b/contracts/test/integration/index.ts index 4fbbc0bd9..d71d1a383 100644 --- a/contracts/test/integration/index.ts +++ b/contracts/test/integration/index.ts @@ -87,29 +87,29 @@ describe("Demo pre-alpha1", function () { await pnk.approve(core.address, ONE_THOUSAND_PNK.mul(100)); - await core.setStake(0, ONE_THOUSAND_PNK); - await core.getJurorBalance(deployer, 0).then((result) => { + await core.setStake(1, ONE_THOUSAND_PNK); + await core.getJurorBalance(deployer, 1).then((result) => { expect(result.staked).to.equal(ONE_THOUSAND_PNK); expect(result.locked).to.equal(0); logJurorBalance(result); }); - await core.setStake(0, ONE_HUNDRED_PNK.mul(5)); - await core.getJurorBalance(deployer, 0).then((result) => { + await core.setStake(1, ONE_HUNDRED_PNK.mul(5)); + await core.getJurorBalance(deployer, 1).then((result) => { expect(result.staked).to.equal(ONE_HUNDRED_PNK.mul(5)); expect(result.locked).to.equal(0); logJurorBalance(result); }); - await core.setStake(0, 0); - await core.getJurorBalance(deployer, 0).then((result) => { + await core.setStake(1, 0); + await core.getJurorBalance(deployer, 1).then((result) => { expect(result.staked).to.equal(0); expect(result.locked).to.equal(0); logJurorBalance(result); }); - await core.setStake(0, ONE_THOUSAND_PNK.mul(4)); - await core.getJurorBalance(deployer, 0).then((result) => { + await core.setStake(1, ONE_THOUSAND_PNK.mul(4)); + await core.getJurorBalance(deployer, 1).then((result) => { expect(result.staked).to.equal(ONE_THOUSAND_PNK.mul(4)); expect(result.locked).to.equal(0); logJurorBalance(result); @@ -141,7 +141,6 @@ describe("Demo pre-alpha1", function () { await network.provider.send("evm_increaseTime", [130]); // Wait for minStakingTime await network.provider.send("evm_mine"); - expect(await core.phase()).to.equal(Phase.staking); expect(await disputeKit.phase()).to.equal(DisputeKitPhase.resolving); expect(await disputeKit.disputesWithoutJurors()).to.equal(1); @@ -176,14 +175,13 @@ describe("Demo pre-alpha1", function () { await core.passPeriod(0); expect((await core.disputes(0)).period).to.equal(Period.vote); - await disputeKit.connect(await ethers.getSigner(deployer)).castVote(0, [0, 1, 2], 0, 0); + await disputeKit.connect(await ethers.getSigner(deployer)).castVote(0, [0, 1, 2], 0, 0, ""); await core.passPeriod(0); await core.passPeriod(0); expect((await core.disputes(0)).period).to.equal(Period.execution); await core.execute(0, 0, 1000); const ticket1 = await fastBridgeSender.currentTicketID(); expect(ticket1).to.equal(1); - const tx4 = await core.executeRuling(0); expect(tx4).to.emit(fastBridgeSender, "OutgoingMessage"); @@ -225,29 +223,29 @@ describe("Demo pre-alpha1", function () { console.log("KC phase: %d, DK phase: ", await core.phase(), await disputeKit.phase()); - await core.setStake(0, ONE_THOUSAND_PNK); - await core.getJurorBalance(deployer, 0).then((result) => { + await core.setStake(1, ONE_THOUSAND_PNK); + await core.getJurorBalance(deployer, 1).then((result) => { expect(result.staked).to.equal(ONE_THOUSAND_PNK); expect(result.locked).to.equal(0); logJurorBalance(result); }); - await core.setStake(0, ONE_HUNDRED_PNK.mul(5)); - await core.getJurorBalance(deployer, 0).then((result) => { + await core.setStake(1, ONE_HUNDRED_PNK.mul(5)); + await core.getJurorBalance(deployer, 1).then((result) => { expect(result.staked).to.equal(ONE_HUNDRED_PNK.mul(5)); expect(result.locked).to.equal(0); logJurorBalance(result); }); - await core.setStake(0, 0); - await core.getJurorBalance(deployer, 0).then((result) => { + await core.setStake(1, 0); + await core.getJurorBalance(deployer, 1).then((result) => { expect(result.staked).to.equal(0); expect(result.locked).to.equal(0); logJurorBalance(result); }); - await core.setStake(0, ONE_THOUSAND_PNK.mul(4)); - await core.getJurorBalance(deployer, 0).then((result) => { + await core.setStake(1, ONE_THOUSAND_PNK.mul(4)); + await core.getJurorBalance(deployer, 1).then((result) => { expect(result.staked).to.equal(ONE_THOUSAND_PNK.mul(4)); expect(result.locked).to.equal(0); logJurorBalance(result); @@ -342,7 +340,7 @@ describe("Demo pre-alpha1", function () { console.log("KC phase: %d, DK phase: ", await core.phase(), await disputeKit.phase()); - await disputeKit.connect(await ethers.getSigner(deployer)).castVote(0, [0, 1, 2], 0, 0); + await disputeKit.connect(await ethers.getSigner(deployer)).castVote(0, [0, 1, 2], 0, 0, ""); await core.passPeriod(0); await core.passPeriod(0); expect((await core.disputes(0)).period).to.equal(Period.execution); @@ -419,29 +417,29 @@ describe("Demo pre-alpha1", function () { await pnk.approve(core.address, ONE_THOUSAND_PNK.mul(100)); - await core.setStake(0, ONE_THOUSAND_PNK); - await core.getJurorBalance(deployer, 0).then((result) => { + await core.setStake(1, ONE_THOUSAND_PNK); + await core.getJurorBalance(deployer, 1).then((result) => { expect(result.staked).to.equal(ONE_THOUSAND_PNK); expect(result.locked).to.equal(0); logJurorBalance(result); }); - await core.setStake(0, ONE_HUNDRED_PNK.mul(5)); - await core.getJurorBalance(deployer, 0).then((result) => { + await core.setStake(1, ONE_HUNDRED_PNK.mul(5)); + await core.getJurorBalance(deployer, 1).then((result) => { expect(result.staked).to.equal(ONE_HUNDRED_PNK.mul(5)); expect(result.locked).to.equal(0); logJurorBalance(result); }); - await core.setStake(0, 0); - await core.getJurorBalance(deployer, 0).then((result) => { + await core.setStake(1, 0); + await core.getJurorBalance(deployer, 1).then((result) => { expect(result.staked).to.equal(0); expect(result.locked).to.equal(0); logJurorBalance(result); }); - await core.setStake(0, ONE_THOUSAND_PNK.mul(4)); - await core.getJurorBalance(deployer, 0).then((result) => { + await core.setStake(1, ONE_THOUSAND_PNK.mul(4)); + await core.getJurorBalance(deployer, 1).then((result) => { expect(result.staked).to.equal(ONE_THOUSAND_PNK.mul(4)); expect(result.locked).to.equal(0); logJurorBalance(result); @@ -507,7 +505,7 @@ describe("Demo pre-alpha1", function () { await core.passPeriod(coreId); expect((await core.disputes(coreId)).period).to.equal(Period.vote); - await disputeKit.connect(await ethers.getSigner(deployer)).castVote(coreId, [0, 1, 2], 0, 0); + await disputeKit.connect(await ethers.getSigner(deployer)).castVote(coreId, [0, 1, 2], 0, 0, ""); await core.passPeriod(coreId); await core.passPeriod(coreId); expect((await core.disputes(coreId)).period).to.equal(Period.execution);