Skip to content

Commit 30d03fe

Browse files
committed
fix: correct incentive architecture — investment → training → inference → dividends
- purchaseShares deposits fund training pool (unchanged) - claimTrainingReward now mints fresh ATN proportional to contribution, capped by the training pool budget — ATN emerges from training, not pre-mint - recordInference pulls payment from requester (transferFrom) before distributing: 60% provider, 15% treasury, 25% dividend pool - Separate dividendPoolBalances from tokenPoolBalances — training pool and dividend pool are independent, no cross-contamination - Remove duplicate OnlyDAO error, consolidate on NotDAO - Tests updated: requester approves before inference, training claim has no token arg (always mints ATN), dividend pool assertions use new getter 301 Hardhat tests passing.
1 parent f4859a7 commit 30d03fe

4 files changed

Lines changed: 53 additions & 43 deletions

File tree

cache/solidity-files-cache.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2117,8 +2117,8 @@
21172117
]
21182118
},
21192119
"C:\\code\\autonet\\contracts\\core\\RPB.sol": {
2120-
"lastModificationDate": 1775427060074,
2121-
"contentHash": "d7fa0defd6ceb3043713694db38367b2",
2120+
"lastModificationDate": 1775428571872,
2121+
"contentHash": "35a318fbe7aa4480caaf921d329fb442",
21222122
"sourceName": "contracts/core/RPB.sol",
21232123
"solcConfig": {
21242124
"version": "0.8.20",

contracts/core/RPB.sol

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -100,24 +100,28 @@ contract RPB is ReentrancyGuard, ERC20, ERC20Permit, ERC20Votes, ERC20Burnable {
100100
// --- Authorized minters (for BME bridge, disbursers, etc.) ---
101101
mapping(address => bool) public authorizedMinters;
102102

103-
// --- Training rewards ---
104-
uint256 public trainingRewardPool;
105-
uint256 public totalTrainingTokens;
103+
// --- Training rewards (funded by share purchases, caps ATN minting) ---
104+
uint256 public trainingRewardPool; // normalized value: total budget ceiling for minting
105+
uint256 public totalTrainingTokens; // total contribution units across all agents
106+
uint256 public totalTrainingMinted; // total ATN minted as training rewards
106107
uint256 public currentEpoch;
107108
mapping(uint256 => TrainingRecord[]) public epochRecords;
108109
mapping(address => uint256) public agentTrainingTokens;
109110
mapping(address => uint256) public claimedRewards;
110111

111-
// --- Per-token pool tracking (token address -> amount held in raw token units) ---
112+
// --- Per-token pool tracking for training pool (token address -> raw units held) ---
112113
mapping(address => uint256) public tokenPoolBalances;
113114

114115
// --- RPB shares (claims on inference dividends) ---
115116
uint256 public totalShares;
116117
mapping(address => uint256) public shares;
117-
uint256 public totalInferenceRevenue;
118+
uint256 public totalInferenceRevenue; // cumulative normalized inference revenue
118119
uint256 public totalDividendsPaid;
119120
mapping(address => uint256) public lastDividendClaim;
120121

122+
// --- Dividend pool (separate from training pool, funded by inference revenue) ---
123+
mapping(address => uint256) public dividendPoolBalances; // token -> raw units held for dividends
124+
121125
// --- Inference accounting ---
122126
uint256 public totalInferenceUnits;
123127
uint256 public inferenceProviderShare; // Basis points
@@ -205,7 +209,6 @@ contract RPB is ReentrancyGuard, ERC20, ERC20Permit, ERC20Votes, ERC20Burnable {
205209
error NoShares();
206210
error NoSharesOutstanding();
207211
error NoDividendsToClaim();
208-
error OnlyDAO();
209212
error RevenueSplitInvalid();
210213
error TransferFailed();
211214
error ProviderPaymentFailed();
@@ -499,36 +502,30 @@ contract RPB is ReentrancyGuard, ERC20, ERC20Permit, ERC20Votes, ERC20Burnable {
499502
}
500503

501504
/**
502-
* @notice Claim training rewards, withdrawn in a specific value-index token.
503-
* @param token ERC20 token to receive (must be in value index; use address(this) for ATN)
505+
* @notice Claim training rewards as freshly minted ATN.
506+
* @dev ATN is minted proportional to the agent's training contribution,
507+
* capped by the training reward pool (funded by share purchases).
504508
*/
505-
function claimTrainingReward(address token) external nonReentrant {
509+
function claimTrainingReward() external nonReentrant {
506510
uint256 tokens = agentTrainingTokens[msg.sender];
507511
uint256 alreadyClaimed = claimedRewards[msg.sender];
508512
if (tokens <= alreadyClaimed) revert NoRewardsToClaim();
509513
if (trainingRewardPool == 0 || totalTrainingTokens == 0) revert RewardPoolEmpty();
510514

511515
uint256 unclaimed = tokens - alreadyClaimed;
512-
uint256 normalizedReward = (unclaimed * trainingRewardPool) / totalTrainingTokens;
516+
uint256 reward = (unclaimed * trainingRewardPool) / totalTrainingTokens;
513517

514-
if (normalizedReward > trainingRewardPool) {
515-
normalizedReward = trainingRewardPool;
518+
if (reward > trainingRewardPool) {
519+
reward = trainingRewardPool;
516520
}
517-
518-
uint256 rawAmount = _denormalize(token, normalizedReward);
519-
if (rawAmount == 0) revert NoRewardsToClaim();
520-
if (tokenPoolBalances[token] < rawAmount) revert InsufficientPoolBalance();
521+
if (reward == 0) revert NoRewardsToClaim();
521522

522523
claimedRewards[msg.sender] = tokens;
523-
trainingRewardPool -= normalizedReward;
524-
tokenPoolBalances[token] -= rawAmount;
524+
totalTrainingMinted += reward;
525+
agents[msg.sender].totalRewardsEarned += reward;
525526

526-
if (token == address(this)) {
527-
_transfer(address(this), msg.sender, rawAmount);
528-
} else {
529-
if (!IERC20(token).transfer(msg.sender, rawAmount)) revert TransferFailed();
530-
}
531-
emit RewardClaimed(msg.sender, token, rawAmount);
527+
_mint(msg.sender, reward);
528+
emit RewardClaimed(msg.sender, address(this), reward);
532529
}
533530

534531
/**
@@ -583,11 +580,11 @@ contract RPB is ReentrancyGuard, ERC20, ERC20Permit, ERC20Votes, ERC20Burnable {
583580

584581
uint256 rawAmount = _denormalize(token, normalizedToClaim);
585582
if (rawAmount == 0) revert NoDividendsToClaim();
586-
if (tokenPoolBalances[token] < rawAmount) revert InsufficientPoolBalance();
583+
if (dividendPoolBalances[token] < rawAmount) revert InsufficientPoolBalance();
587584

588585
lastDividendClaim[msg.sender] = myShare;
589586
totalDividendsPaid += normalizedToClaim;
590-
tokenPoolBalances[token] -= rawAmount;
587+
dividendPoolBalances[token] -= rawAmount;
591588

592589
if (token == address(this)) {
593590
_transfer(address(this), msg.sender, rawAmount);
@@ -610,6 +607,13 @@ contract RPB is ReentrancyGuard, ERC20, ERC20Permit, ERC20Votes, ERC20Burnable {
610607
) external onlyOperator nonReentrant {
611608
if (!agents[provider].active) revert AgentNotActive();
612609

610+
// Pull payment from the requester
611+
if (token == address(this)) {
612+
_transfer(requester, address(this), cost);
613+
} else {
614+
if (!IERC20(token).transferFrom(requester, address(this), cost)) revert TransferFailed();
615+
}
616+
613617
uint256 normalizedCost = _normalize(token, cost);
614618

615619
totalInferenceUnits += units;
@@ -627,6 +631,7 @@ contract RPB is ReentrancyGuard, ERC20, ERC20Permit, ERC20Votes, ERC20Burnable {
627631
uint256 treasuryPayment = (cost * treasuryShare) / 10000;
628632
uint256 shareholderPoolPayment = cost - providerPayment - treasuryPayment;
629633

634+
// Distribute provider and treasury shares
630635
if (token == address(this)) {
631636
if (providerPayment > 0) _transfer(address(this), provider, providerPayment);
632637
if (treasuryPayment > 0) _transfer(address(this), dao, treasuryPayment);
@@ -639,8 +644,9 @@ contract RPB is ReentrancyGuard, ERC20, ERC20Permit, ERC20Votes, ERC20Burnable {
639644
}
640645
}
641646

647+
// Shareholder portion goes to dividend pool (separate from training pool)
642648
if (shareholderPoolPayment > 0) {
643-
tokenPoolBalances[token] += shareholderPoolPayment;
649+
dividendPoolBalances[token] += shareholderPoolPayment;
644650
}
645651

646652
emit InferenceServed(provider, requester, units, normalizedCost);
@@ -671,9 +677,9 @@ contract RPB is ReentrancyGuard, ERC20, ERC20Permit, ERC20Votes, ERC20Burnable {
671677
}
672678
}
673679

674-
// Track shareholder pool (bridge already minted ATN to this contract)
680+
// Track dividend pool (bridge already minted ATN to this contract)
675681
if (shareholderAmount > 0) {
676-
tokenPoolBalances[address(this)] += shareholderAmount;
682+
dividendPoolBalances[address(this)] += shareholderAmount;
677683
}
678684

679685
emit InferenceServed(provider, requester, units, totalRevenue);
@@ -782,7 +788,7 @@ contract RPB is ReentrancyGuard, ERC20, ERC20Permit, ERC20Votes, ERC20Burnable {
782788
uint256 _shareholderShare,
783789
uint256 _treasuryShare
784790
) external {
785-
if (msg.sender != dao) revert OnlyDAO();
791+
if (msg.sender != dao) revert NotDAO();
786792
if (_providerShare + _shareholderShare + _treasuryShare != 10000) revert RevenueSplitInvalid();
787793
inferenceProviderShare = _providerShare;
788794
shareholderShare = _shareholderShare;
@@ -856,6 +862,10 @@ contract RPB is ReentrancyGuard, ERC20, ERC20Permit, ERC20Votes, ERC20Burnable {
856862
return tokenPoolBalances[token];
857863
}
858864

865+
function getDividendPoolBalance(address token) external view returns (uint256) {
866+
return dividendPoolBalances[token];
867+
}
868+
859869
function getRegisteredAgents() external view returns (address[] memory) {
860870
return registeredAgentList;
861871
}

tests/contracts-full.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1791,7 +1791,7 @@ describe("Autonet Contracts — Full Test Suite", function () {
17911791
// Only DAO can update revenue split
17921792
await expect(
17931793
rpb.connect(f.user1).updateRevenueSplit(3000, 3000, 3000)
1794-
).to.be.revertedWithCustomError(rpb, "OnlyDAO");
1794+
).to.be.revertedWithCustomError(rpb, "NotDAO");
17951795
});
17961796

17971797
it("alignment updates", async function () {

tests/economic-loop.test.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -317,10 +317,10 @@ describe("Full Economic Loop — E2E", function () {
317317
ethers.ZeroAddress
318318
);
319319

320-
// Fund RPB with ATN for inference payments (owner sends to RPB)
321-
await rpb.transfer(rpbAddr, e18(10000));
320+
// Requester approves RPB to pull inference payment
321+
await rpb.connect(investor1).approve(rpbAddr, e18(10000));
322322

323-
// Record inference (owner is contract owner, can call recordInference)
323+
// Record inference — RPB pulls 1000 ATN from requester, splits it
324324
// 100 units, 1000 ATN cost, paid in ATN (address(this))
325325
await rpb.recordInference(
326326
investor1.address, // requester
@@ -395,13 +395,13 @@ describe("Full Economic Loop — E2E", function () {
395395
expect(unclaimed).to.equal(e18(5000));
396396

397397
const balBefore = await rpb.balanceOf(solver1.address);
398-
await rpb.connect(solver1).claimTrainingReward(rpbAddr);
398+
await rpb.connect(solver1).claimTrainingReward();
399399
const balAfter = await rpb.balanceOf(solver1.address);
400400
expect(balAfter - balBefore).to.equal(e18(5000));
401401

402402
// Cannot double-claim
403403
await expect(
404-
rpb.connect(solver1).claimTrainingReward(rpbAddr)
404+
rpb.connect(solver1).claimTrainingReward()
405405
).to.be.revertedWithCustomError(rpb, "NoRewardsToClaim");
406406
});
407407
});
@@ -501,7 +501,7 @@ describe("Full Economic Loop — E2E", function () {
501501
);
502502

503503
// ── D: Inference generates revenue ──
504-
await rpb.transfer(rpbAddr, e18(10000)); // Fund RPB for inference payments
504+
await rpb.connect(investor1).approve(rpbAddr, e18(10000));
505505
await rpb.recordInference(
506506
investor1.address, // requester
507507
solver1.address, // provider
@@ -534,8 +534,8 @@ describe("Full Economic Loop — E2E", function () {
534534
lineage, ethers.randomBytes(32), ethers.ZeroAddress, ethers.ZeroAddress
535535
);
536536

537-
// Fund RPB for payments
538-
await rpb.transfer(rpbAddr, e18(10000));
537+
// Requester approves RPB to pull inference payment
538+
await rpb.connect(investor1).approve(rpbAddr, e18(10000));
539539

540540
const providerBefore = await rpb.balanceOf(solver1.address);
541541
const daoBefore = await rpb.balanceOf(owner.address); // owner = dao in test
@@ -551,8 +551,8 @@ describe("Full Economic Loop — E2E", function () {
551551
expect(providerAfter - providerBefore).to.equal(e18(600));
552552
// DAO/treasury gets 15%
553553
expect(daoAfter - daoBefore).to.equal(e18(150));
554-
// Remaining 25% stays in RPB contract for shareholders
555-
const shareholderPool = await rpb.getTokenPoolBalance(rpbAddr);
554+
// Remaining 25% stays in RPB contract's dividend pool for shareholders
555+
const shareholderPool = await rpb.getDividendPoolBalance(rpbAddr);
556556
expect(shareholderPool).to.equal(e18(250));
557557
});
558558
});

0 commit comments

Comments
 (0)