polyglot-alpha / contracts /src /TranslationAuction.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 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];
}
}