aere-contracts/contracts/AereFeeMonetization.sol
Aere Network acac2f00a6
Some checks failed
contracts-ci / Install (lockfile) → compile → full test suite (push) Has been cancelled
contracts-ci / Ethereum interop (EIP-2537 BLS, prague hardfork) (push) Has been cancelled
contracts-ci / PQC known-answer tests (NIST vectors) (push) Has been cancelled
contracts-ci / Coverage (scoped, with artifacts) (push) Has been cancelled
The unpublished line of work joins the sanitized public line
The published line and the local line had no common ancestor: the public one
carried the redaction pass, the local one carried three weeks of corrections
that never shipped. This commit ports the local work onto the public line,
keeps every public redaction, and extends the same discretion to seven client
mentions that were still named in published comments.

Carried: LICENSE year and LICENSING.md; the measured burn figures replacing
the deflation claim (the vault holds ~0.137 AERE of 2.8 billion, and burn is
a share of validator coinbase revenue, which is zero today); 'audited' removed
from next to Bouncy Castle; citation paths rewritten to published form with
CITATIONS-UNRESOLVED.md remeasured 2026-08-11; VERIFY-POLICY.md; slashing and
ownership comments brought down to what the code does; the AerePyth repair;
the shutter test helper the tests cite; runnable package.json entries; the CI
file split into a GitHub/Gitea twin pair with a real measured test-run status;
and the .gitignore hardening written after a compiled artifact leaked a local
path in a sibling repository. A false '2-of-3 multisig' description of the
owner account is corrected to what the chain measures: an externally owned
account. The self-audit findings catalog stays unpublished pending an explicit
decision.
2026-08-15 13:59:30 +03:00

223 lines
10 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
/**
* @title AereFeeMonetization
* @notice Pay developers (and Foundation treasury) automatically when their contracts
* get used. Modelled on Mode Network's Sequencer Fee Sharing pattern:
*
* - Developer calls register(contractAddr, payoutAddr) and receives an
* ERC-721 NFT representing the right to collect fees from contractAddr.
* - An off-chain distributor (Foundation-operated) parses recent blocks,
* attributes gas per top-level contract, and calls distribute() with
* per-tokenId amounts.
* - NFT owner calls claim(tokenId) to withdraw accumulated AERE.
*
* The NFT is transferable, so fee streams become sellable / collateralisable.
*
* Plus: a fixed `treasuryBps` slice that always flows to AERE Foundation
* (separate from per-contract dev-share). Default 500 = 5%.
*
* Intended allocation of VALIDATOR COINBASE REVENUE attributable to a
* registered contract's gas usage (set by Foundation governance). These are
* shares of the validator's coinbase reward, NOT a base-fee burn and NOT a
* protocol-level cut of transaction fees:
* burnBps (Tier 1.8): 3750 = 37.5% (permanent burn, capped at 5000)
* treasuryBps (this tier): 500 = 5% (always to Foundation)
* devShareBps (this tier): 2000 = 20% (to registered NFT owner, else to validator)
* validatorBps (residual): 3750 = 37.5% (to block validator)
*
* MEASURED: validator coinbase revenue on chain 2800 is currently zero, so
* these shares currently distribute nothing and no fee stream has accrued.
* The percentages are conditional rates on future revenue.
*
* Foundation registers its own foundational contracts (AereSwapRouter,
* AereSettlement, AereMessenger, AereNFTMarketplace, etc.) so their dev-share
* flows back to Foundation. External devs register their own; their NFT owner
* collects that fee stream.
*/
contract AereFeeMonetization is ERC721, Ownable, ReentrancyGuard {
/// Treasury address. All `treasuryBps` revenue flows here.
address payable public treasury;
/// Dev-share rate in basis points (10000 = 100%). Hard cap at 3000 (30%).
uint256 public devShareBps = 2000;
/// Treasury slice in basis points. Hard cap at 1500 (15%).
uint256 public treasuryBps = 500;
uint256 public constant BPS = 10_000;
/// Authorised distributors (Foundation runs these — same trust model as Tier 1.8).
mapping(address => bool) public isDistributor;
/// Auto-incrementing tokenId.
uint256 public nextTokenId = 1;
/// tokenId → contract that earns this share.
mapping(uint256 => address) public earningContract;
/// contractAddr → tokenId (0 if not registered).
mapping(address => uint256) public tokenIdOf;
/// tokenId → unclaimed AERE balance.
mapping(uint256 => uint256) public pendingRewards;
/// Lifetime cumulative paid to each NFT and to treasury.
mapping(uint256 => uint256) public totalPaidToToken;
uint256 public totalPaidToTreasury;
event Registered(address indexed contractAddr, uint256 indexed tokenId, address indexed payout);
event Distributed(uint256 indexed tokenId, uint256 amount, uint256 newPending);
event TreasuryFunded(uint256 amount, uint256 newTotal);
event Claimed(uint256 indexed tokenId, address indexed to, uint256 amount);
event RateChanged(string what, uint256 oldValue, uint256 newValue);
event DistributorChanged(address indexed distributor, bool authorised);
event TreasuryChanged(address indexed previous, address indexed next);
modifier onlyDistributor() {
require(isDistributor[msg.sender], "FeeMon: not a distributor");
_;
}
constructor(address payable _treasury) ERC721("AERE Fee Monetization", "AERE-FM") {
require(_treasury != address(0), "FeeMon: zero treasury");
treasury = _treasury;
}
// ───────────────────── Admin ─────────────────────
function setTreasury(address payable _treasury) external onlyOwner {
require(_treasury != address(0), "FeeMon: zero treasury");
emit TreasuryChanged(treasury, _treasury);
treasury = _treasury;
}
function setDevShareBps(uint256 _bps) external onlyOwner {
require(_bps <= 3000, "FeeMon: devShare too high");
emit RateChanged("devShareBps", devShareBps, _bps);
devShareBps = _bps;
}
function setTreasuryBps(uint256 _bps) external onlyOwner {
require(_bps <= 1500, "FeeMon: treasuryBps too high");
emit RateChanged("treasuryBps", treasuryBps, _bps);
treasuryBps = _bps;
}
function setDistributor(address dist, bool ok) external onlyOwner {
isDistributor[dist] = ok;
emit DistributorChanged(dist, ok);
}
// ───────────────────── Registration ─────────────────────
/**
* @notice Register a contract for fee-share. The caller becomes the initial
* NFT owner. payoutAddr is informational — the NFT owner at claim time
* is who receives the AERE.
*/
function register(address contractAddr, address payoutAddr) external returns (uint256 tokenId) {
require(contractAddr != address(0), "FeeMon: zero contract");
require(tokenIdOf[contractAddr] == 0, "FeeMon: already registered");
require(payoutAddr != address(0), "FeeMon: zero payout");
tokenId = nextTokenId++;
earningContract[tokenId] = contractAddr;
tokenIdOf[contractAddr] = tokenId;
_mint(payoutAddr, tokenId);
emit Registered(contractAddr, tokenId, payoutAddr);
}
/// Convenience: Foundation registers in bulk.
function registerBatch(address[] calldata contractAddrs, address payoutAddr)
external onlyOwner returns (uint256[] memory tokenIds)
{
tokenIds = new uint256[](contractAddrs.length);
for (uint256 i = 0; i < contractAddrs.length; i++) {
require(tokenIdOf[contractAddrs[i]] == 0, "FeeMon: already registered");
uint256 tid = nextTokenId++;
earningContract[tid] = contractAddrs[i];
tokenIdOf[contractAddrs[i]] = tid;
_mint(payoutAddr, tid);
emit Registered(contractAddrs[i], tid, payoutAddr);
tokenIds[i] = tid;
}
}
// ───────────────────── Distribution (called by Foundation distributor) ─────────────────────
/**
* @notice Credit per-tokenId fee shares. The distributor sends `totalAmount`
* as msg.value. Sum of `amounts` plus the treasury slice = totalAmount.
*/
function distribute(
uint256[] calldata tokenIds,
uint256[] calldata amounts,
uint256 treasuryAmount
) external payable onlyDistributor nonReentrant {
require(tokenIds.length == amounts.length, "FeeMon: length mismatch");
uint256 sum = treasuryAmount;
for (uint256 i = 0; i < amounts.length; i++) {
sum += amounts[i];
}
require(sum == msg.value, "FeeMon: amount mismatch");
// Treasury slice
if (treasuryAmount > 0) {
(bool ok, ) = treasury.call{ value: treasuryAmount }("");
require(ok, "FeeMon: treasury transfer failed");
totalPaidToTreasury += treasuryAmount;
emit TreasuryFunded(treasuryAmount, totalPaidToTreasury);
}
// Per-token shares (held in contract until claim)
for (uint256 i = 0; i < tokenIds.length; i++) {
require(earningContract[tokenIds[i]] != address(0), "FeeMon: bad tokenId");
pendingRewards[tokenIds[i]] += amounts[i];
totalPaidToToken[tokenIds[i]] += amounts[i];
emit Distributed(tokenIds[i], amounts[i], pendingRewards[tokenIds[i]]);
}
}
// ───────────────────── Claim ─────────────────────
function claim(uint256 tokenId) external nonReentrant returns (uint256 amount) {
require(ownerOf(tokenId) == msg.sender, "FeeMon: not NFT owner");
amount = pendingRewards[tokenId];
require(amount > 0, "FeeMon: nothing to claim");
pendingRewards[tokenId] = 0;
(bool ok, ) = payable(msg.sender).call{ value: amount }("");
require(ok, "FeeMon: claim transfer failed");
emit Claimed(tokenId, msg.sender, amount);
}
/// View: how much would the current NFT owner withdraw if they called claim() now?
function claimableFor(address holder) external view returns (uint256 total) {
// Walk via balanceOf — gas-heavy, intended for off-chain UI only.
uint256 bal = balanceOf(holder);
if (bal == 0) return 0;
// ERC-721 doesn't index tokens by owner natively in OZ basic. Caller should
// pass tokenIds explicitly via pendingRewards mapping for production reads.
// This helper is a UI-friendly shortcut for small wallets.
for (uint256 t = 1; t < nextTokenId; t++) {
if (_ownerOf(t) == holder) {
total += pendingRewards[t];
}
}
}
// ───────────────────── Stats ─────────────────────
function policy() external view returns (uint256 burnBps_, uint256 devShareBps_, uint256 treasuryBps_, uint256 validatorBps_) {
burnBps_ = 3750; // Tier 1.8 - documented here for clarity only; actual burn lives in AereCoinbaseSplitter
devShareBps_ = devShareBps;
treasuryBps_ = treasuryBps;
// Validator residual = BPS - burn - devShare(if registered) - treasury
validatorBps_ = BPS - 3750 - devShareBps - treasuryBps;
}
receive() external payable {}
}