Skip to main content

Overview

The Proteus protocol uses two payout manager contracts for distributing platform fees to stakeholders:
  1. PayoutManager - Basic payout distribution (deployed)
  2. DistributedPayoutManager - Advanced multi-stakeholder distribution with Genesis NFT holder rewards (recommended)
Both contracts manage the 7% platform fee collected from market resolution.

Contract Addresses (BASE Sepolia)

ContractAddress
PayoutManager0x88d399C949Ff2f1aaa8eA5a859Ae4d97c74f6871
DistributedPayoutManagerNot yet deployed

Fee Distribution

The 7% platform fee is distributed across multiple stakeholders:
RecipientShare of Fees% of Volume
Genesis NFT Holders20%1.4%
Oracles28.6%2.0%
Market Creators14.3%1.0%
Node Operators14.3%1.0%
Builder Pool28.6%2.0%
Example: On a 10 ETH market, the 7% fee is 0.7 ETH. Genesis NFT holders receive 0.14 ETH (20% of 0.7 ETH), distributed equally across 100 NFTs = 0.0014 ETH per NFT.

DistributedPayoutManager

The advanced payout manager with multi-stakeholder distribution.

Architecture

DistributedPayoutManager.sol:57-70
contract DistributedPayoutManager is ReentrancyGuard, Pausable {
    // Fee distribution percentages (out of 700 = 7%)
    uint16 public constant GENESIS_SHARE = 20;      // 0.2% (0.002% per NFT with 100 NFTs)
    uint16 public constant ORACLE_SHARE = 180;      // 1.8%
    uint16 public constant NODE_SHARE = 100;        // 1%
    uint16 public constant CREATOR_SHARE = 100;     // 1%
    uint16 public constant BUILDER_POOL_SHARE = 200; // 2%
    uint16 public constant BITTENSOR_POOL_SHARE = 100; // 1%
    uint16 public constant TOTAL_FEE = 700;         // 7% total
    
    IPredictionMarket public predictionMarket;
    IGenesisNFT public genesisNFT;
}

Core Functions

distributeFees

Distribute platform fees for a resolved market across all stakeholders.
_marketId
uint256
The market ID to distribute fees for
DistributedPayoutManager.sol:124-182
function distributeFees(uint256 _marketId) external nonReentrant whenNotPaused {
    (
        address creator,
        ,
        ,
        ,
        bool resolved,
        ,
        ,
        ,
        ,
        ,
        uint256 platformFeeCollected
    ) = predictionMarket.markets(_marketId);
    
    require(resolved, "Market not resolved");
    require(platformFeeCollected > 0, "No fees to distribute");
    
    // Calculate distribution amounts
    uint256 genesisReward = (platformFeeCollected * GENESIS_SHARE) / TOTAL_FEE;
    uint256 oracleReward = (platformFeeCollected * ORACLE_SHARE) / TOTAL_FEE;
    uint256 nodeReward = (platformFeeCollected * NODE_SHARE) / TOTAL_FEE;
    uint256 creatorReward = (platformFeeCollected * CREATOR_SHARE) / TOTAL_FEE;
    uint256 builderReward = (platformFeeCollected * BUILDER_POOL_SHARE) / TOTAL_FEE;
    uint256 bittensorReward = (platformFeeCollected * BITTENSOR_POOL_SHARE) / TOTAL_FEE;
    
    // Distribute to Genesis NFT holders
    _distributeToGenesisHolders(_marketId, genesisReward);
    
    // Distribute to oracles who participated
    _distributeToOracles(_marketId, oracleReward);
    
    // Distribute to active nodes
    _distributeToNodes(nodeReward);
    
    // Reward market creator
    unclaimedRewards[creator] += creatorReward;
    
    // Deposit to reward pools
    builderRewardPool.deposit{value: builderReward}();
    bittensorRewardPool.deposit{value: bittensorReward}();
    
    emit FeesDistributed(
        _marketId,
        genesisReward,
        oracleReward,
        nodeReward,
        creatorReward,
        builderReward,
        bittensorReward
    );
}
Events: FeesDistributed(uint256 indexed marketId, uint256 genesisRewards, uint256 oracleRewards, uint256 nodeRewards, uint256 creatorReward, uint256 builderPoolDeposit, uint256 bittensorPoolDeposit)

claimRewards

Claim accumulated rewards from multiple markets. DistributedPayoutManager.sol:317-327
function claimRewards() external nonReentrant {
    uint256 amount = unclaimedRewards[msg.sender];
    require(amount > 0, "No rewards to claim");
    
    unclaimedRewards[msg.sender] = 0;
    
    (bool success, ) = payable(msg.sender).call{value: amount}("");
    require(success, "Transfer failed");
    
    emit RewardClaimed(msg.sender, amount);
}
Events: RewardClaimed(address indexed recipient, uint256 amount)

Genesis NFT Distribution

Genesis NFT holders receive rewards proportional to the number of NFTs they hold. DistributedPayoutManager.sol:188-226
function _distributeToGenesisHolders(uint256 _marketId, uint256 _totalReward) internal {
    if (_totalReward == 0 || address(genesisNFT) == address(0)) return;
    
    uint256 totalSupply = genesisNFT.totalSupply();
    if (totalSupply == 0) return;
    
    // Track unique holders to avoid double rewards
    address[] memory processedHolders = new address[](totalSupply);
    uint256 uniqueHolderCount = 0;
    
    // Calculate reward per NFT
    uint256 rewardPerNFT = _totalReward / totalSupply;
    
    // Distribute to each NFT holder
    for (uint256 tokenId = 1; tokenId <= totalSupply; tokenId++) {
        address holder = genesisNFT.ownerOf(tokenId);
        
        // Check if we've already processed this holder
        bool alreadyProcessed = false;
        for (uint256 j = 0; j < uniqueHolderCount; j++) {
            if (processedHolders[j] == holder) {
                alreadyProcessed = true;
                break;
            }
        }
        
        if (!alreadyProcessed) {
            // Count how many NFTs this holder owns
            uint256 holderBalance = genesisNFT.balanceOf(holder);
            uint256 holderReward = rewardPerNFT * holderBalance;
            
            unclaimedRewards[holder] += holderReward;
            emit GenesisHolderRewarded(holder, holderReward, _marketId);
            
            processedHolders[uniqueHolderCount] = holder;
            uniqueHolderCount++;
        }
    }
}
Logic:
  1. Calculate reward per NFT: totalReward / 100
  2. Iterate through all 100 NFTs
  3. Track unique holders to avoid double-counting
  4. For each unique holder, multiply reward per NFT by their balance
  5. Accumulate rewards in unclaimedRewards mapping
Events: GenesisHolderRewarded(address indexed holder, uint256 amount, uint256 marketId)

Oracle Distribution

Oracles receive rewards weighted by their contribution to market resolution. DistributedPayoutManager.sol:231-257
function _distributeToOracles(uint256 _marketId, uint256 _totalReward) internal {
    address[] memory oracles = marketOracles[_marketId];
    if (oracles.length == 0 || _totalReward == 0) return;
    
    // Calculate total contributions
    uint256 totalContributions = 0;
    for (uint256 i = 0; i < oracles.length; i++) {
        totalContributions += oracleContributions[_marketId][oracles[i]];
    }
    
    if (totalContributions == 0) {
        // Equal distribution if no contributions tracked
        uint256 perOracleReward = _totalReward / oracles.length;
        for (uint256 i = 0; i < oracles.length; i++) {
            unclaimedRewards[oracles[i]] += perOracleReward;
            emit OracleRewarded(oracles[i], perOracleReward, _marketId);
        }
    } else {
        // Weighted distribution based on contributions
        for (uint256 i = 0; i < oracles.length; i++) {
            uint256 contribution = oracleContributions[_marketId][oracles[i]];
            uint256 reward = (_totalReward * contribution) / totalContributions;
            unclaimedRewards[oracles[i]] += reward;
            emit OracleRewarded(oracles[i], reward, _marketId);
        }
    }
}
Events: OracleRewarded(address indexed oracle, uint256 amount, uint256 marketId)

Node Distribution

Node operators receive equal distribution of their share. DistributedPayoutManager.sol:262-271
function _distributeToNodes(uint256 _totalReward) internal {
    if (activeNodes.length == 0 || _totalReward == 0) return;
    
    uint256 perNodeReward = _totalReward / activeNodes.length;
    for (uint256 i = 0; i < activeNodes.length; i++) {
        nodeRewards[activeNodes[i]] += perNodeReward;
        unclaimedRewards[activeNodes[i]] += perNodeReward;
        emit NodeRewarded(activeNodes[i], perNodeReward);
    }
}
Events: NodeRewarded(address indexed node, uint256 amount)

PayoutManager (Basic)

The simpler payout manager without Genesis NFT distribution.

Architecture

PayoutManager.sol:14-45
contract PayoutManager is ReentrancyGuard, Ownable {
    PredictionMarket public predictionMarket;
    ClockchainOracle public oracle;
    
    struct Payout {
        address recipient;
        uint256 amount;
        uint256 marketId;
        uint256 submissionId;
        bool claimed;
        uint256 timestamp;
    }
    
    struct MarketPayoutInfo {
        uint256 totalPool;
        uint256 winnerPool;
        uint256 platformFees;
        uint256 totalPayouts;
        bool processed;
        mapping(address => uint256) userPayouts;
        mapping(address => bool) hasClaimed;
    }
    
    uint256 public payoutCount;
    uint256 public totalDistributed;
    uint256 public platformFeesCollected;
}

Core Functions

calculatePayouts

Calculate payouts for a resolved market based on winner pool and loser pool.
_marketId
uint256
The market ID to calculate payouts for
PayoutManager.sol:65-122
function calculatePayouts(uint256 _marketId) external marketResolved(_marketId) {
    MarketPayoutInfo storage payoutInfo = marketPayouts[_marketId];
    require(!payoutInfo.processed, "Payouts already calculated");
    
    (
        ,
        ,
        ,
        ,
        ,
        uint256 winningSubmissionId,
        uint256 totalVolume,
        ,
        ,
        ,
        uint256 platformFeeCollected
    ) = predictionMarket.markets(_marketId);
    
    // Get all submissions for the market
    uint256[] memory submissionIds = predictionMarket.getMarketSubmissions(_marketId);
    
    uint256 totalLoserPool = 0;
    uint256 totalWinnerStake = 0;
    
    // Calculate winner and loser pools
    for (uint256 i = 0; i < submissionIds.length; i++) {
        (
            ,
            ,
            ,
            uint256 stake,
            uint256 totalBets,
            ,
            ,
            ,
        ) = predictionMarket.submissions(submissionIds[i]);
        
        if (submissionIds[i] == winningSubmissionId) {
            totalWinnerStake = totalBets;
        } else {
            totalLoserPool += totalBets;
        }
    }
    
    payoutInfo.totalPool = totalVolume - platformFeeCollected;
    payoutInfo.winnerPool = totalWinnerStake;
    payoutInfo.platformFees = platformFeeCollected;
    payoutInfo.processed = true;
    
    platformFeesCollected += platformFeeCollected;
    
    // Calculate individual payouts for winning bettors
    if (totalWinnerStake > 0) {
        _calculateWinnerPayouts(_marketId, winningSubmissionId, totalLoserPool, totalWinnerStake);
    }
    
    emit PayoutCalculated(_marketId, payoutInfo.totalPool, payoutInfo.winnerPool);
}
Events: PayoutCalculated(uint256 indexed marketId, uint256 totalPool, uint256 winnerPool)

claimPayout

Claim payout for a specific market.
_marketId
uint256
The market to claim payout from
PayoutManager.sol:199-214
function claimPayout(uint256 _marketId) external nonReentrant {
    MarketPayoutInfo storage payoutInfo = marketPayouts[_marketId];
    require(payoutInfo.processed, "Payouts not calculated");
    require(!payoutInfo.hasClaimed[msg.sender], "Already claimed");
    require(payoutInfo.userPayouts[msg.sender] > 0, "No payout available");
    
    uint256 amount = payoutInfo.userPayouts[msg.sender];
    payoutInfo.hasClaimed[msg.sender] = true;
    userTotalWinnings[msg.sender] += amount;
    totalDistributed += amount;
    
    payable(msg.sender).transfer(amount);
    
    emit PayoutClaimed(msg.sender, amount, _marketId);
}
Events: PayoutClaimed(address indexed recipient, uint256 amount, uint256 marketId)

Events

DistributedPayoutManager Events

event FeesDistributed(
    uint256 indexed marketId,
    uint256 genesisRewards,
    uint256 oracleRewards,
    uint256 nodeRewards,
    uint256 creatorReward,
    uint256 builderPoolDeposit,
    uint256 bittensorPoolDeposit
);

event GenesisHolderRewarded(address indexed holder, uint256 amount, uint256 marketId);
event OracleRewarded(address indexed oracle, uint256 amount, uint256 marketId);
event NodeRewarded(address indexed node, uint256 amount);
event CreatorRewarded(address indexed creator, uint256 amount, uint256 marketId);
event RewardClaimed(address indexed recipient, uint256 amount);

PayoutManager Events

event PayoutCalculated(uint256 indexed marketId, uint256 totalPool, uint256 winnerPool);
event PayoutClaimed(address indexed recipient, uint256 amount, uint256 marketId);
event PlatformFeeWithdrawn(address indexed recipient, uint256 amount);

Usage Example

const payoutManager = await ethers.getContractAt(
  "DistributedPayoutManager",
  "0x..." // Contract address
);

// After market resolution, distribute fees
const distributeTx = await payoutManager.distributeFees(marketId);
await distributeTx.wait();
console.log("Fees distributed to all stakeholders");

// Check unclaimed rewards
const rewards = await payoutManager.unclaimedRewards(myAddress);
console.log("Unclaimed rewards:", ethers.utils.formatEther(rewards));

// Claim rewards
if (rewards.gt(0)) {
  const claimTx = await payoutManager.claimRewards();
  await claimTx.wait();
  console.log("Rewards claimed!");
}

Genesis NFT Holder Example

// Genesis NFT holder claiming rewards
const genesisNFT = await ethers.getContractAt("GenesisNFT", genesisAddress);
const balance = await genesisNFT.balanceOf(myAddress);

console.log("Genesis NFTs owned:", balance.toString());

// Check accumulated rewards from all markets
const rewards = await payoutManager.unclaimedRewards(myAddress);
console.log("Total unclaimed rewards:", ethers.utils.formatEther(rewards));

// Claim all rewards at once
const claimTx = await payoutManager.claimRewards();
await claimTx.wait();
console.log("Claimed rewards from", balance.toString(), "NFTs");

Security Considerations

Reentrancy Protected

All claim functions use ReentrancyGuard

Pull-Based Claims

Users pull rewards rather than push, preventing griefing

Double-Claim Prevention

State is updated before transfers to prevent double claims

Pausable

Contract can be paused in emergency situations

Next Steps

Oracle System

Learn about decentralized text validation and oracle rewards

Build docs developers (and LLMs) love