Aere Network public source. Everything here can be checked against the live chain (chain id 2800, https://rpc.aere.network). Scope note, stated up front rather than buried: consensus on chain 2800 is classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at the signature, precompile, account and transport layers. Nothing here makes the consensus post-quantum, and no document in it should be read as claiming so.
290 lines
14 KiB
Solidity
290 lines
14 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";
|
|
|
|
/// Minimal Ownable-style interface used to prove the caller controls a contract.
|
|
interface IOwnableProbe {
|
|
function owner() external view returns (address);
|
|
}
|
|
|
|
/**
|
|
* @title AereFeeMonetizationV2
|
|
* @notice Bug-fix redeploy of AereFeeMonetization (V1 at
|
|
* 0x6b62DC6cC974F779354c953F41b64a7aB994dd98).
|
|
*
|
|
* Pay developers (and the Foundation treasury) automatically when their
|
|
* contracts get used. A developer registers a contract and receives an
|
|
* ERC-721 representing the right to collect that contract's dev-share of
|
|
* gas fees; a Foundation distributor credits per-tokenId amounts; the NFT
|
|
* owner claims accumulated AERE.
|
|
*
|
|
* ────────────────────────── WHY V2 (finding #1, HIGH) ──────────────────────
|
|
* V1.register(contractAddr, payout) performed NO check that msg.sender
|
|
* controls contractAddr. Anyone could "squat" a victim contract's fee stream
|
|
* by registering the victim's address and minting the NFT to themselves; the
|
|
* real developer could then NEVER register (permanent DoS, "already
|
|
* registered"), and the squatter collected every fee the distributor
|
|
* attributed to that contract. There was also no unregister / reclaim path.
|
|
*
|
|
* V2 fixes:
|
|
* (1) PERMISSIONLESS-but-AUTHENTICATED register: the caller must prove it
|
|
* controls contractAddr — either
|
|
* * msg.sender == contractAddr (the contract registers itself), OR
|
|
* * msg.sender == Ownable(contractAddr).owner() (try/catch; the
|
|
* on-chain owner registers it).
|
|
* A contract that has no owner() and is not self-registering cannot be
|
|
* registered by a third party. The permissionless-monetization intent is
|
|
* preserved: NO Foundation gatekeeping of who may register their own
|
|
* contract, it is not onlyOwner.
|
|
* (2) unregister(contractAddr): the current NFT owner (registrant) OR the
|
|
* FeeMonetization owner (Foundation, for corrections) can clear a
|
|
* mistaken/stale registration. Any pending rewards are paid to the NFT
|
|
* owner first (nothing is lost), the NFT is burned, and the contract can
|
|
* be registered again cleanly.
|
|
* (3) reassignPayout(contractAddr, newPayout): convenience wrapper that moves
|
|
* the fee-stream NFT to a new payout address (equivalent to an ERC-721
|
|
* transfer, exposed by contract address for UX).
|
|
*
|
|
* Fee-distribution, claim, treasury and policy logic are byte-for-byte
|
|
* identical to V1.
|
|
*/
|
|
contract AereFeeMonetizationV2 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 Unregistered(address indexed contractAddr, uint256 indexed tokenId, address indexed to, uint256 pendingPaid);
|
|
event PayoutReassigned(address indexed contractAddr, uint256 indexed tokenId, address indexed newPayout);
|
|
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 Returns true iff `who` provably controls `target`: either it IS
|
|
/// `target` (the contract self-registering) or it is `target`'s
|
|
/// on-chain Ownable owner. try/catch so a non-Ownable target simply
|
|
/// yields false rather than reverting.
|
|
function controlsContract(address who, address target) public view returns (bool) {
|
|
if (who == target) return true;
|
|
// No-code target (EOA): only self-registration is allowed. Guard the
|
|
// owner() probe so a call to a codeless address returns false gracefully
|
|
// instead of tripping Solidity's extcodesize check (a reasonless revert).
|
|
if (target.code.length == 0) return false;
|
|
try IOwnableProbe(target).owner() returns (address o) {
|
|
return (o != address(0) && o == who);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice Register a contract for fee-share. Permissionless, but the caller
|
|
* MUST control contractAddr (self-registration or its Ownable owner).
|
|
* The NFT is minted to payoutAddr (the NFT owner at claim time is who
|
|
* receives the AERE). This closes the V1 fee-stream squat + DoS.
|
|
*/
|
|
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");
|
|
require(controlsContract(msg.sender, contractAddr), "FeeMon: not controller");
|
|
|
|
tokenId = nextTokenId++;
|
|
earningContract[tokenId] = contractAddr;
|
|
tokenIdOf[contractAddr] = tokenId;
|
|
|
|
_mint(payoutAddr, tokenId);
|
|
emit Registered(contractAddr, tokenId, payoutAddr);
|
|
}
|
|
|
|
/// @notice Clear a mistaken/stale registration. Callable by the current NFT
|
|
/// owner (registrant) or the FeeMonetization owner. Any pending
|
|
/// rewards are paid to the current NFT owner first (nothing lost),
|
|
/// the NFT is burned, and the contract can be registered again.
|
|
function unregister(address contractAddr) external nonReentrant {
|
|
uint256 tokenId = tokenIdOf[contractAddr];
|
|
require(tokenId != 0, "FeeMon: not registered");
|
|
address holder = ownerOf(tokenId); // reverts if already burned
|
|
require(msg.sender == holder || msg.sender == owner(), "FeeMon: not authorized");
|
|
|
|
uint256 amount = pendingRewards[tokenId];
|
|
pendingRewards[tokenId] = 0;
|
|
earningContract[tokenId] = address(0);
|
|
tokenIdOf[contractAddr] = 0;
|
|
_burn(tokenId);
|
|
|
|
if (amount > 0) {
|
|
(bool ok, ) = payable(holder).call{ value: amount }("");
|
|
require(ok, "FeeMon: payout failed");
|
|
}
|
|
emit Unregistered(contractAddr, tokenId, holder, amount);
|
|
}
|
|
|
|
/// @notice Move a fee-stream NFT to a new payout address. Convenience wrapper
|
|
/// over a plain ERC-721 transfer, addressed by the earning contract.
|
|
function reassignPayout(address contractAddr, address newPayout) external {
|
|
require(newPayout != address(0), "FeeMon: zero payout");
|
|
uint256 tokenId = tokenIdOf[contractAddr];
|
|
require(tokenId != 0, "FeeMon: not registered");
|
|
require(ownerOf(tokenId) == msg.sender, "FeeMon: not NFT owner");
|
|
_transfer(msg.sender, newPayout, tokenId);
|
|
emit PayoutReassigned(contractAddr, tokenId, newPayout);
|
|
}
|
|
|
|
/// @notice Convenience: Foundation registers its OWN protocol contracts in
|
|
/// bulk. This is an onlyOwner ADMIN bootstrap (the FeeMonetization
|
|
/// owner is the Foundation multisig) — it is NOT the permissionless
|
|
/// register() path and is NOT a squat vector: only the Foundation can
|
|
/// call it, and only for the contracts it operates.
|
|
function registerBatch(address[] calldata contractAddrs, address payoutAddr)
|
|
external onlyOwner returns (uint256[] memory tokenIds)
|
|
{
|
|
require(payoutAddr != address(0), "FeeMon: zero payout");
|
|
tokenIds = new uint256[](contractAddrs.length);
|
|
for (uint256 i = 0; i < contractAddrs.length; i++) {
|
|
require(contractAddrs[i] != address(0), "FeeMon: zero contract");
|
|
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) {
|
|
uint256 bal = balanceOf(holder);
|
|
if (bal == 0) return 0;
|
|
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;
|
|
validatorBps_ = BPS - 3750 - devShareBps - treasuryBps;
|
|
}
|
|
|
|
receive() external payable {}
|
|
}
|