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]
        );
    }
}