From b6ca2adb671f5d284d2537c791124f1eed30790d Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Sat, 25 Oct 2025 01:36:55 +0100 Subject: [PATCH 1/4] feat: new dispute kit with specialized gating logic For accredited Argentinian consumer protection lawyers --- contracts/src/arbitration/KlerosCore.sol | 6 +- .../dispute-kits/DisputeKitClassicBase.sol | 12 +- ...uteKitGatedArgentinaConsumerProtection.sol | 143 ++++++++++++++++++ .../arbitration/interfaces/IDisputeKit.sol | 4 +- .../university/KlerosCoreUniversity.sol | 2 +- 5 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 contracts/src/arbitration/dispute-kits/DisputeKitGatedArgentinaConsumerProtection.sol diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index e0dbfc0be..447579812 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -753,7 +753,11 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { uint256 startIndex = round.drawIterations; // for gas: less storage reads uint256 i; while (i < _iterations && round.drawnJurors.length < round.nbVotes) { - (address drawnAddress, uint96 fromSubcourtID) = disputeKit.draw(_disputeID, startIndex + i++); + (address drawnAddress, uint96 fromSubcourtID) = disputeKit.draw( + _disputeID, + startIndex + i++, + round.nbVotes + ); if (drawnAddress == address(0)) { continue; } diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index e03ce1831..525664d49 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -251,8 +251,16 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// @inheritdoc IDisputeKit function draw( uint256 _coreDisputeID, - uint256 _nonce - ) external override onlyByCore isActive(_coreDisputeID) returns (address drawnAddress, uint96 fromSubcourtID) { + uint256 _nonce, + uint256 /*_roundNbVotes*/ + ) + public + virtual + override + onlyByCore + isActive(_coreDisputeID) + returns (address drawnAddress, uint96 fromSubcourtID) + { uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; Dispute storage dispute = disputes[localDisputeID]; uint256 localRoundID = dispute.rounds.length - 1; diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGatedArgentinaConsumerProtection.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGatedArgentinaConsumerProtection.sol new file mode 100644 index 000000000..bff83866f --- /dev/null +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGatedArgentinaConsumerProtection.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {DisputeKitClassicBase} from "./DisputeKitClassicBase.sol"; +import {KlerosCore} from "../KlerosCore.sol"; + +interface IBalanceHolder { + /// @notice Returns the number of tokens in `owner` account. + /// @dev Compatible with ERC-20 and ERC-721. + /// @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 DisputeKitGatedArgentinaConsumerProtection +/// @notice Dispute kit implementation adapted from DisputeKitClassic +/// - a drawing system: proportional to staked PNK among the jurors holding a `accreditedLawyerToken` or a `accreditedConsumerProtectionLawyerToken` +/// and at least one of the drawn jurors is holding a `accreditedConsumerProtectionLawyerToken`, +/// - 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 DisputeKitGatedArgentinaConsumerProtection is DisputeKitClassicBase { + string public constant override version = "2.0.0"; + + // ************************************* // + // * Storage * // + // ************************************* // + + address public accreditedLawyerToken; // The address of the accredited lawyer token. + address public accreditedConsumerProtectionLawyerToken; // The address of the accredited consumer protection lawyer token. + mapping(uint256 localDisputeID => mapping(uint256 localRoundID => bool)) public drawnConsumerProtectionLawyer; // Maps the local dispute and round ID to the boolean indicating if the consumer protection lawyer was drawn. + + // ************************************* // + // * Constructor * // + // ************************************* // + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Initializer. + /// @param _owner The owner's address. + /// @param _core The KlerosCore arbitrator. + /// @param _wNative The wrapped native token address, typically wETH. + /// @param _accreditedLawyerToken The address of the accredited lawyer token. + /// @param _accreditedConsumerProtectionLawyerToken The address of the accredited consumer protection lawyer token. + function initialize( + address _owner, + KlerosCore _core, + address _wNative, + address _accreditedLawyerToken, + address _accreditedConsumerProtectionLawyerToken + ) external initializer { + __DisputeKitClassicBase_initialize(_owner, _core, _wNative); + accreditedLawyerToken = _accreditedLawyerToken; + accreditedConsumerProtectionLawyerToken = _accreditedConsumerProtectionLawyerToken; + } + + // ************************ // + // * Governance * // + // ************************ // + + /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) + /// Only the owner can perform upgrades (`onlyByOwner`) + function _authorizeUpgrade(address) internal view override onlyByOwner { + // NOP + } + + /// @notice Changes the accredited lawyer token. + /// @param _accreditedLawyerToken The address of the accredited lawyer token. + function changeAccreditedLawyerToken(address _accreditedLawyerToken) external onlyByOwner { + accreditedLawyerToken = _accreditedLawyerToken; + } + + /// @notice Changes the accredited consumer protection lawyer token. + /// @param _accreditedConsumerProtectionLawyerToken The address of the accredited consumer protection lawyer token. + function changeAccreditedConsumerProtectionLawyerToken( + address _accreditedConsumerProtectionLawyerToken + ) external onlyByOwner { + accreditedConsumerProtectionLawyerToken = _accreditedConsumerProtectionLawyerToken; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @inheritdoc DisputeKitClassicBase + function draw( + uint256 _coreDisputeID, + uint256 _nonce, + uint256 _roundNbVotes + ) public override onlyByCore isActive(_coreDisputeID) returns (address drawnAddress, uint96 fromSubcourtID) { + (drawnAddress, fromSubcourtID) = super.draw(_coreDisputeID, _nonce, _roundNbVotes); + + if (drawnAddress == address(0)) return (drawnAddress, fromSubcourtID); + + uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; + Dispute storage dispute = disputes[localDisputeID]; + uint256 localRoundID = dispute.rounds.length - 1; + if (IBalanceHolder(accreditedConsumerProtectionLawyerToken).balanceOf(drawnAddress) > 0) { + // The drawnAddress is a consumer protection lawyer. + drawnConsumerProtectionLawyer[localDisputeID][localRoundID] = true; + } else { + // The drawnAddress is not a consumer protection lawyer. + if ( + dispute.rounds[localRoundID].votes.length == _roundNbVotes && + !drawnConsumerProtectionLawyer[localDisputeID][localRoundID] + ) { + // This is the last draw iteration and we still have not drawn a consumer protection lawyer. + // Drop the last round.votes pushed by super.draw(), so that another iteration can try again. + drawnAddress = address(0); + dispute.rounds[localRoundID].votes.pop(); + // Note that round.alreadyDrawn[drawnAddress] is not cleared because we don't know if it has been drawn more than once. + // It's fine because this DisputeKit does not enable singleDrawPerJuror. + } + } + return (drawnAddress, fromSubcourtID); + } + + // ************************************* // + // * Internal * // + // ************************************* // + + /// @inheritdoc DisputeKitClassicBase + function _postDrawCheck( + Round storage _round, + uint256 _coreDisputeID, + address _juror + ) internal view override returns (bool) { + if (!super._postDrawCheck(_round, _coreDisputeID, _juror)) return false; + return + (IBalanceHolder(accreditedLawyerToken).balanceOf(_juror) > 0) || + (IBalanceHolder(accreditedConsumerProtectionLawyerToken).balanceOf(_juror) > 0); + } + + // ************************************* // + // * Errors * // + // ************************************* // + + error TokenNotSupported(address tokenGate); +} diff --git a/contracts/src/arbitration/interfaces/IDisputeKit.sol b/contracts/src/arbitration/interfaces/IDisputeKit.sol index dcb6c423f..8c8271f0f 100644 --- a/contracts/src/arbitration/interfaces/IDisputeKit.sol +++ b/contracts/src/arbitration/interfaces/IDisputeKit.sol @@ -50,11 +50,13 @@ interface IDisputeKit { /// @dev Access restricted to Kleros Core only. /// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit. /// @param _nonce Nonce. + /// @param _roundNbVotes The number of votes in the round, including already drawn and yet to be drawn. /// @return drawnAddress The drawn address. /// @return fromSubcourtID The subcourt ID from which the juror was drawn. function draw( uint256 _coreDisputeID, - uint256 _nonce + uint256 _nonce, + uint256 _roundNbVotes ) external returns (address drawnAddress, uint96 fromSubcourtID); // ************************************* // diff --git a/contracts/src/arbitration/university/KlerosCoreUniversity.sol b/contracts/src/arbitration/university/KlerosCoreUniversity.sol index fd9f49eaa..9e70c0092 100644 --- a/contracts/src/arbitration/university/KlerosCoreUniversity.sol +++ b/contracts/src/arbitration/university/KlerosCoreUniversity.sol @@ -602,7 +602,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { { IDisputeKit disputeKit = disputeKits[round.disputeKitID]; uint256 iteration = round.drawIterations + 1; - (address drawnAddress, uint96 fromSubcourtID) = disputeKit.draw(_disputeID, iteration); + (address drawnAddress, uint96 fromSubcourtID) = disputeKit.draw(_disputeID, iteration, round.nbVotes); if (drawnAddress == address(0)) { revert NoJurorDrawn(); } From f5f3079d8d96ea1faf5bbac406e51b10b0ead257 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Sat, 25 Oct 2025 02:47:33 +0100 Subject: [PATCH 2/4] test: added coverage for the Argentina CP DK --- ...dArgentinaConsumerProtection_Drawing.t.sol | 503 ++++++++++++++++++ .../test/foundry/KlerosCore_Appeals.t.sol | 8 +- 2 files changed, 507 insertions(+), 4 deletions(-) create mode 100644 contracts/test/foundry/DisputeKitGatedArgentinaConsumerProtection_Drawing.t.sol diff --git a/contracts/test/foundry/DisputeKitGatedArgentinaConsumerProtection_Drawing.t.sol b/contracts/test/foundry/DisputeKitGatedArgentinaConsumerProtection_Drawing.t.sol new file mode 100644 index 000000000..0ea2fb383 --- /dev/null +++ b/contracts/test/foundry/DisputeKitGatedArgentinaConsumerProtection_Drawing.t.sol @@ -0,0 +1,503 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; +import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; +import {DisputeKitGatedArgentinaConsumerProtection} from "../../src/arbitration/dispute-kits/DisputeKitGatedArgentinaConsumerProtection.sol"; +import {ArbitrableExample} from "../../src/arbitration/arbitrables/ArbitrableExample.sol"; +import {TestERC721} from "../../src/token/TestERC721.sol"; +import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {console} from "forge-std/console.sol"; +import "../../src/libraries/Constants.sol"; + +/// @title DisputeKitGatedArgentinaConsumerProtection_DrawingTest +/// @dev Tests for DisputeKitGatedArgentinaConsumerProtection drawing logic +/// This dispute kit requires: +/// - Jurors must own either accreditedLawyerToken OR accreditedConsumerProtectionLawyerToken +/// - Each round must have at least one juror with accreditedConsumerProtectionLawyerToken +contract DisputeKitGatedArgentinaConsumerProtection_DrawingTest is KlerosCore_TestBase { + // ************************************* // + // * Test Contracts * // + // ************************************* // + + DisputeKitGatedArgentinaConsumerProtection argentinaDK; + TestERC721 accreditedLawyerToken; + TestERC721 accreditedConsumerProtectionLawyerToken; + + // ************************************* // + // * Test Accounts * // + // ************************************* // + + address lawyer1; // Has only accredited lawyer token + address lawyer2; // Has only accredited lawyer token + address consumerLawyer1; // Has only consumer protection lawyer token + address consumerLawyer2; // Has only consumer protection lawyer token + address bothLawyer; // Has both tokens + address noTokenJuror; // Has no tokens + + // ************************************* // + // * Test Parameters * // + // ************************************* // + + uint96 argentinaCourt; + uint256 constant ARGENTINA_DK_ID = 2; // Assuming DK ID 1 is DisputeKitClassic + ArbitrableExample argentinaArbitrable; // Arbitrable for Argentina court + + function setUp() public override { + super.setUp(); + + // Set up additional test accounts + lawyer1 = vm.addr(10); + lawyer2 = vm.addr(11); + consumerLawyer1 = vm.addr(12); + consumerLawyer2 = vm.addr(13); + bothLawyer = vm.addr(14); + noTokenJuror = vm.addr(15); + + // Deploy token contracts + accreditedLawyerToken = new TestERC721("Accredited Lawyer", "AL"); + accreditedConsumerProtectionLawyerToken = new TestERC721("Consumer Protection Lawyer", "CPL"); + + // Deploy and initialize the Argentina dispute kit + DisputeKitGatedArgentinaConsumerProtection dkLogic = new DisputeKitGatedArgentinaConsumerProtection(); + bytes memory initData = abi.encodeWithSignature( + "initialize(address,address,address,address,address)", + owner, + address(core), + address(wNative), + address(accreditedLawyerToken), + address(accreditedConsumerProtectionLawyerToken) + ); + UUPSProxy proxyDK = new UUPSProxy(address(dkLogic), initData); + argentinaDK = DisputeKitGatedArgentinaConsumerProtection(address(proxyDK)); + + // Add the dispute kit to core + vm.prank(owner); + core.addNewDisputeKit(argentinaDK); + + // Create a court that uses the Argentina dispute kit + // Note: Courts must always support DISPUTE_KIT_CLASSIC + uint256[] memory supportedDK = new uint256[](2); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + supportedDK[1] = ARGENTINA_DK_ID; + + vm.prank(owner); + core.createCourt( + GENERAL_COURT, + false, // Hidden votes + 1000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + sortitionExtraData, + supportedDK + ); + + uint256[] memory children = core.getCourtChildren(GENERAL_COURT); + argentinaCourt = uint96(children[children.length - 1]); + + // Enable the dispute kit in the Argentina court + uint256[] memory dkIDs = new uint256[](1); + dkIDs[0] = ARGENTINA_DK_ID; + vm.prank(owner); + core.enableDisputeKits(argentinaCourt, dkIDs, true); + + // Create an arbitrable that uses the Argentina court + bytes memory argentinaExtraData = abi.encodePacked( + uint256(argentinaCourt), + uint256(DEFAULT_NB_OF_JURORS), + uint256(ARGENTINA_DK_ID) + ); + argentinaArbitrable = new ArbitrableExample( + core, + templateData, + templateDataMappings, + argentinaExtraData, + registry, + feeToken + ); + + // Give PNK to all test jurors and approve core + address[6] memory jurors = [lawyer1, lawyer2, consumerLawyer1, consumerLawyer2, bothLawyer, noTokenJuror]; + for (uint256 i = 0; i < jurors.length; i++) { + vm.prank(owner); + pinakion.transfer(jurors[i], 5000); + vm.prank(jurors[i]); + pinakion.approve(address(core), 5000); + } + } + + // ************************************* // + // * Tests * // + // ************************************* // + + /// @notice Test helper to verify setup is correct + function test_setUp() public { + // Verify the Argentina court was created + ( + uint96 parent, + bool courtHiddenVotes, + uint256 minStakeValue, + uint256 alphaValue, + uint256 feeForJurorValue, + uint256 jurorsForJumpValue + ) = core.courts(argentinaCourt); + assertEq(parent, GENERAL_COURT, "Wrong parent court"); + assertEq(courtHiddenVotes, false, "Wrong hiddenVotes"); + assertEq(minStakeValue, 1000, "Wrong minStake"); + assertEq(alphaValue, 10000, "Wrong alpha"); + assertEq(feeForJurorValue, 0.03 ether, "Wrong feeForJuror"); + assertEq(jurorsForJumpValue, 50, "Wrong jurorsForJump"); + + // Debug: Log the values being encoded + console.log("argentinaCourt:", argentinaCourt); + console.log("DEFAULT_NB_OF_JURORS:", DEFAULT_NB_OF_JURORS); + console.log("ARGENTINA_DK_ID:", ARGENTINA_DK_ID); + + // Verify the arbitration cost is correct - use uint256 for all values + bytes memory extraData = abi.encodePacked( + uint256(argentinaCourt), + uint256(DEFAULT_NB_OF_JURORS), + uint256(ARGENTINA_DK_ID) + ); + console.log("extraData length:", extraData.length); + uint256 cost = core.arbitrationCost(extraData); + console.log("Actual cost:", cost); + console.log("Expected cost:", 0.03 ether * DEFAULT_NB_OF_JURORS); + assertEq(cost, 0.03 ether * DEFAULT_NB_OF_JURORS, "Wrong arbitration cost"); + } + + /// @notice Test that drawing fails when only accredited lawyers (no consumer protection lawyers) are staked + /// Expected: Drawing never completes all required jurors due to last-iteration rejection logic + function test_drawFailsWithOnlyAccreditedLawyers() public { + // Mint accredited lawyer tokens (but NOT consumer protection tokens) + accreditedLawyerToken.safeMint(lawyer1); + accreditedLawyerToken.safeMint(lawyer2); + + // Stake in the Argentina court + vm.prank(lawyer1); + core.setStake(argentinaCourt, 3000); + vm.prank(lawyer2); + core.setStake(argentinaCourt, 3000); + + // Create dispute + vm.prank(disputer); + argentinaArbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + uint256 disputeID = 0; + uint256 roundID = 0; + + // Try to draw jurors - should not complete all draws + core.draw(disputeID, DEFAULT_NB_OF_JURORS * 10); // Try many iterations + + // Verify that NOT all jurors were drawn (because last draw keeps getting rejected) + (, , , , uint256 nbVoters, ) = argentinaDK.getRoundInfo(disputeID, roundID, 0); + assertLt(nbVoters, DEFAULT_NB_OF_JURORS, "Should not have drawn all jurors"); + + // Verify the dispute is still waiting for jurors + assertEq(sortitionModule.disputesWithoutJurors(), 1, "Dispute should still be waiting for jurors"); + } + + /// @notice Test that drawing succeeds when only consumer protection lawyers are staked + function test_drawSucceedsWithOnlyConsumerProtectionLawyers() public { + // Mint consumer protection lawyer tokens + accreditedConsumerProtectionLawyerToken.safeMint(consumerLawyer1); + accreditedConsumerProtectionLawyerToken.safeMint(consumerLawyer2); + + // Stake in the Argentina court + vm.prank(consumerLawyer1); + core.setStake(argentinaCourt, 3000); + vm.prank(consumerLawyer2); + core.setStake(argentinaCourt, 3000); + + // Create dispute + vm.prank(disputer); + argentinaArbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + uint256 disputeID = 0; + uint256 roundID = 0; + + // Draw jurors + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + // Verify all jurors were drawn + (, , , , uint256 nbVoters, ) = argentinaDK.getRoundInfo(disputeID, roundID, 0); + assertEq(nbVoters, DEFAULT_NB_OF_JURORS, "Should have drawn all jurors"); + + // Verify at least one consumer protection lawyer was drawn + _verifyConsumerProtectionLawyerDrawn(disputeID, roundID, nbVoters); + + // Verify the dispute has all jurors + assertEq(sortitionModule.disputesWithoutJurors(), 0, "Dispute should have all jurors"); + } + + /// @notice Test that drawing succeeds with a mix of both lawyer types + function test_drawSucceedsWithMixedLawyers() public { + // Mint tokens to different jurors + accreditedLawyerToken.safeMint(lawyer1); + accreditedLawyerToken.safeMint(lawyer2); + accreditedConsumerProtectionLawyerToken.safeMint(consumerLawyer1); + + // Stake in the Argentina court + vm.prank(lawyer1); + core.setStake(argentinaCourt, 3000); + vm.prank(lawyer2); + core.setStake(argentinaCourt, 3000); + vm.prank(consumerLawyer1); + core.setStake(argentinaCourt, 3000); + + // Create dispute + vm.prank(disputer); + argentinaArbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + uint256 disputeID = 0; + uint256 roundID = 0; + + // Draw jurors + core.draw(disputeID, DEFAULT_NB_OF_JURORS * 5); // Extra iterations in case of retries + + // Verify all jurors were drawn + (, , , , uint256 nbVoters, ) = argentinaDK.getRoundInfo(disputeID, roundID, 0); + assertEq(nbVoters, DEFAULT_NB_OF_JURORS, "Should have drawn all jurors"); + + // Verify at least one consumer protection lawyer was drawn + _verifyConsumerProtectionLawyerDrawn(disputeID, roundID, nbVoters); + + // Verify the dispute has all jurors + assertEq(sortitionModule.disputesWithoutJurors(), 0, "Dispute should have all jurors"); + } + + /// @notice Test drawing when a juror holds both token types + function test_drawWithJurorHoldingBothTokens() public { + // Mint both tokens to the same juror + accreditedLawyerToken.safeMint(bothLawyer); + accreditedConsumerProtectionLawyerToken.safeMint(bothLawyer); + + // Stake in the Argentina court + vm.prank(bothLawyer); + core.setStake(argentinaCourt, 3000); + + // Create dispute + vm.prank(disputer); + argentinaArbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + uint256 disputeID = 0; + uint256 roundID = 0; + + // Draw jurors + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + // Verify all jurors were drawn + (, , , , uint256 nbVoters, ) = argentinaDK.getRoundInfo(disputeID, roundID, 0); + assertEq(nbVoters, DEFAULT_NB_OF_JURORS, "Should have drawn all jurors"); + + // Verify the juror with both tokens was drawn + bool foundBothLawyer = false; + for (uint256 i = 0; i < nbVoters; i++) { + (address account, , , ) = argentinaDK.getVoteInfo(disputeID, roundID, i); + if (account == bothLawyer) { + foundBothLawyer = true; + break; + } + } + assertTrue(foundBothLawyer, "Juror with both tokens should be drawn"); + + // Verify consumer protection requirement is satisfied + _verifyConsumerProtectionLawyerDrawn(disputeID, roundID, nbVoters); + } + + /// @notice Test that jurors without tokens cannot be drawn + function test_rejectJurorsWithoutTokens() public { + // Mint consumer protection token to one juror (to allow drawing to complete) + accreditedConsumerProtectionLawyerToken.safeMint(consumerLawyer1); + + // Stake: one with token, one without + vm.prank(consumerLawyer1); + core.setStake(argentinaCourt, 3000); + vm.prank(noTokenJuror); + core.setStake(argentinaCourt, 3000); + + // Create dispute + vm.prank(disputer); + argentinaArbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + uint256 disputeID = 0; + uint256 roundID = 0; + + // Draw jurors + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + // Verify that if any jurors were drawn, none of them is the noTokenJuror + (, , , , uint256 nbVoters, ) = argentinaDK.getRoundInfo(disputeID, roundID, 0); + for (uint256 i = 0; i < nbVoters; i++) { + (address account, , , ) = argentinaDK.getVoteInfo(disputeID, roundID, i); + assertNotEq(account, noTokenJuror, "Juror without token should not be drawn"); + } + } + + /// @notice Test that each round independently satisfies the consumer protection lawyer requirement + function test_multipleRoundsRequirement() public { + // Mint tokens + accreditedLawyerToken.safeMint(lawyer1); + accreditedConsumerProtectionLawyerToken.safeMint(consumerLawyer1); + + // Stake in the Argentina court + vm.prank(lawyer1); + core.setStake(argentinaCourt, 5000); + vm.prank(consumerLawyer1); + core.setStake(argentinaCourt, 5000); + + // Create dispute + vm.deal(disputer, 100 ether); + vm.prank(disputer); + argentinaArbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + uint256 disputeID = 0; + + // Draw round 0 + core.draw(disputeID, DEFAULT_NB_OF_JURORS * 5); + (, , , , uint256 nbVoters0, ) = argentinaDK.getRoundInfo(disputeID, 0, 0); + assertEq(nbVoters0, DEFAULT_NB_OF_JURORS, "Round 0 should have all jurors"); + _verifyConsumerProtectionLawyerDrawn(disputeID, 0, nbVoters0); + + // Move to voting period - need to pass evidence period first + vm.warp(block.timestamp + 10); // evidence period duration + core.passPeriod(disputeID); // Move to voting period + + // Vote with all jurors (no commit phase since hiddenVotes=false) + for (uint256 i = 0; i < nbVoters0; i++) { + (address account, , , ) = argentinaDK.getVoteInfo(disputeID, 0, i); + uint256 choice = (i % 2) + 1; // Vote 1 or 2 + uint256[] memory voteIDs = new uint256[](1); + voteIDs[0] = i; + vm.prank(account); + argentinaDK.castVote(disputeID, voteIDs, choice, 0, ""); + } + + // Move to appeal period + vm.warp(block.timestamp + 20); // voting period duration + core.passPeriod(disputeID); + + // Fund appeal for both sides + uint256 appealCost = core.appealCost(disputeID); + // Total cost = appealCost + (appealCost * multiplier / 10000) + uint256 winnerCost = appealCost + (appealCost * 10000) / 10000; // = appealCost + appealCost = 2x + uint256 loserCost = appealCost + (appealCost * 20000) / 10000; // = appealCost + 2*appealCost = 3x + + // Determine which choice is the winner/loser + (uint256 ruling, , ) = argentinaDK.currentRuling(disputeID); + uint256 winningChoice = ruling; + uint256 losingChoice = winningChoice == 1 ? 2 : 1; + + // Fund the losing side first + vm.prank(crowdfunder1); + vm.deal(crowdfunder1, 100 ether); + argentinaDK.fundAppeal{value: loserCost}(disputeID, losingChoice); + + // Fund the winning side - this triggers the appeal creation + vm.prank(crowdfunder2); + vm.deal(crowdfunder2, 100 ether); + argentinaDK.fundAppeal{value: winnerCost}(disputeID, winningChoice); + + // The dispute is now in evidence period for round 1, ready for drawing + // Round 1 has nbVotes * 2 + 1 jurors = 3 * 2 + 1 = 7 jurors + uint256 expectedJurorsRound1 = DEFAULT_NB_OF_JURORS * 2 + 1; + + // Draw round 1 + core.draw(disputeID, expectedJurorsRound1 * 5); // Extra iterations for retries + (, , , , uint256 nbVoters1, ) = argentinaDK.getRoundInfo(disputeID, 1, 0); + assertEq(nbVoters1, expectedJurorsRound1, "Round 1 should have all jurors"); + + // Verify round 1 also has a consumer protection lawyer + _verifyConsumerProtectionLawyerDrawn(disputeID, 1, nbVoters1); + } + + /// @notice Test that the drawnConsumerProtectionLawyer mapping is correctly updated + function test_drawnConsumerProtectionLawyerMapping() public { + // Mint consumer protection lawyer token + accreditedConsumerProtectionLawyerToken.safeMint(consumerLawyer1); + + // Stake in the Argentina court + vm.prank(consumerLawyer1); + core.setStake(argentinaCourt, 3000); + + // Create dispute + vm.prank(disputer); + argentinaArbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + uint256 disputeID = 0; + uint256 roundID = 0; + + // Get the local dispute ID + uint256 localDisputeID = argentinaDK.coreDisputeIDToLocal(disputeID); + + // Before drawing, the mapping should be false + assertFalse( + argentinaDK.drawnConsumerProtectionLawyer(localDisputeID, roundID), + "Should be false before drawing" + ); + + // Draw jurors + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + // After drawing, the mapping should be true + assertTrue( + argentinaDK.drawnConsumerProtectionLawyer(localDisputeID, roundID), + "Should be true after drawing consumer protection lawyer" + ); + } + + // ************************************* // + // * Helper Functions * // + // ************************************* // + + /// @notice Verify that at least one drawn juror owns the consumer protection lawyer token + function _verifyConsumerProtectionLawyerDrawn(uint256 _disputeID, uint256 _roundID, uint256 _nbVoters) internal { + bool foundConsumerLawyer = false; + for (uint256 i = 0; i < _nbVoters; i++) { + (address account, , , ) = argentinaDK.getVoteInfo(_disputeID, _roundID, i); + if (accreditedConsumerProtectionLawyerToken.balanceOf(account) > 0) { + foundConsumerLawyer = true; + break; + } + } + assertTrue(foundConsumerLawyer, "At least one consumer protection lawyer should be drawn"); + } +} diff --git a/contracts/test/foundry/KlerosCore_Appeals.t.sol b/contracts/test/foundry/KlerosCore_Appeals.t.sol index 12d07d389..8a50c56e6 100644 --- a/contracts/test/foundry/KlerosCore_Appeals.t.sol +++ b/contracts/test/foundry/KlerosCore_Appeals.t.sol @@ -342,7 +342,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { // Check jump modifier vm.prank(address(core)); vm.expectRevert(DisputeKitClassicBase.DisputeJumpedToAnotherDisputeKit.selector); - newDisputeKit.draw(disputeID, 1); + newDisputeKit.draw(disputeID, 1, round.nbVotes); // And check that draw in the new round works vm.expectEmit(true, true, true, true); @@ -491,7 +491,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { // Check jump modifier vm.prank(address(core)); vm.expectRevert(DisputeKitClassicBase.DisputeJumpedToAnotherDisputeKit.selector); - disputeKit3.draw(disputeID, 1); + disputeKit3.draw(disputeID, 1, round.nbVotes); // And check that draw in the new round works vm.expectEmit(true, true, true, true); @@ -715,7 +715,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { vm.prank(address(core)); vm.expectRevert(DisputeKitClassicBase.DisputeJumpedToAnotherDisputeKit.selector); - disputeKit3.draw(disputeID, 1); + disputeKit3.draw(disputeID, 1, round.nbVotes); core.draw(disputeID, 7); // New round requires 7 jurors vm.warp(block.timestamp + timesPerPeriod[0]); @@ -794,7 +794,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { vm.prank(address(core)); vm.expectRevert(DisputeKitClassicBase.DisputeJumpedToAnotherDisputeKit.selector); - disputeKit2.draw(disputeID, 1); + disputeKit2.draw(disputeID, 1, round.nbVotes); core.draw(disputeID, 15); // New round requires 15 jurors vm.warp(block.timestamp + timesPerPeriod[0]); From 7f64aba9a6af7e04cbba5b6d9bd0435c5c28911d Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 27 Oct 2025 12:45:54 +0000 Subject: [PATCH 3/4] refactor: moved more logic to _postDrawHook() to avoid extra push/pop --- .../dispute-kits/DisputeKitClassicBase.sol | 15 ++++--- .../dispute-kits/DisputeKitGated.sol | 5 ++- ...uteKitGatedArgentinaConsumerProtection.sol | 44 ++++++++++--------- .../dispute-kits/DisputeKitGatedShutter.sol | 5 ++- .../dispute-kits/DisputeKitSybilResistant.sol | 5 ++- 5 files changed, 40 insertions(+), 34 deletions(-) diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 525664d49..1f4294cc2 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -252,7 +252,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi function draw( uint256 _coreDisputeID, uint256 _nonce, - uint256 /*_roundNbVotes*/ + uint256 _roundNbVotes ) public virtual @@ -274,7 +274,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi return (drawnAddress, fromSubcourtID); } - if (_postDrawCheck(round, _coreDisputeID, drawnAddress)) { + if (_postDrawCheck(round, _coreDisputeID, drawnAddress, _roundNbVotes)) { Vote storage vote = round.votes.push(); vote.account = drawnAddress; round.alreadyDrawn[drawnAddress] = true; @@ -786,19 +786,20 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// /// @param _coreDisputeID ID of the dispute in the core contract. /// @param _juror Chosen address. - /// @return result Whether the address passes the check or not. + /// @return Whether the address passes the check or not. function _postDrawCheck( Round storage /*_round*/, uint256 _coreDisputeID, - address _juror - ) internal view virtual returns (bool result) { + address _juror, + uint256 /*_roundNbVotes*/ + ) internal view virtual returns (bool) { if (singleDrawPerJuror) { uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; Dispute storage dispute = disputes[localDisputeID]; Round storage round = dispute.rounds[dispute.rounds.length - 1]; - result = !round.alreadyDrawn[_juror]; + return !round.alreadyDrawn[_juror]; } else { - result = true; + return true; } } diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol index 38c5041c8..e24a87b93 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol @@ -129,9 +129,10 @@ contract DisputeKitGated is DisputeKitClassicBase { function _postDrawCheck( Round storage _round, uint256 _coreDisputeID, - address _juror + address _juror, + uint256 _roundNbVotes ) internal view override returns (bool) { - if (!super._postDrawCheck(_round, _coreDisputeID, _juror)) return false; + if (!super._postDrawCheck(_round, _coreDisputeID, _juror, _roundNbVotes)) return false; // Get the local dispute and extract token info from extraData uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGatedArgentinaConsumerProtection.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGatedArgentinaConsumerProtection.sol index bff83866f..98fb797ef 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGatedArgentinaConsumerProtection.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGatedArgentinaConsumerProtection.sol @@ -96,25 +96,11 @@ contract DisputeKitGatedArgentinaConsumerProtection is DisputeKitClassicBase { if (drawnAddress == address(0)) return (drawnAddress, fromSubcourtID); - uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; - Dispute storage dispute = disputes[localDisputeID]; - uint256 localRoundID = dispute.rounds.length - 1; if (IBalanceHolder(accreditedConsumerProtectionLawyerToken).balanceOf(drawnAddress) > 0) { // The drawnAddress is a consumer protection lawyer. + uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; + uint256 localRoundID = disputes[localDisputeID].rounds.length - 1; drawnConsumerProtectionLawyer[localDisputeID][localRoundID] = true; - } else { - // The drawnAddress is not a consumer protection lawyer. - if ( - dispute.rounds[localRoundID].votes.length == _roundNbVotes && - !drawnConsumerProtectionLawyer[localDisputeID][localRoundID] - ) { - // This is the last draw iteration and we still have not drawn a consumer protection lawyer. - // Drop the last round.votes pushed by super.draw(), so that another iteration can try again. - drawnAddress = address(0); - dispute.rounds[localRoundID].votes.pop(); - // Note that round.alreadyDrawn[drawnAddress] is not cleared because we don't know if it has been drawn more than once. - // It's fine because this DisputeKit does not enable singleDrawPerJuror. - } } return (drawnAddress, fromSubcourtID); } @@ -127,12 +113,28 @@ contract DisputeKitGatedArgentinaConsumerProtection is DisputeKitClassicBase { function _postDrawCheck( Round storage _round, uint256 _coreDisputeID, - address _juror + address _juror, + uint256 _roundNbVotes ) internal view override returns (bool) { - if (!super._postDrawCheck(_round, _coreDisputeID, _juror)) return false; - return - (IBalanceHolder(accreditedLawyerToken).balanceOf(_juror) > 0) || - (IBalanceHolder(accreditedConsumerProtectionLawyerToken).balanceOf(_juror) > 0); + if (IBalanceHolder(accreditedConsumerProtectionLawyerToken).balanceOf(_juror) == 0) { + // The juror is not a consumer protection lawyer. + uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; + Dispute storage dispute = disputes[localDisputeID]; + uint256 localRoundID = dispute.rounds.length - 1; + if ( + dispute.rounds[localRoundID].votes.length == _roundNbVotes - 1 && + !drawnConsumerProtectionLawyer[localDisputeID][localRoundID] + ) { + // This is the last draw iteration and we still have not drawn a consumer protection lawyer. + // Reject this draw so that another iteration can try again later. + return false; + } + if (IBalanceHolder(accreditedLawyerToken).balanceOf(_juror) == 0) { + // The juror does not hold either of the tokens. + return false; + } + } + return super._postDrawCheck(_round, _coreDisputeID, _juror, _roundNbVotes); } // ************************************* // diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol index ce3a68b42..ad56ff475 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol @@ -256,9 +256,10 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase { function _postDrawCheck( Round storage _round, uint256 _coreDisputeID, - address _juror + address _juror, + uint256 _roundNbVotes ) internal view override returns (bool) { - if (!super._postDrawCheck(_round, _coreDisputeID, _juror)) return false; + if (!super._postDrawCheck(_round, _coreDisputeID, _juror, _roundNbVotes)) return false; // Get the local dispute and extract token info from extraData uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol index aa76f9b45..613c26c66 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol @@ -70,8 +70,9 @@ contract DisputeKitSybilResistant is DisputeKitClassicBase { function _postDrawCheck( Round storage _round, uint256 _coreDisputeID, - address _juror + address _juror, + uint256 _roundNbVotes ) internal view override returns (bool) { - return super._postDrawCheck(_round, _coreDisputeID, _juror) && poh.isRegistered(_juror); + return super._postDrawCheck(_round, _coreDisputeID, _juror, _roundNbVotes) && poh.isRegistered(_juror); } } From 32fe70a9e4a3ff10f487b1424792f95d0bc9a9e1 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 27 Oct 2025 14:57:35 +0000 Subject: [PATCH 4/4] refactor: renamed token accreditedLawyerToken into accreditedProfessionalToken --- ...uteKitGatedArgentinaConsumerProtection.sol | 20 ++++++++-------- ...dArgentinaConsumerProtection_Drawing.t.sol | 24 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGatedArgentinaConsumerProtection.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGatedArgentinaConsumerProtection.sol index 98fb797ef..c2e539371 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGatedArgentinaConsumerProtection.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGatedArgentinaConsumerProtection.sol @@ -15,7 +15,7 @@ interface IBalanceHolder { /// @title DisputeKitGatedArgentinaConsumerProtection /// @notice Dispute kit implementation adapted from DisputeKitClassic -/// - a drawing system: proportional to staked PNK among the jurors holding a `accreditedLawyerToken` or a `accreditedConsumerProtectionLawyerToken` +/// - a drawing system: proportional to staked PNK among the jurors holding a `accreditedProfessionalToken` or a `accreditedConsumerProtectionLawyerToken` /// and at least one of the drawn jurors is holding a `accreditedConsumerProtectionLawyerToken`, /// - a vote aggregation system: plurality, /// - an incentive system: equal split between coherent votes, @@ -27,7 +27,7 @@ contract DisputeKitGatedArgentinaConsumerProtection is DisputeKitClassicBase { // * Storage * // // ************************************* // - address public accreditedLawyerToken; // The address of the accredited lawyer token. + address public accreditedProfessionalToken; // The address of the accredited professional token. address public accreditedConsumerProtectionLawyerToken; // The address of the accredited consumer protection lawyer token. mapping(uint256 localDisputeID => mapping(uint256 localRoundID => bool)) public drawnConsumerProtectionLawyer; // Maps the local dispute and round ID to the boolean indicating if the consumer protection lawyer was drawn. @@ -44,17 +44,17 @@ contract DisputeKitGatedArgentinaConsumerProtection is DisputeKitClassicBase { /// @param _owner The owner's address. /// @param _core The KlerosCore arbitrator. /// @param _wNative The wrapped native token address, typically wETH. - /// @param _accreditedLawyerToken The address of the accredited lawyer token. + /// @param _accreditedProfessionalToken The address of the accredited professional token. /// @param _accreditedConsumerProtectionLawyerToken The address of the accredited consumer protection lawyer token. function initialize( address _owner, KlerosCore _core, address _wNative, - address _accreditedLawyerToken, + address _accreditedProfessionalToken, address _accreditedConsumerProtectionLawyerToken ) external initializer { __DisputeKitClassicBase_initialize(_owner, _core, _wNative); - accreditedLawyerToken = _accreditedLawyerToken; + accreditedProfessionalToken = _accreditedProfessionalToken; accreditedConsumerProtectionLawyerToken = _accreditedConsumerProtectionLawyerToken; } @@ -68,10 +68,10 @@ contract DisputeKitGatedArgentinaConsumerProtection is DisputeKitClassicBase { // NOP } - /// @notice Changes the accredited lawyer token. - /// @param _accreditedLawyerToken The address of the accredited lawyer token. - function changeAccreditedLawyerToken(address _accreditedLawyerToken) external onlyByOwner { - accreditedLawyerToken = _accreditedLawyerToken; + /// @notice Changes the accredited professional token. + /// @param _accreditedProfessionalToken The address of the accredited lawyer token. + function changeAccreditedProfessionalToken(address _accreditedProfessionalToken) external onlyByOwner { + accreditedProfessionalToken = _accreditedProfessionalToken; } /// @notice Changes the accredited consumer protection lawyer token. @@ -129,7 +129,7 @@ contract DisputeKitGatedArgentinaConsumerProtection is DisputeKitClassicBase { // Reject this draw so that another iteration can try again later. return false; } - if (IBalanceHolder(accreditedLawyerToken).balanceOf(_juror) == 0) { + if (IBalanceHolder(accreditedProfessionalToken).balanceOf(_juror) == 0) { // The juror does not hold either of the tokens. return false; } diff --git a/contracts/test/foundry/DisputeKitGatedArgentinaConsumerProtection_Drawing.t.sol b/contracts/test/foundry/DisputeKitGatedArgentinaConsumerProtection_Drawing.t.sol index 0ea2fb383..0c81c8bfe 100644 --- a/contracts/test/foundry/DisputeKitGatedArgentinaConsumerProtection_Drawing.t.sol +++ b/contracts/test/foundry/DisputeKitGatedArgentinaConsumerProtection_Drawing.t.sol @@ -14,7 +14,7 @@ import "../../src/libraries/Constants.sol"; /// @title DisputeKitGatedArgentinaConsumerProtection_DrawingTest /// @dev Tests for DisputeKitGatedArgentinaConsumerProtection drawing logic /// This dispute kit requires: -/// - Jurors must own either accreditedLawyerToken OR accreditedConsumerProtectionLawyerToken +/// - Jurors must own either accreditedProfessionalToken OR accreditedConsumerProtectionLawyerToken /// - Each round must have at least one juror with accreditedConsumerProtectionLawyerToken contract DisputeKitGatedArgentinaConsumerProtection_DrawingTest is KlerosCore_TestBase { // ************************************* // @@ -22,15 +22,15 @@ contract DisputeKitGatedArgentinaConsumerProtection_DrawingTest is KlerosCore_Te // ************************************* // DisputeKitGatedArgentinaConsumerProtection argentinaDK; - TestERC721 accreditedLawyerToken; + TestERC721 accreditedProfessionalToken; TestERC721 accreditedConsumerProtectionLawyerToken; // ************************************* // // * Test Accounts * // // ************************************* // - address lawyer1; // Has only accredited lawyer token - address lawyer2; // Has only accredited lawyer token + address lawyer1; // Has only accredited professional token + address lawyer2; // Has only accredited professional token address consumerLawyer1; // Has only consumer protection lawyer token address consumerLawyer2; // Has only consumer protection lawyer token address bothLawyer; // Has both tokens @@ -56,7 +56,7 @@ contract DisputeKitGatedArgentinaConsumerProtection_DrawingTest is KlerosCore_Te noTokenJuror = vm.addr(15); // Deploy token contracts - accreditedLawyerToken = new TestERC721("Accredited Lawyer", "AL"); + accreditedProfessionalToken = new TestERC721("Accredited Lawyer", "AL"); accreditedConsumerProtectionLawyerToken = new TestERC721("Consumer Protection Lawyer", "CPL"); // Deploy and initialize the Argentina dispute kit @@ -66,7 +66,7 @@ contract DisputeKitGatedArgentinaConsumerProtection_DrawingTest is KlerosCore_Te owner, address(core), address(wNative), - address(accreditedLawyerToken), + address(accreditedProfessionalToken), address(accreditedConsumerProtectionLawyerToken) ); UUPSProxy proxyDK = new UUPSProxy(address(dkLogic), initData); @@ -173,8 +173,8 @@ contract DisputeKitGatedArgentinaConsumerProtection_DrawingTest is KlerosCore_Te /// Expected: Drawing never completes all required jurors due to last-iteration rejection logic function test_drawFailsWithOnlyAccreditedLawyers() public { // Mint accredited lawyer tokens (but NOT consumer protection tokens) - accreditedLawyerToken.safeMint(lawyer1); - accreditedLawyerToken.safeMint(lawyer2); + accreditedProfessionalToken.safeMint(lawyer1); + accreditedProfessionalToken.safeMint(lawyer2); // Stake in the Argentina court vm.prank(lawyer1); @@ -246,8 +246,8 @@ contract DisputeKitGatedArgentinaConsumerProtection_DrawingTest is KlerosCore_Te /// @notice Test that drawing succeeds with a mix of both lawyer types function test_drawSucceedsWithMixedLawyers() public { // Mint tokens to different jurors - accreditedLawyerToken.safeMint(lawyer1); - accreditedLawyerToken.safeMint(lawyer2); + accreditedProfessionalToken.safeMint(lawyer1); + accreditedProfessionalToken.safeMint(lawyer2); accreditedConsumerProtectionLawyerToken.safeMint(consumerLawyer1); // Stake in the Argentina court @@ -287,7 +287,7 @@ contract DisputeKitGatedArgentinaConsumerProtection_DrawingTest is KlerosCore_Te /// @notice Test drawing when a juror holds both token types function test_drawWithJurorHoldingBothTokens() public { // Mint both tokens to the same juror - accreditedLawyerToken.safeMint(bothLawyer); + accreditedProfessionalToken.safeMint(bothLawyer); accreditedConsumerProtectionLawyerToken.safeMint(bothLawyer); // Stake in the Argentina court @@ -365,7 +365,7 @@ contract DisputeKitGatedArgentinaConsumerProtection_DrawingTest is KlerosCore_Te /// @notice Test that each round independently satisfies the consumer protection lawyer requirement function test_multipleRoundsRequirement() public { // Mint tokens - accreditedLawyerToken.safeMint(lawyer1); + accreditedProfessionalToken.safeMint(lawyer1); accreditedConsumerProtectionLawyerToken.safeMint(consumerLawyer1); // Stake in the Argentina court