Spaces:
Running
Running
File size: 15,416 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 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 | // SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/// @notice Minimal ERC20 interface; only the calls TranslationAuction makes.
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);
}
interface IReputationRegistry {
function getReputation(address agent) external view returns (uint256);
function updateOnAuction(address agent, bool won) external;
}
/// @title TranslationAuction
/// @notice Sealed-bid (open-book in practice for hackathon) auction where AI
/// translation agents bid USDC for the right to translate a Chinese
/// news event into a Polymarket-shaped market question. Each auction
/// has a fixed 60-second submission window. The winner is chosen by
/// the largest reputation-adjusted bid: bid / max(reputation, 1.0).
contract TranslationAuction is ReentrancyGuard {
// ---------------------------------------------------------------
// Constants
// ---------------------------------------------------------------
/// @notice Mandatory stake required to register as a bidding agent (5 USDC).
/// USDC has 6 decimals on most chains but Arc testnet's bridged
/// USDC is 18-decimal; we keep this as 5 * 1e6 and let the deploy
/// script swap based on the live token's decimals.
uint256 public constant REGISTRATION_STAKE = 5_000_000; // 5 USDC @ 6 decimals
/// @notice Auction submission window length in seconds.
uint256 public constant AUCTION_WINDOW_SECONDS = 60;
/// @notice Fixed-point ONE used to read ReputationRegistry scores (1e18).
uint256 public constant REPUTATION_ONE = 1e18;
/// @notice Minimum reputation an agent must hold to submit a bid (README §5.6:
/// reputation gate at 0.7). Agents below this threshold are rejected
/// to keep low-quality bidders out of the auction.
uint256 public constant MIN_REPUTATION_TO_BID = 7e17;
/// @notice Length of the post-settlement window during which the winner's
/// registration stake remains slashable (README §5.6: "5 USDC stays
/// locked for 72h, slashable on Polymarket review").
uint256 public constant SLASHABLE_WINDOW_SECONDS = 72 hours;
// ---------------------------------------------------------------
// Storage
// ---------------------------------------------------------------
struct Auction {
bytes32 eventHash;
uint256 deadline;
address[] bidders;
mapping(address => uint256) bids;
mapping(address => bytes32) candidateMetadataHashes;
address winner;
uint256 winningBid;
bool settled;
bool opened;
}
/// @notice operator (deployer) is allowed to open auctions, settle, slash.
address public operator;
/// @notice USDC token used for stakes and bids.
IERC20 public immutable usdc;
/// @notice Reputation registry consulted at settle-time for reputation-weighting.
IReputationRegistry public reputation;
/// @notice Per-agent unlocked stake balance.
mapping(address => uint256) public stakes;
/// @notice Stake amount currently locked because the agent is registered
/// (cannot be withdrawn until the agent explicitly unregisters).
mapping(address => uint256) public lockedStakes;
/// @notice Whether an address is currently registered as a bidder.
mapping(address => bool) public registered;
/// @notice eventId -> Auction. eventId is computed off-chain (typically
/// keccak256 of news article URL + cutoff timestamp).
mapping(bytes32 => Auction) internal auctions;
/// @notice eventId -> unix timestamp at which the winner's locked
/// registration stake becomes withdrawable. Populated at
/// `settleAuction` time as `block.timestamp + SLASHABLE_WINDOW_SECONDS`
/// (72h). Until this time, the operator may invoke `slashStake`
/// on the winner for malformed submissions / quality failures.
mapping(bytes32 => uint256) public reputationStakeUnlockAt;
/// @notice agent -> latest unlock timestamp from any auction they won.
/// Tracked separately so `withdrawStake` can block withdrawals
/// without scanning all eventIds.
mapping(address => uint256) public stakeUnlockAt;
// ---------------------------------------------------------------
// Events
// ---------------------------------------------------------------
event AgentRegistered(address indexed agent, uint256 stake);
event StakeWithdrawn(address indexed agent, uint256 amount);
event AuctionOpened(bytes32 indexed eventId, bytes32 eventHash, uint256 deadline);
event BidSubmitted(
bytes32 indexed eventId,
address indexed bidder,
uint256 bidAmount,
bytes32 candidateHash
);
event AuctionSettled(bytes32 indexed eventId, address indexed winner, uint256 winningBid);
event StakeSlashed(address indexed agent, uint256 amount, string reason);
event ReputationRegistrySet(address indexed registry);
event SlashableWindowOpened(
bytes32 indexed eventId,
address indexed winner,
uint256 unlockAt
);
// ---------------------------------------------------------------
// Modifiers
// ---------------------------------------------------------------
modifier onlyOperator() {
require(msg.sender == operator, "not operator");
_;
}
// ---------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------
/// @param _usdc Arc-testnet USDC ERC20 address.
/// @param _reputation Optional address of an already-deployed ReputationRegistry.
/// Pass address(0) and call setReputationRegistry later if it has not
/// been deployed yet.
constructor(address _usdc, address _reputation) {
require(_usdc != address(0), "usdc zero");
operator = msg.sender;
usdc = IERC20(_usdc);
if (_reputation != address(0)) {
reputation = IReputationRegistry(_reputation);
}
}
function setReputationRegistry(address _reputation) external onlyOperator {
require(_reputation != address(0), "reputation zero");
reputation = IReputationRegistry(_reputation);
emit ReputationRegistrySet(_reputation);
}
function transferOperator(address newOperator) external onlyOperator {
require(newOperator != address(0), "zero op");
operator = newOperator;
}
// ---------------------------------------------------------------
// Agent registration
// ---------------------------------------------------------------
/// @notice Register as a bidding agent by transferring REGISTRATION_STAKE
/// USDC to the contract. Caller must have approved this contract
/// for at least REGISTRATION_STAKE first.
function registerAgent() external nonReentrant {
require(!registered[msg.sender], "already registered");
// Checks-Effects-Interactions: mark agent registered and stake-locked
// BEFORE the external transferFrom call. A reentrant call into
// registerAgent will hit the "already registered" guard, and
// ReentrancyGuard provides defense in depth.
stakes[msg.sender] += REGISTRATION_STAKE;
lockedStakes[msg.sender] += REGISTRATION_STAKE;
registered[msg.sender] = true;
bool ok = usdc.transferFrom(msg.sender, address(this), REGISTRATION_STAKE);
require(ok, "usdc transferFrom failed");
emit AgentRegistered(msg.sender, REGISTRATION_STAKE);
}
/// @notice Withdraw any UN-locked stake balance to the caller. The
/// registration stake stays locked until the operator unregisters
/// the agent (via slash with zero amount, or future extension).
/// If the caller recently won an auction, the call reverts until
/// the 72h slashable window has elapsed (README §5.6).
function withdrawStake() external nonReentrant {
uint256 unlockAt = stakeUnlockAt[msg.sender];
require(block.timestamp >= unlockAt, "slashable window open");
uint256 locked = lockedStakes[msg.sender];
uint256 total = stakes[msg.sender];
require(total > locked, "no unlocked stake");
uint256 amount = total - locked;
stakes[msg.sender] = locked;
bool ok = usdc.transfer(msg.sender, amount);
require(ok, "usdc transfer failed");
emit StakeWithdrawn(msg.sender, amount);
}
// ---------------------------------------------------------------
// Auction lifecycle
// ---------------------------------------------------------------
function openAuction(bytes32 eventId, bytes32 eventHash) external onlyOperator {
Auction storage a = auctions[eventId];
require(!a.opened, "already opened");
a.eventHash = eventHash;
a.deadline = block.timestamp + AUCTION_WINDOW_SECONDS;
a.opened = true;
emit AuctionOpened(eventId, eventHash, a.deadline);
}
/// @notice Submit a bid for an open auction.
/// @param eventId The auction id (must already be opened).
/// @param bidAmount Bid in USDC (held as commitment only — not transferred
/// until settle; the auction is a sealed-bid commitment auction for
/// the hackathon).
/// @param candidateHash keccak256 of the candidate translation JSON blob.
function submitBid(bytes32 eventId, uint256 bidAmount, bytes32 candidateHash) external {
Auction storage a = auctions[eventId];
require(a.opened, "not opened");
require(!a.settled, "settled");
require(block.timestamp < a.deadline, "window closed");
require(registered[msg.sender], "not registered");
require(bidAmount > 0, "zero bid");
// Reputation gate (README §5.6 final mechanism design): agents below
// 0.7 reputation are excluded from the auction. Unknown agents resolve
// to ONE (1.0) from ReputationRegistry.getReputation, so newcomers pass.
if (address(reputation) != address(0)) {
uint256 rep = reputation.getReputation(msg.sender);
require(rep >= MIN_REPUTATION_TO_BID, "reputation gate");
}
// First-time bid for this auction -> push into bidders list.
if (a.bids[msg.sender] == 0) {
a.bidders.push(msg.sender);
}
a.bids[msg.sender] = bidAmount;
a.candidateMetadataHashes[msg.sender] = candidateHash;
emit BidSubmitted(eventId, msg.sender, bidAmount, candidateHash);
}
/// @notice Settle the auction. Picks the bidder with the largest score:
/// score = bid / max(reputation, 1.0).
/// Ties are broken by first-seen (lower bidder index).
function settleAuction(bytes32 eventId) external onlyOperator {
Auction storage a = auctions[eventId];
require(a.opened, "not opened");
require(!a.settled, "already settled");
require(block.timestamp >= a.deadline, "window open");
uint256 nBidders = a.bidders.length;
address bestBidder = address(0);
uint256 bestScore = 0;
uint256 bestBid = 0;
for (uint256 i = 0; i < nBidders; i++) {
address bidder = a.bidders[i];
uint256 bid = a.bids[bidder];
uint256 rep = address(reputation) == address(0)
? REPUTATION_ONE
: reputation.getReputation(bidder);
if (rep < REPUTATION_ONE) {
rep = REPUTATION_ONE; // floor at 1.0 per spec
}
// score = bid * 1e18 / rep (keeps integer precision)
uint256 score = (bid * REPUTATION_ONE) / rep;
if (score > bestScore) {
bestScore = score;
bestBidder = bidder;
bestBid = bid;
}
}
a.winner = bestBidder;
a.winningBid = bestBid;
a.settled = true;
// Push reputation updates: winner gets won=true, losers get won=false.
if (address(reputation) != address(0)) {
for (uint256 i = 0; i < nBidders; i++) {
address bidder = a.bidders[i];
reputation.updateOnAuction(bidder, bidder == bestBidder);
}
}
// Open the 72-hour slashable window on the winner's stake (README §5.6).
// Operator may call `slashStake` during this window for malformed
// submissions; the winner's `withdrawStake` is blocked until expiry.
if (bestBidder != address(0)) {
uint256 unlockAt = block.timestamp + SLASHABLE_WINDOW_SECONDS;
reputationStakeUnlockAt[eventId] = unlockAt;
if (unlockAt > stakeUnlockAt[bestBidder]) {
stakeUnlockAt[bestBidder] = unlockAt;
}
emit SlashableWindowOpened(eventId, bestBidder, unlockAt);
}
emit AuctionSettled(eventId, bestBidder, bestBid);
}
// ---------------------------------------------------------------
// Operator: stake slashing
// ---------------------------------------------------------------
/// @notice Operator can slash an agent's stake (e.g. on quality failure
/// flagged by the 11-judge ensemble). Slashed funds remain in
/// the contract treasury (operator-controlled).
function slashStake(address agent, uint256 amount, string calldata reason)
external
onlyOperator
{
uint256 current = stakes[agent];
require(amount > 0 && amount <= current, "bad slash amount");
stakes[agent] = current - amount;
if (lockedStakes[agent] >= amount) {
lockedStakes[agent] -= amount;
} else {
lockedStakes[agent] = 0;
}
emit StakeSlashed(agent, amount, reason);
}
// ---------------------------------------------------------------
// Views
// ---------------------------------------------------------------
function getAuction(bytes32 eventId)
external
view
returns (
bytes32 eventHash,
uint256 deadline,
address winner,
uint256 winningBid,
bool settled,
bool opened,
uint256 bidderCount
)
{
Auction storage a = auctions[eventId];
return (
a.eventHash,
a.deadline,
a.winner,
a.winningBid,
a.settled,
a.opened,
a.bidders.length
);
}
function getBid(bytes32 eventId, address bidder)
external
view
returns (uint256 bidAmount, bytes32 candidateHash)
{
Auction storage a = auctions[eventId];
return (a.bids[bidder], a.candidateMetadataHashes[bidder]);
}
function getBidder(bytes32 eventId, uint256 index) external view returns (address) {
return auctions[eventId].bidders[index];
}
}
|