Spaces:
Running
Running
File size: 8,381 Bytes
88d2f2a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | // 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]
);
}
}
|