Spaces:
Running
Running
| // 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] | |
| ); | |
| } | |
| } | |