From 208e0093ce8c7de0c4902dab256d60fe134b9a69 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 25 Aug 2025 22:26:08 +0100 Subject: [PATCH 1/3] feat: let the dispute kit force an early court jump and override the next round nbVotes --- contracts/hardhat.config.ts | 2 +- contracts/src/arbitration/KlerosCoreBase.sol | 71 ++++++++++++++----- .../dispute-kits/DisputeKitClassicBase.sol | 13 ++++ .../arbitration/interfaces/IDisputeKit.sol | 10 +++ 4 files changed, 76 insertions(+), 20 deletions(-) diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index dc1cce1c6..3bb2787ca 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -31,7 +31,7 @@ const config: HardhatUserConfig = { viaIR: process.env.VIA_IR !== "false", // Defaults to true optimizer: { enabled: true, - runs: 10000, + runs: 2000, }, outputSelection: { "*": { diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 5816f872a..abee79655 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -639,24 +639,17 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable Round storage round = dispute.rounds[dispute.rounds.length - 1]; if (msg.sender != address(disputeKits[round.disputeKitID])) revert DisputeKitOnly(); - uint96 newCourtID = dispute.courtID; - uint256 newDisputeKitID = round.disputeKitID; - // Warning: the extra round must be created before calling disputeKit.createDispute() Round storage extraRound = dispute.rounds.push(); - if (round.nbVotes >= courts[newCourtID].jurorsForCourtJump) { - // Jump to parent court. - newCourtID = courts[newCourtID].parent; - - if (!courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { - // Switch to classic dispute kit if parent court doesn't support the current one. - newDisputeKitID = DISPUTE_KIT_CLASSIC; - } - - if (newCourtID != dispute.courtID) { - emit CourtJump(_disputeID, dispute.rounds.length - 1, dispute.courtID, newCourtID); - } + (uint96 newCourtID, uint256 newDisputeKitID, bool courtJump, ) = _getCourtAndDisputeKitJumps( + dispute, + round, + courts[dispute.courtID], + _disputeID + ); + if (courtJump) { + emit CourtJump(_disputeID, dispute.rounds.length - 1, dispute.courtID, newCourtID); } dispute.courtID = newCourtID; @@ -934,17 +927,22 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable Dispute storage dispute = disputes[_disputeID]; Round storage round = dispute.rounds[dispute.rounds.length - 1]; Court storage court = courts[dispute.courtID]; - if (round.nbVotes >= court.jurorsForCourtJump) { + + (, uint256 newDisputeKitID, bool courtJump, ) = _getCourtAndDisputeKitJumps(dispute, round, court, _disputeID); + + uint256 nbVotesAfterAppeal = disputeKits[newDisputeKitID].getNbVotesAfterAppeal(round.nbVotes); + + if (courtJump) { // Jump to parent court. if (dispute.courtID == GENERAL_COURT) { // TODO: Handle the forking when appealed in General court. cost = NON_PAYABLE_AMOUNT; // Get the cost of the parent court. } else { - cost = courts[court.parent].feeForJuror * ((round.nbVotes * 2) + 1); + cost = courts[court.parent].feeForJuror * nbVotesAfterAppeal; } } else { // Stay in current court. - cost = court.feeForJuror * ((round.nbVotes * 2) + 1); + cost = court.feeForJuror * nbVotesAfterAppeal; } } @@ -1033,7 +1031,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable Round storage round = dispute.rounds[dispute.rounds.length - 1]; Court storage court = courts[dispute.courtID]; - if (round.nbVotes < court.jurorsForCourtJump) { + if (!_isCourtJumping(round, court, _disputeID)) { return false; } @@ -1053,6 +1051,41 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable // * Internal * // // ************************************* // + /// @dev Returns true if the round is jumping to a parent court. + /// @param _round The round to check. + /// @param _court The court to check. + /// @return Whether the round is jumping to a parent court or not. + function _isCourtJumping( + Round storage _round, + Court storage _court, + uint256 _disputeID + ) internal view returns (bool) { + return + disputeKits[_round.disputeKitID].earlyCourtJump(_disputeID) || _round.nbVotes >= _court.jurorsForCourtJump; + } + + function _getCourtAndDisputeKitJumps( + Dispute storage _dispute, + Round storage _round, + Court storage _court, + uint256 _disputeID + ) internal view returns (uint96 newCourtID, uint256 newDisputeKitID, bool courtJump, bool disputeKitJump) { + newCourtID = _dispute.courtID; + newDisputeKitID = _round.disputeKitID; + + if (!_isCourtJumping(_round, _court, _disputeID)) return (newCourtID, newDisputeKitID, false, false); + + // Jump to parent court. + newCourtID = courts[newCourtID].parent; + courtJump = true; + + if (!courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { + // Switch to classic dispute kit if parent court doesn't support the current one. + newDisputeKitID = DISPUTE_KIT_CLASSIC; + disputeKitJump = true; + } + } + /// @dev Internal function to transfer fee tokens (ETH or ERC20) /// @param _feeToken The token to transfer (NATIVE_CURRENCY for ETH). /// @param _recipient The recipient address. diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 0922f047b..f3e3d00a0 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -627,6 +627,19 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi ((appealPeriodEnd - appealPeriodStart) * LOSER_APPEAL_PERIOD_MULTIPLIER) / ONE_BASIS_POINT); } + /// @dev Returns true if the dispute is jumping to a parent court. + /// @return Whether the dispute is jumping to a parent court or not. + function earlyCourtJump(uint256 /* _coreDisputeID */) external pure override returns (bool) { + return false; + } + + /// @dev Returns the number of votes after the appeal. + /// @param _currentNbVotes The number of votes before the appeal. + /// @return The number of votes after the appeal. + function getNbVotesAfterAppeal(uint256 _currentNbVotes) external pure override returns (uint256) { + return (_currentNbVotes * 2) + 1; + } + /// @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. diff --git a/contracts/src/arbitration/interfaces/IDisputeKit.sol b/contracts/src/arbitration/interfaces/IDisputeKit.sol index 6a72b35e4..b5e6f03a9 100644 --- a/contracts/src/arbitration/interfaces/IDisputeKit.sol +++ b/contracts/src/arbitration/interfaces/IDisputeKit.sol @@ -113,6 +113,16 @@ interface IDisputeKit { /// @return Whether the appeal funding is finished. function isAppealFunded(uint256 _coreDisputeID) external view returns (bool); + /// @dev Returns true if the dispute is jumping to a parent court. + /// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit. + /// @return Whether the dispute is jumping to a parent court or not. + function earlyCourtJump(uint256 _coreDisputeID) external view returns (bool); + + /// @dev Returns the number of votes after the appeal. + /// @param _currentNbVotes The number of votes before the appeal. + /// @return The number of votes after the appeal. + function getNbVotesAfterAppeal(uint256 _currentNbVotes) external view returns (uint256); + /// @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. From 42cc971d9542b07ed6c252157b48708b27fd82a7 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 26 Aug 2025 20:11:58 +0100 Subject: [PATCH 2/3] feat: added the current dispute kit as parameter to getNbVotesAfterAppeal() --- contracts/src/arbitration/KlerosCoreBase.sol | 5 ++++- .../src/arbitration/dispute-kits/DisputeKitClassicBase.sol | 5 ++++- contracts/src/arbitration/interfaces/IDisputeKit.sol | 6 +++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index abee79655..eb595096e 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -930,7 +930,10 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable (, uint256 newDisputeKitID, bool courtJump, ) = _getCourtAndDisputeKitJumps(dispute, round, court, _disputeID); - uint256 nbVotesAfterAppeal = disputeKits[newDisputeKitID].getNbVotesAfterAppeal(round.nbVotes); + uint256 nbVotesAfterAppeal = disputeKits[newDisputeKitID].getNbVotesAfterAppeal( + disputeKits[round.disputeKitID], + round.nbVotes + ); if (courtJump) { // Jump to parent court. diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index f3e3d00a0..a9e96f7eb 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -636,7 +636,10 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// @dev Returns the number of votes after the appeal. /// @param _currentNbVotes The number of votes before the appeal. /// @return The number of votes after the appeal. - function getNbVotesAfterAppeal(uint256 _currentNbVotes) external pure override returns (uint256) { + function getNbVotesAfterAppeal( + IDisputeKit /* _previousDisputeKit */, + uint256 _currentNbVotes + ) external pure override returns (uint256) { return (_currentNbVotes * 2) + 1; } diff --git a/contracts/src/arbitration/interfaces/IDisputeKit.sol b/contracts/src/arbitration/interfaces/IDisputeKit.sol index b5e6f03a9..0aab45b9c 100644 --- a/contracts/src/arbitration/interfaces/IDisputeKit.sol +++ b/contracts/src/arbitration/interfaces/IDisputeKit.sol @@ -119,9 +119,13 @@ interface IDisputeKit { function earlyCourtJump(uint256 _coreDisputeID) external view returns (bool); /// @dev Returns the number of votes after the appeal. + /// @param _previousDisputeKit The previous Dispute Kit. /// @param _currentNbVotes The number of votes before the appeal. /// @return The number of votes after the appeal. - function getNbVotesAfterAppeal(uint256 _currentNbVotes) external view returns (uint256); + function getNbVotesAfterAppeal( + IDisputeKit _previousDisputeKit, + uint256 _currentNbVotes + ) external view returns (uint256); // TODO: remove previousDisputeKit /// @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. From 057957e2f03744aed877244dd12834b5d659ea30 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 27 Aug 2025 03:47:19 +0100 Subject: [PATCH 3/3] chore: changelog --- contracts/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index bea677f34..751bd5473 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -23,6 +23,10 @@ The format is based on [Common Changelog](https://common-changelog.org/). - Bump `hardhat` to v2.26.2 ([#2069](https://github.com/kleros/kleros-v2/issues/2069)) - Bump `@kleros/vea-contracts` to v0.7.0 ([#2073](https://github.com/kleros/kleros-v2/issues/2073)) +### Added + +- Allow the dispute kits to force an early court jump and to override the number of votes after an appeal (future-proofing) ([#2110](https://github.com/kleros/kleros-v2/issues/2110)) + ### Fixed - Do not pass to Voting period if all the commits are cast because it breaks the current Shutter auto-reveal process. ([#2085](https://github.com/kleros/kleros-v2/issues/2085))