polyglot-alpha / contracts /src /BuilderFeeRouter.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";
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function balanceOf(address owner) external view returns (uint256);
}
interface IReputationRegistry {
function updateOnFee(address agent, uint256 amount) external;
}
/// @title BuilderFeeRouter
/// @notice Records builder-fee accrual to winning translators per Polymarket
/// fill event, lets translators claim their accumulated USDC, and
/// exposes a top-10 leaderboard view.
/// @dev The operator (Polymarket fill listener service) is the sole address
/// allowed to invoke recordFill. recordFill assumes the corresponding
/// USDC has already been deposited into this contract (e.g. via the
/// builder-fee payout). For convenience there is also `fund()` so the
/// operator can pre-deposit USDC tokens.
contract BuilderFeeRouter is ReentrancyGuard {
// ---------------------------------------------------------------
// Constants
// ---------------------------------------------------------------
/// @notice Number of leaderboard entries returned by getLeaderboard().
uint256 public constant LEADERBOARD_SIZE = 10;
// ---------------------------------------------------------------
// Storage
// ---------------------------------------------------------------
address public operator;
IERC20 public immutable usdc;
IReputationRegistry public reputation;
/// @notice Total fees earned (lifetime) per translator, including already-claimed.
mapping(address => uint256) public cumulativeFees;
/// @notice Currently claimable USDC balance per translator.
mapping(address => uint256) public claimable;
/// @notice Number of fills recorded for a translator.
mapping(address => uint256) public fillCount;
/// @notice All translators ever credited (for leaderboard iteration).
address[] public translators;
mapping(address => bool) internal isKnownTranslator;
// ---------------------------------------------------------------
// Events
// ---------------------------------------------------------------
event PayoutAccrued(
address indexed translator,
string marketId,
uint256 amount,
uint256 newCumulative
);
event FeesClaimed(address indexed translator, uint256 totalAmount);
event ReputationRegistrySet(address indexed registry);
event Funded(address indexed from, uint256 amount);
// ---------------------------------------------------------------
// Modifiers
// ---------------------------------------------------------------
modifier onlyOperator() {
require(msg.sender == operator, "not operator");
_;
}
// ---------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------
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;
}
// ---------------------------------------------------------------
// Funding (so the contract has USDC to pay out)
// ---------------------------------------------------------------
/// @notice Pull USDC from caller into the router. Operator usually calls
/// this after withdrawing builder fees from Polymarket.
function fund(uint256 amount) external nonReentrant {
require(amount > 0, "zero fund");
bool ok = usdc.transferFrom(msg.sender, address(this), amount);
require(ok, "usdc transferFrom failed");
emit Funded(msg.sender, amount);
}
// ---------------------------------------------------------------
// Recording & claiming
// ---------------------------------------------------------------
/// @notice Record a Polymarket fill that credits builder fees to a translator.
/// @param marketId Off-chain Polymarket market identifier (passed through in event).
/// @param fillAmount USDC amount in the token's base units to credit to translator.
/// @param translator Address of the winning translator entitled to the fee.
function recordFill(
string calldata marketId,
uint256 fillAmount,
address translator
) external onlyOperator {
require(translator != address(0), "zero translator");
require(fillAmount > 0, "zero fill");
cumulativeFees[translator] += fillAmount;
claimable[translator] += fillAmount;
fillCount[translator] += 1;
if (!isKnownTranslator[translator]) {
isKnownTranslator[translator] = true;
translators.push(translator);
}
if (address(reputation) != address(0)) {
reputation.updateOnFee(translator, fillAmount);
}
emit PayoutAccrued(translator, marketId, fillAmount, cumulativeFees[translator]);
}
/// @notice Claim all currently-claimable USDC for `translator`. Anyone may
/// trigger this on behalf of the translator; funds always flow to
/// the translator address.
function claimFees(address translator) external nonReentrant {
uint256 amount = claimable[translator];
require(amount > 0, "nothing to claim");
claimable[translator] = 0;
bool ok = usdc.transfer(translator, amount);
require(ok, "usdc transfer failed");
emit FeesClaimed(translator, amount);
}
// ---------------------------------------------------------------
// Views
// ---------------------------------------------------------------
function getCumulativeFees(address translator) external view returns (uint256) {
return cumulativeFees[translator];
}
function getTranslatorCount() external view returns (uint256) {
return translators.length;
}
/// @notice Returns the top-LEADERBOARD_SIZE translators by lifetime fees.
/// Performs an O(N*K) in-memory selection where N=translators.length
/// and K=LEADERBOARD_SIZE. Suitable for hackathon-scale leaderboards;
/// if N grows past ~1000 consider a paginated off-chain sort.
function getLeaderboard()
external
view
returns (address[] memory topAddrs, uint256[] memory topFees)
{
uint256 n = translators.length;
uint256 k = LEADERBOARD_SIZE;
if (k > n) {
k = n;
}
topAddrs = new address[](k);
topFees = new uint256[](k);
// Track which entries we've already placed via "used" mask.
bool[] memory used = new bool[](n);
for (uint256 slot = 0; slot < k; slot++) {
uint256 bestIdx = type(uint256).max;
uint256 bestVal = 0;
for (uint256 i = 0; i < n; i++) {
if (used[i]) continue;
uint256 v = cumulativeFees[translators[i]];
if (bestIdx == type(uint256).max || v > bestVal) {
bestIdx = i;
bestVal = v;
}
}
if (bestIdx == type(uint256).max) {
// shouldn't happen given k <= n but defensive
break;
}
used[bestIdx] = true;
topAddrs[slot] = translators[bestIdx];
topFees[slot] = bestVal;
}
}
}