Overview
The Proteus protocol uses two payout manager contracts for distributing platform fees to stakeholders:
PayoutManager - Basic payout distribution (deployed)
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)
Contract Address PayoutManager 0x88d399C949Ff2f1aaa8eA5a859Ae4d97c74f6871DistributedPayoutManager Not yet deployed
Fee Distribution
The 7% platform fee is distributed across multiple stakeholders:
Recipient Share of Fees % of Volume Genesis NFT Holders 20% 1.4% Oracles 28.6% 2.0% Market Creators 14.3% 1.0% Node Operators 14.3% 1.0% Builder Pool 28.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.
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 :
Calculate reward per NFT: totalReward / 100
Iterate through all 100 NFTs
Track unique holders to avoid double-counting
For each unique holder, multiply reward per NFT by their balance
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.
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.
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