From 9303c625feabf09ae821246fd08901fccf1f9f7f Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 18 Dec 2024 17:05:59 +0000 Subject: [PATCH] feat: gated dispute kit --- .../dispute-kits/DisputeKitGated.sol | 639 ++++++++++++++++++ 1 file changed, 639 insertions(+) create mode 100644 contracts/src/arbitration/dispute-kits/DisputeKitGated.sol diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol new file mode 100644 index 000000000..c33bd0f13 --- /dev/null +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol @@ -0,0 +1,639 @@ +// SPDX-License-Identifier: MIT + +/// @custom:authors: [@unknownunknown1, @jaybuidl] +/// @custom:reviewers: [] +/// @custom:auditors: [] +/// @custom:bounties: [] +/// @custom:deployments: [] + +pragma solidity 0.8.24; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +import "../KlerosCore.sol"; +import "../interfaces/IDisputeKit.sol"; +import "../../proxy/UUPSProxiable.sol"; +import "../../proxy/Initializable.sol"; + +interface IToken { + /// @dev Returns the number of tokens in `owner` account. + /// @param owner The address of the owner. + /// @return balance The number of tokens in `owner` account. + function balanceOf(address owner) external view returns (uint256 balance); +} + +/// @title DisputeKitGated +/// Dispute kit implementation adapted from DisputeKitClassic +/// - a drawing system: proportional to staked PNK with a non-zero balance of `tokenGate`, +/// - a vote aggregation system: plurality, +/// - an incentive system: equal split between coherent votes, +/// - an appeal system: fund 2 choices only, vote on any choice. +contract DisputeKitGated is IDisputeKit, Initializable, UUPSProxiable { + // ************************************* // + // * Structs * // + // ************************************* // + + struct Dispute { + Round[] rounds; // Rounds of the dispute. 0 is the default round, and [1, ..n] are the appeal rounds. + uint256 numberOfChoices; // The number of choices jurors have when voting. This does not include choice `0` which is reserved for "refuse to arbitrate". + bool jumped; // True if dispute jumped to a parent dispute kit and won't be handled by this DK anymore. + mapping(uint256 => uint256) coreRoundIDToLocal; // Maps id of the round in the core contract to the index of the round of related local dispute. + bytes extraData; // Extradata for the dispute. + } + + struct Round { + Vote[] votes; // Former votes[_appeal][]. + uint256 winningChoice; // The choice with the most votes. Note that in the case of a tie, it is the choice that reached the tied number of votes first. + mapping(uint256 => uint256) counts; // The sum of votes for each choice in the form `counts[choice]`. + bool tied; // True if there is a tie, false otherwise. + uint256 totalVoted; // Former uint[_appeal] votesInEachRound. + uint256 totalCommitted; // Former commitsInRound. + mapping(uint256 choiceId => uint256) paidFees; // Tracks the fees paid for each choice in this round. + mapping(uint256 choiceId => bool) hasPaid; // True if this choice was fully funded, false otherwise. + mapping(address account => mapping(uint256 choiceId => uint256)) contributions; // Maps contributors to their contributions for each choice. + 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. + } + + struct Vote { + address account; // The address of the juror. + bytes32 commit; // The commit of the juror. For courts with hidden votes. + uint256 choice; // The choice of the juror. + bool voted; // True if the vote has been cast. + } + + // ************************************* // + // * Storage * // + // ************************************* // + + uint256 public constant WINNER_STAKE_MULTIPLIER = 10000; // Multiplier of the appeal cost that the winner has to pay as fee stake for a round in basis points. Default is 1x of appeal fee. + uint256 public constant LOSER_STAKE_MULTIPLIER = 20000; // Multiplier of the appeal cost that the loser has to pay as fee stake for a round in basis points. Default is 2x of appeal fee. + uint256 public constant LOSER_APPEAL_PERIOD_MULTIPLIER = 5000; // Multiplier of the appeal period for the choice that wasn't voted for in the previous round, in basis points. Default is 1/2 of original appeal period. + uint256 public constant ONE_BASIS_POINT = 10000; // One basis point, for scaling. + + address public governor; // The governor of the contract. + KlerosCore public core; // The Kleros Core arbitrator + Dispute[] public disputes; // Array of the locally created disputes. + mapping(uint256 => uint256) public coreDisputeIDToLocal; // Maps the dispute ID in Kleros Core to the local dispute ID. + IToken public tokenGate; // The token used for gating access. + + // ************************************* // + // * Events * // + // ************************************* // + + /// @dev To be emitted when a dispute is created. + /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. + /// @param _numberOfChoices The number of choices available in the dispute. + /// @param _extraData The extra data for the dispute. + event DisputeCreation(uint256 indexed _coreDisputeID, uint256 _numberOfChoices, bytes _extraData); + + /// @dev To be emitted when a vote commitment is cast. + /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. + /// @param _juror The address of the juror casting the vote commitment. + /// @param _voteIDs The identifiers of the votes in the dispute. + /// @param _commit The commitment of the juror. + event CommitCast(uint256 indexed _coreDisputeID, address indexed _juror, uint256[] _voteIDs, bytes32 _commit); + + /// @dev To be emitted when a funding contribution is made. + /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. + /// @param _coreRoundID The identifier of the round in the Arbitrator contract. + /// @param _choice The choice that is being funded. + /// @param _contributor The address of the contributor. + /// @param _amount The amount contributed. + event Contribution( + uint256 indexed _coreDisputeID, + uint256 indexed _coreRoundID, + uint256 _choice, + address indexed _contributor, + uint256 _amount + ); + + /// @dev To be emitted when the contributed funds are withdrawn. + /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. + /// @param _coreRoundID The identifier of the round in the Arbitrator contract. + /// @param _choice The choice that is being funded. + /// @param _contributor The address of the contributor. + /// @param _amount The amount withdrawn. + event Withdrawal( + uint256 indexed _coreDisputeID, + uint256 indexed _coreRoundID, + uint256 _choice, + address indexed _contributor, + uint256 _amount + ); + + /// @dev To be emitted when a choice is fully funded for an appeal. + /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. + /// @param _coreRoundID The identifier of the round in the Arbitrator contract. + /// @param _choice The choice that is being funded. + event ChoiceFunded(uint256 indexed _coreDisputeID, uint256 indexed _coreRoundID, uint256 indexed _choice); + + // ************************************* // + // * Modifiers * // + // ************************************* // + + modifier onlyByGovernor() { + require(governor == msg.sender, "Access not allowed: Governor only."); + _; + } + + modifier onlyByCore() { + require(address(core) == msg.sender, "Access not allowed: KlerosCore only."); + _; + } + + modifier notJumped(uint256 _coreDisputeID) { + require(!disputes[coreDisputeIDToLocal[_coreDisputeID]].jumped, "Dispute jumped to a parent DK!"); + _; + } + + // ************************************* // + // * Constructor * // + // ************************************* // + + /// @dev Constructor, initializing the implementation to reduce attack surface. + constructor() { + _disableInitializers(); + } + + /// @dev Initializer. + /// @param _governor The governor's address. + /// @param _core The KlerosCore arbitrator. + /// @param _tokenGate The token used for gating access. + function initialize(address _governor, KlerosCore _core, IToken _tokenGate) external reinitializer(1) { + governor = _governor; + core = _core; + tokenGate = _tokenGate; + } + + // ************************ // + // * Governance * // + // ************************ // + + /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) + /// Only the governor can perform upgrades (`onlyByGovernor`) + function _authorizeUpgrade(address) internal view override onlyByGovernor { + // NOP + } + + /// @dev Allows the governor to call anything on behalf of the contract. + /// @param _destination The destination of the call. + /// @param _amount The value sent with the call. + /// @param _data The data sent with the call. + function executeGovernorProposal( + address _destination, + uint256 _amount, + bytes memory _data + ) external onlyByGovernor { + (bool success, ) = _destination.call{value: _amount}(_data); + require(success, "Unsuccessful call"); + } + + /// @dev Changes the `governor` storage variable. + /// @param _governor The new value for the `governor` storage variable. + function changeGovernor(address payable _governor) external onlyByGovernor { + governor = _governor; + } + + /// @dev Changes the `core` storage variable. + /// @param _core The new value for the `core` storage variable. + function changeCore(address _core) external onlyByGovernor { + core = KlerosCore(_core); + } + + /// @dev Changes the `tokenGate` storage variable. + /// @param _tokenGate The new value for the `tokenGate` storage variable. + function changeTokenGate(address _tokenGate) external onlyByGovernor { + tokenGate = IToken(_tokenGate); + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @dev Creates a local dispute and maps it to the dispute ID in the Core contract. + /// Note: Access restricted to Kleros Core only. + /// @param _coreDisputeID The ID of the dispute in Kleros Core. + /// @param _numberOfChoices Number of choices of the dispute + /// @param _extraData Additional info about the dispute, for possible use in future dispute kits. + /// @param _nbVotes Number of votes for this dispute. + function createDispute( + uint256 _coreDisputeID, + uint256 _numberOfChoices, + bytes calldata _extraData, + uint256 _nbVotes + ) external override onlyByCore { + uint256 localDisputeID = disputes.length; + Dispute storage dispute = disputes.push(); + dispute.numberOfChoices = _numberOfChoices; + dispute.extraData = _extraData; + + // New round in the Core should be created before the dispute creation in DK. + dispute.coreRoundIDToLocal[core.getNumberOfRounds(_coreDisputeID) - 1] = dispute.rounds.length; + + Round storage round = dispute.rounds.push(); + round.nbVotes = _nbVotes; + round.tied = true; + + coreDisputeIDToLocal[_coreDisputeID] = localDisputeID; + emit DisputeCreation(_coreDisputeID, _numberOfChoices, _extraData); + } + + /// @dev Draws the juror from the sortition tree. The drawn address is picked up by Kleros Core. + /// Note: Access restricted to Kleros Core only. + /// @param _coreDisputeID The ID of the dispute in Kleros Core. + /// @param _nonce Nonce of the drawing iteration. + /// @return drawnAddress The drawn address. + function draw( + uint256 _coreDisputeID, + uint256 _nonce + ) external override onlyByCore notJumped(_coreDisputeID) returns (address drawnAddress) { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + + ISortitionModule sortitionModule = core.sortitionModule(); + (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); + bytes32 key = bytes32(uint256(courtID)); // Get the ID of the tree. + + drawnAddress = sortitionModule.draw(key, _coreDisputeID, _nonce); + + if (_postDrawCheck(_coreDisputeID, drawnAddress)) { + round.votes.push(Vote({account: drawnAddress, commit: bytes32(0), choice: 0, voted: false})); + } else { + drawnAddress = address(0); + } + } + + /// @dev Sets the caller's commit for the specified votes. It can be called multiple times during the + /// commit period, each call overrides the commits of the previous one. + /// `O(n)` where + /// `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. Note that justification string is a part of the commit. + function castCommit( + uint256 _coreDisputeID, + uint256[] calldata _voteIDs, + bytes32 _commit + ) external notJumped(_coreDisputeID) { + (, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID); + require(period == KlerosCoreBase.Period.commit, "The dispute should be in Commit period."); + require(_commit != bytes32(0), "Empty commit."); + + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + for (uint256 i = 0; i < _voteIDs.length; i++) { + require(round.votes[_voteIDs[i]].account == msg.sender, "The caller has to own the vote."); + round.votes[_voteIDs[i]].commit = _commit; + } + round.totalCommitted += _voteIDs.length; + emit CommitCast(_coreDisputeID, msg.sender, _voteIDs, _commit); + } + + /// @dev Sets the caller's choices for the specified votes. + /// `O(n)` where + /// `n` is the number of votes. + /// @param _coreDisputeID The ID of the dispute in Kleros Core. + /// @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, + string memory _justification + ) external notJumped(_coreDisputeID) { + (, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID); + require(period == KlerosCoreBase.Period.vote, "The dispute should be in Vote period."); + require(_voteIDs.length > 0, "No voteID provided"); + + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + require(_choice <= dispute.numberOfChoices, "Choice out of bounds"); + + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); + (, bool hiddenVotes, , , , , ) = core.courts(courtID); + + // Save the votes. + 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)), + "The commit must match the choice in courts with hidden votes." + ); + require(!round.votes[_voteIDs[i]].voted, "Vote already cast."); + round.votes[_voteIDs[i]].choice = _choice; + round.votes[_voteIDs[i]].voted = true; + } + + round.totalVoted += _voteIDs.length; + + round.counts[_choice] += _voteIDs.length; + if (_choice == round.winningChoice) { + if (round.tied) round.tied = false; + } else { + // Voted for another choice. + if (round.counts[_choice] == round.counts[round.winningChoice]) { + // Tie. + if (!round.tied) round.tied = true; + } else if (round.counts[_choice] > round.counts[round.winningChoice]) { + // New winner. + round.winningChoice = _choice; + round.tied = false; + } + } + emit VoteCast(_coreDisputeID, msg.sender, _voteIDs, _choice, _justification); + } + + /// @dev Manages contributions, and appeals a dispute if at least two choices are fully funded. + /// Note that the surplus deposit will be reimbursed. + /// @param _coreDisputeID Index of the dispute in Kleros Core. + /// @param _choice A choice that receives funding. + function fundAppeal(uint256 _coreDisputeID, uint256 _choice) external payable notJumped(_coreDisputeID) { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + require(_choice <= dispute.numberOfChoices, "There is no such ruling to fund."); + + (uint256 appealPeriodStart, uint256 appealPeriodEnd) = core.appealPeriod(_coreDisputeID); + require(block.timestamp >= appealPeriodStart && block.timestamp < appealPeriodEnd, "Appeal period is over."); + + uint256 multiplier; + (uint256 ruling, , ) = this.currentRuling(_coreDisputeID); + if (ruling == _choice) { + multiplier = WINNER_STAKE_MULTIPLIER; + } else { + require( + block.timestamp - appealPeriodStart < + ((appealPeriodEnd - appealPeriodStart) * LOSER_APPEAL_PERIOD_MULTIPLIER) / ONE_BASIS_POINT, + "Appeal period is over for loser" + ); + multiplier = LOSER_STAKE_MULTIPLIER; + } + + 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; + + // Take up to the amount necessary to fund the current round at the current costs. + uint256 contribution; + if (totalCost > round.paidFees[_choice]) { + 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, coreRoundID, _choice, msg.sender, contribution); + } + + round.contributions[msg.sender][_choice] += contribution; + round.paidFees[_choice] += contribution; + if (round.paidFees[_choice] >= totalCost) { + round.feeRewards += round.paidFees[_choice]; + round.fundedChoices.push(_choice); + round.hasPaid[_choice] = true; + emit ChoiceFunded(_coreDisputeID, coreRoundID, _choice); + } + + if (round.fundedChoices.length > 1) { + // At least two sides are fully funded. + round.feeRewards = round.feeRewards - appealCost; + + if (core.isDisputeKitJumping(_coreDisputeID)) { + // Don't create a new round in case of a jump, and remove local dispute from the flow. + dispute.jumped = true; + } else { + // Don't subtract 1 from length since both round arrays haven't been updated yet. + dispute.coreRoundIDToLocal[coreRoundID + 1] = dispute.rounds.length; + + Round storage newRound = dispute.rounds.push(); + newRound.nbVotes = core.getNumberOfVotes(_coreDisputeID); + newRound.tied = true; + } + core.appeal{value: appealCost}(_coreDisputeID, dispute.numberOfChoices, dispute.extraData); + } + + if (msg.value > contribution) payable(msg.sender).send(msg.value - contribution); + } + + /// @dev Allows those contributors who attempted to fund an appeal round to withdraw any reimbursable fees or rewards after the dispute gets resolved. + /// Note that withdrawals are not possible if the core contract is paused. + /// @param _coreDisputeID Index of the dispute in Kleros Core contract. + /// @param _beneficiary The address whose rewards to withdraw. + /// @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 _coreRoundID, + uint256 _choice + ) external returns (uint256 amount) { + (, , , bool isRuled, ) = core.disputes(_coreDisputeID); + require(isRuled, "Dispute should be resolved."); + require(!core.paused(), "Core is paused"); + + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Round storage round = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; + (uint256 finalRuling, , ) = core.currentRuling(_coreDisputeID); + + if (!round.hasPaid[_choice]) { + // Allow to reimburse if funding was unsuccessful for this ruling option. + amount = round.contributions[_beneficiary][_choice]; + } else { + // Funding was successful for this ruling option. + if (_choice == finalRuling) { + // This ruling option is the ultimate winner. + amount = round.paidFees[_choice] > 0 + ? (round.contributions[_beneficiary][_choice] * round.feeRewards) / round.paidFees[_choice] + : 0; + } else if (!round.hasPaid[finalRuling]) { + // The ultimate winner was not funded in this round. In this case funded ruling option(s) are reimbursed. + amount = + (round.contributions[_beneficiary][_choice] * round.feeRewards) / + (round.paidFees[round.fundedChoices[0]] + round.paidFees[round.fundedChoices[1]]); + } + } + round.contributions[_beneficiary][_choice] = 0; + + 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, _coreRoundID, _choice, _beneficiary, amount); + } + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + function getFundedChoices(uint256 _coreDisputeID) public view returns (uint256[] memory fundedChoices) { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Round storage lastRound = dispute.rounds[dispute.rounds.length - 1]; + return lastRound.fundedChoices; + } + + /// @dev Gets the current ruling of a specified dispute. + /// @param _coreDisputeID The ID of the dispute in Kleros Core. + /// @return ruling The current ruling. + /// @return tied Whether it's a tie or not. + /// @return overridden Whether the ruling was overridden by appeal funding or not. + function currentRuling( + uint256 _coreDisputeID + ) external view override returns (uint256 ruling, bool tied, bool overridden) { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + tied = round.tied; + ruling = tied ? 0 : round.winningChoice; + (, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID); + // Override the final ruling if only one side funded the appeals. + if (period == KlerosCoreBase.Period.execution) { + uint256[] memory fundedChoices = getFundedChoices(_coreDisputeID); + if (fundedChoices.length == 1) { + ruling = fundedChoices[0]; + tied = false; + overridden = true; + } + } + } + + /// @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. + /// @param _coreRoundID The ID of the round in Kleros Core, not in the Dispute Kit. + /// @param _voteID The ID of the vote. + /// @return The degree of coherence in basis points. + function getDegreeOfCoherence( + uint256 _coreDisputeID, + uint256 _coreRoundID, + uint256 _voteID, + uint256 /* _feePerJuror */, + uint256 /* _pnkAtStakePerJuror */ + ) external view override returns (uint256) { + // In this contract this degree can be either 0 or 1, but in other dispute kits this value can be something in between. + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Vote storage vote = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]].votes[_voteID]; + (uint256 winningChoice, bool tied, ) = core.currentRuling(_coreDisputeID); + + if (vote.voted && (vote.choice == winningChoice || tied)) { + return ONE_BASIS_POINT; + } else { + return 0; + } + } + + /// @dev Gets the number of jurors who are eligible to a reward in this round. + /// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit. + /// @param _coreRoundID The ID of the round in Kleros Core, not in the Dispute Kit. + /// @return The number of coherent jurors. + function getCoherentCount(uint256 _coreDisputeID, uint256 _coreRoundID) external view override returns (uint256) { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Round storage currentRound = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; + (uint256 winningChoice, bool tied, ) = core.currentRuling(_coreDisputeID); + + if (currentRound.totalVoted == 0 || (!tied && currentRound.counts[winningChoice] == 0)) { + return 0; + } else if (tied) { + return currentRound.totalVoted; + } else { + return currentRound.counts[winningChoice]; + } + } + + /// @dev Returns true if all of the jurors have cast their commits for the last round. + /// @param _coreDisputeID The ID of the dispute in Kleros Core. + /// @return Whether all of the jurors have cast their commits for the last round. + function areCommitsAllCast(uint256 _coreDisputeID) external view override returns (bool) { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + return round.totalCommitted == round.votes.length; + } + + /// @dev Returns true if all of the jurors have cast their votes for the last round. + /// @param _coreDisputeID The ID of the dispute in Kleros Core. + /// @return Whether all of the jurors have cast their votes for the last round. + function areVotesAllCast(uint256 _coreDisputeID) external view override returns (bool) { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + return round.totalVoted == round.votes.length; + } + + /// @dev Returns true if the specified voter was active in this round. + /// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit. + /// @param _coreRoundID The ID of the round in Kleros Core, not in the Dispute Kit. + /// @param _voteID The ID of the voter. + /// @return Whether the voter was active or not. + function isVoteActive( + uint256 _coreDisputeID, + uint256 _coreRoundID, + uint256 _voteID + ) external view override returns (bool) { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Vote storage vote = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]].votes[_voteID]; + return vote.voted; + } + + function getRoundInfo( + uint256 _coreDisputeID, + uint256 _coreRoundID, + uint256 _choice + ) + external + view + override + returns ( + uint256 winningChoice, + bool tied, + uint256 totalVoted, + uint256 totalCommited, + uint256 nbVoters, + uint256 choiceCount + ) + { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Round storage round = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; + return ( + round.winningChoice, + round.tied, + round.totalVoted, + round.totalCommitted, + round.votes.length, + round.counts[_choice] + ); + } + + function getVoteInfo( + uint256 _coreDisputeID, + uint256 _coreRoundID, + uint256 _voteID + ) external view override returns (address account, bytes32 commit, uint256 choice, bool voted) { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Vote storage vote = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]].votes[_voteID]; + return (vote.account, vote.commit, vote.choice, vote.voted); + } + + // ************************************* // + // * 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. + /// Note that we don't check the minStake requirement here because of the implicit staking in parent courts. + /// minStake is checked directly during staking process however it's possible for the juror to get drawn + /// while having < minStake if it is later increased by governance. + /// This issue is expected and harmless since we check for insolvency anyway. + function _postDrawCheck(uint256 _coreDisputeID, address _juror) internal view returns (bool) { + (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); + uint256 lockedAmountPerJuror = core + .getRoundInfo(_coreDisputeID, core.getNumberOfRounds(_coreDisputeID) - 1) + .pnkAtStakePerJuror; + (uint256 totalStaked, uint256 totalLocked, , ) = core.sortitionModule().getJurorBalance(_juror, courtID); + if (totalStaked < totalLocked + lockedAmountPerJuror) { + return false; + } else { + return tokenGate.balanceOf(_juror) > 0; + } + } +}