polyglot-alpha / contracts /src /JudgePanel.sol
licaomeng
deploy: main@8970ffb → HF Spaces (2026-05-27T05:19Z)
88d2f2a
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/// @notice Minimal ERC20 surface used by JudgePanel.
interface IERC20 {
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address owner) external view returns (uint256);
}
/// @title JudgePanel
/// @notice On-chain registry + attestation store for the 11-judge ensemble
/// described in README §5.6 / §5.22 (3 translation MQM judges + 8
/// style-alignment judges). Each judge stakes USDC; bias / collusion
/// is punished by `slashJudge`. Verdicts are stamped to chain via
/// `recordAttestation` so any party can audit "who judged what."
/// @dev Two judge classes:
/// - Translation judge: 2 USDC stake (BLEU/COMET/MQM scoring).
/// - Style-alignment judge: 1 USDC stake (D1-D8 dimensions).
/// Stake constants assume the live USDC token uses 6 decimals
/// (Arc-testnet MockUSDC). If the deploy target uses 18-dec USDC,
/// redeploy with adjusted constants.
contract JudgePanel is ReentrancyGuard {
// ---------------------------------------------------------------
// Constants (per README §5.6 / §5.22)
// ---------------------------------------------------------------
/// @notice Translation-judge USDC stake (2 USDC at 6-decimal precision).
uint256 public constant TRANSLATION_JUDGE_STAKE = 2_000_000;
/// @notice Style-alignment-judge USDC stake (1 USDC at 6-decimal precision).
uint256 public constant STYLE_JUDGE_STAKE = 1_000_000;
string public constant JUDGE_TYPE_TRANSLATION = "translation";
string public constant JUDGE_TYPE_STYLE = "style";
// ---------------------------------------------------------------
// Storage
// ---------------------------------------------------------------
address public operator;
IERC20 public immutable usdc;
/// @notice Currently held USDC stake per judge address.
mapping(address => uint256) public judgeStakes;
mapping(address => bool) public isTranslationJudge;
mapping(address => bool) public isStyleJudge;
/// @notice Number of attestations recorded for a judge (for collusion
/// analysis off-chain — the on-chain stake-slash decision still
/// requires an operator call).
mapping(address => uint256) public attestationCount;
// ---------------------------------------------------------------
// Events
// ---------------------------------------------------------------
event JudgeRegistered(address indexed judge, string judgeType, uint256 stake);
event AttestationRecorded(
bytes32 indexed eventId,
address indexed judge,
uint256 score,
bytes32 attestationHash
);
event JudgeSlashed(address indexed judge, uint256 amount, string reason);
event JudgeWithdrew(address indexed judge, uint256 amount);
// ---------------------------------------------------------------
// Modifiers
// ---------------------------------------------------------------
modifier onlyOperator() {
require(msg.sender == operator, "not operator");
_;
}
// ---------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------
constructor(address _usdc) {
require(_usdc != address(0), "usdc zero");
operator = msg.sender;
usdc = IERC20(_usdc);
}
function transferOperator(address newOperator) external onlyOperator {
require(newOperator != address(0), "zero op");
operator = newOperator;
}
// ---------------------------------------------------------------
// Judge registration
// ---------------------------------------------------------------
/// @notice Stake 2 USDC and join the translation-MQM sub-panel. Caller
/// must have approved this contract for the stake amount first.
function registerTranslationJudge() external nonReentrant {
require(!isTranslationJudge[msg.sender], "already translation judge");
// Checks-Effects-Interactions: flip the role + stake bookkeeping BEFORE
// the external transferFrom call so any reentrant path observes the
// up-to-date storage. ReentrancyGuard provides defense in depth.
judgeStakes[msg.sender] += TRANSLATION_JUDGE_STAKE;
isTranslationJudge[msg.sender] = true;
bool ok = usdc.transferFrom(msg.sender, address(this), TRANSLATION_JUDGE_STAKE);
require(ok, "usdc transferFrom failed");
emit JudgeRegistered(msg.sender, JUDGE_TYPE_TRANSLATION, TRANSLATION_JUDGE_STAKE);
}
/// @notice Stake 1 USDC and join the style-alignment sub-panel. Caller
/// must have approved this contract for the stake amount first.
function registerStyleJudge() external nonReentrant {
require(!isStyleJudge[msg.sender], "already style judge");
// Checks-Effects-Interactions: see registerTranslationJudge above.
judgeStakes[msg.sender] += STYLE_JUDGE_STAKE;
isStyleJudge[msg.sender] = true;
bool ok = usdc.transferFrom(msg.sender, address(this), STYLE_JUDGE_STAKE);
require(ok, "usdc transferFrom failed");
emit JudgeRegistered(msg.sender, JUDGE_TYPE_STYLE, STYLE_JUDGE_STAKE);
}
// ---------------------------------------------------------------
// Attestations (operator-pushed; off-chain orchestrator computes the
// attestation hash + score and stamps it here for audit)
// ---------------------------------------------------------------
/// @notice Record an attestation produced by a judge for a specific event.
/// @param eventId The auction / event identifier.
/// @param judge Judge wallet (must be registered).
/// @param score Score in the judge's natural units (e.g. MQM 0-100).
/// @param attestationHash keccak256 of the off-chain JSON attestation.
function recordAttestation(
bytes32 eventId,
address judge,
uint256 score,
bytes32 attestationHash
) external onlyOperator {
require(
isTranslationJudge[judge] || isStyleJudge[judge],
"not a registered judge"
);
attestationCount[judge] += 1;
emit AttestationRecorded(eventId, judge, score, attestationHash);
}
// ---------------------------------------------------------------
// Slashing
// ---------------------------------------------------------------
/// @notice Operator slashes a judge's stake for systemic bias or collusion.
/// Slashed USDC stays in the contract treasury (operator-controlled).
function slashJudge(address judge, uint256 amount, string calldata reason)
external
onlyOperator
{
uint256 current = judgeStakes[judge];
require(amount > 0 && amount <= current, "bad slash amount");
judgeStakes[judge] = current - amount;
emit JudgeSlashed(judge, amount, reason);
}
/// @notice Operator-triggered exit: refund a judge's remaining stake and
/// remove them from the panel. Useful for graceful retirement.
function withdrawJudge(address judge) external onlyOperator {
uint256 amount = judgeStakes[judge];
require(amount > 0, "no stake");
judgeStakes[judge] = 0;
isTranslationJudge[judge] = false;
isStyleJudge[judge] = false;
bool ok = usdc.transfer(judge, amount);
require(ok, "usdc transfer failed");
emit JudgeWithdrew(judge, amount);
}
// ---------------------------------------------------------------
// Views
// ---------------------------------------------------------------
function getJudgeInfo(address judge)
external
view
returns (
uint256 stake,
bool translation,
bool style,
uint256 attestations
)
{
return (
judgeStakes[judge],
isTranslationJudge[judge],
isStyleJudge[judge],
attestationCount[judge]
);
}
}