Documentation Index
Fetch the complete documentation index at: https://kleros-mintlify-changelog-2026-05-12-1778458371.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Testing Strategy
Testing arbitrable contracts requires simulating the full dispute lifecycle. You can’t just unit test you need to interact with Kleros contracts.
Unit Testing with Mock Arbitrator
Create a minimal mock to test your contract’s logic:
// test/mocks/MockArbitrator.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "@kleros/kleros-v2-contracts/arbitration/interfaces/IArbitratorV2.sol";
contract MockArbitrator is IArbitratorV2 {
uint256 public disputeCount;
uint256 public fixedCost = 0.01 ether;
mapping(uint256 => address) public disputeArbitrable;
mapping(uint256 => uint256) public disputeChoices;
function arbitrationCost(bytes calldata) external view override returns (uint256) {
return fixedCost;
}
function createDispute(
uint256 _choices,
bytes calldata
) external payable override returns (uint256 disputeID) {
require(msg.value >= fixedCost, "Insufficient fee");
disputeID = disputeCount++;
disputeArbitrable[disputeID] = msg.sender;
disputeChoices[disputeID] = _choices;
emit DisputeCreation(disputeID, IArbitrableV2(msg.sender));
}
// Test helper: manually deliver ruling
function giveRuling(uint256 _disputeID, uint256 _ruling) external {
IArbitrableV2(disputeArbitrable[_disputeID]).rule(_disputeID, _ruling);
}
function currentRuling(uint256) external pure override returns (uint256, bool, bool) {
return (0, false, false);
}
}
Foundry Test Example
// test/Escrow.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "forge-std/Test.sol";
import "../src/Escrow.sol";
import "./mocks/MockArbitrator.sol";
contract EscrowTest is Test {
Escrow escrow;
MockArbitrator arbitrator;
address buyer = address(0x1);
address seller = address(0x2);
function setUp() public {
arbitrator = new MockArbitrator();
escrow = new Escrow(
IArbitratorV2(address(arbitrator)),
abi.encodePacked(uint96(1), uint256(3)), // court 1, 3 jurors
"/ipfs/QmTemplate"
);
vm.deal(buyer, 10 ether);
vm.deal(seller, 10 ether);
}
function testCreateTransaction() public {
vm.prank(buyer);
uint256 txID = escrow.createTransaction{value: 1 ether}(seller);
(address _buyer, address _seller, uint256 amount,,) = escrow.transactions(txID);
assertEq(_buyer, buyer);
assertEq(_seller, seller);
assertEq(amount, 1 ether);
}
function testRaiseDispute() public {
vm.prank(buyer);
uint256 txID = escrow.createTransaction{value: 1 ether}(seller);
uint256 cost = escrow.getArbitrationCost();
vm.prank(buyer);
escrow.raiseDispute{value: cost}(txID);
(,,,Escrow.Status status,) = escrow.transactions(txID);
assertEq(uint256(status), uint256(Escrow.Status.Disputed));
}
function testRulingPaysSeller() public {
// Setup
vm.prank(buyer);
uint256 txID = escrow.createTransaction{value: 1 ether}(seller);
vm.prank(buyer);
escrow.raiseDispute{value: 0.01 ether}(txID);
// Get dispute ID
(,,,,uint256 disputeID) = escrow.transactions(txID);
uint256 sellerBalanceBefore = seller.balance;
// Arbitrator delivers ruling: 1 = PaySeller
arbitrator.giveRuling(disputeID, 1);
assertEq(seller.balance, sellerBalanceBefore + 1 ether);
}
function testRulingRefundsBuyer() public {
vm.prank(buyer);
uint256 txID = escrow.createTransaction{value: 1 ether}(seller);
vm.prank(buyer);
escrow.raiseDispute{value: 0.01 ether}(txID);
(,,,,uint256 disputeID) = escrow.transactions(txID);
uint256 buyerBalanceBefore = buyer.balance;
// Ruling: 2 = RefundBuyer
arbitrator.giveRuling(disputeID, 2);
assertEq(buyer.balance, buyerBalanceBefore + 1 ether);
}
function testOnlyArbitratorCanRule() public {
vm.prank(buyer);
uint256 txID = escrow.createTransaction{value: 1 ether}(seller);
vm.prank(buyer);
escrow.raiseDispute{value: 0.01 ether}(txID);
(,,,,uint256 disputeID) = escrow.transactions(txID);
vm.prank(address(0xBAD));
vm.expectRevert("Only arbitrator");
escrow.rule(disputeID, 1);
}
function testCannotRuleTwice() public {
vm.prank(buyer);
uint256 txID = escrow.createTransaction{value: 1 ether}(seller);
vm.prank(buyer);
escrow.raiseDispute{value: 0.01 ether}(txID);
(,,,,uint256 disputeID) = escrow.transactions(txID);
arbitrator.giveRuling(disputeID, 1);
vm.expectRevert("Not disputed");
arbitrator.giveRuling(disputeID, 2);
}
}
Run with:
Testnet Testing (Arbitrum Sepolia)
Setup
-
Get testnet ETH
# Arbitrum Sepolia faucet
https://faucet.quicknode.com/arbitrum/sepolia
-
Deploy your contract
forge script script/Deploy.s.sol --rpc-url arbitrum_sepolia --broadcast
-
Verify contract addresses
Check kleros-v2 deployments for testnet addresses.
Creating a Test Dispute
// scripts/createTestDispute.js
const { ethers } = require("hardhat");
async function main() {
const escrow = await ethers.getContractAt("Escrow", ESCROW_ADDRESS);
// 1. Create transaction
const tx = await escrow.createTransaction(SELLER_ADDRESS, {
value: ethers.parseEther("0.001")
});
await tx.wait();
console.log("Transaction created");
// 2. Get arbitration cost
const cost = await escrow.getArbitrationCost();
console.log("Arbitration cost:", ethers.formatEther(cost), "ETH");
// 3. Raise dispute
const disputeTx = await escrow.raiseDispute(0, { value: cost });
const receipt = await disputeTx.wait();
// 4. Find dispute ID from events
const event = receipt.logs.find(log =>
log.topics[0] === ethers.id("DisputeRequest(address,uint256,uint256,uint256,string)")
);
console.log("Dispute created! Check Kleros Court UI");
}
Monitoring Dispute Progress
// Track dispute status
async function checkDispute(escrow, txID) {
const [ruling, tied, overridden] = await escrow.getDisputeStatus(txID);
console.log({
ruling: ruling.toString(),
tied,
overridden
});
}
Test Scenarios Checklist
Happy Path
Edge Cases
Security
Gas Optimization
Debugging Tips
Common Issues
'Insufficient fee' on createDispute
Arbitration cost changes. Always fetch fresh:uint256 cost = arbitrator.arbitrationCost(extraData);
- Dispute may still be in voting/appeal period
- Check dispute status on Kleros Court UI
- Testnet disputes can take days if no jurors
Verify you’re using the correct network’s KlerosCore address. Testnet ≠ mainnet.
Evidence not showing in UI
- Check
Evidence event was emitted with correct _evidenceGroupID
- Verify JSON is valid
- IPFS content may take time to propagate
Event Debugging
// Listen for all relevant events
escrow.on("DisputeRequest", (arbitrator, disputeID, externalID, templateId, templateUri) => {
console.log("Dispute created:", { disputeID, externalID });
});
escrow.on("Ruling", (arbitrator, disputeID, ruling) => {
console.log("Ruling received:", { disputeID, ruling });
});
escrow.on("Evidence", (arbitrator, evidenceGroupID, party, evidence) => {
console.log("Evidence submitted:", { evidenceGroupID, party });
});
Mainnet Fork Testing
Test against production state without spending real ETH:
# Foundry
forge test --fork-url https://arb1.arbitrum.io/rpc -vvv
# Hardhat
npx hardhat test --network hardhat --fork https://arb1.arbitrum.io/rpc
function testWithRealKleros() public {
// Use actual mainnet KlerosCore address
IArbitratorV2 realArbitrator = IArbitratorV2(0x33d0b8879368acD8ca868e656Ade97bBcfeB12BA);
uint256 cost = realArbitrator.arbitrationCost(extraData);
// Verify cost is reasonable
assertGt(cost, 0);
assertLt(cost, 1 ether);
}
Next Steps
Production Checklist
Pre-deployment security and operational checklist