// SPDX-License-Identifier: MIT pragma solidity ^0.8.23; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; interface IBurnVault { function burn() external payable; } /** * @title AereCoinbaseSplitter * @notice Routes validator coinbase rewards through an atomic split: * burnBps to AereFeeBurnVault (default 3750 = 37.5%, matching whitepaper §3.3), * remainder back to the validator's address. * * Validators (or their forwarder daemons) call `splitAndDistribute()` with * their accumulated coinbase as msg.value. The contract atomically: * 1. Sends burnBps/10000 of msg.value to AereFeeBurnVault. * 2. Sends the remainder back to the validator's address. * 3. Emits Burned + Distributed events with cumulative totals. * * Anyone can call this contract — there is no allowlist of "who can burn." * The splitter is owner-configurable only for the burnBps parameter and * the burn-vault address; it cannot hold or steal funds. * * Cumulative on-chain accounting allows /network-status to display a live * burn rate, and any third-party explorer or analyst to verify the * whitepaper §3.3 deflation claim in O(1) reads. */ contract AereCoinbaseSplitter is Ownable, ReentrancyGuard { /// AereFeeBurnVault — where the burned portion goes. address payable public burnVault; /// Burn rate in basis points. 3750 = 37.5% (matches whitepaper §3.3). uint256 public burnBps = 3750; uint256 public constant BPS = 10_000; /// Cumulative AERE burned through this splitter (lifetime). uint256 public totalBurned; /// Cumulative AERE distributed back to validators (lifetime). uint256 public totalDistributed; /// Per-validator (or per-caller) cumulative burn contribution. mapping(address => uint256) public burnedBy; mapping(address => uint256) public distributedTo; event Burned(address indexed contributor, uint256 amount, uint256 newTotalBurned); event Distributed(address indexed validator, uint256 amount, uint256 newTotalDistributed); event BurnBpsChanged(uint256 oldBps, uint256 newBps); event BurnVaultChanged(address oldVault, address newVault); constructor(address payable _burnVault) { require(_burnVault != address(0), "Splitter: zero vault"); burnVault = _burnVault; } // ───────────────────── Admin ───────────────────── /// Foundation can adjust the burn rate up to a hard cap of 50% — never higher. /// The whitepaper says "up to 37.5%" so this provides headroom but caps abuse. function setBurnBps(uint256 _bps) external onlyOwner { require(_bps <= 5000, "Splitter: bps too high"); emit BurnBpsChanged(burnBps, _bps); burnBps = _bps; } function setBurnVault(address payable _vault) external onlyOwner { require(_vault != address(0), "Splitter: zero vault"); emit BurnVaultChanged(burnVault, _vault); burnVault = _vault; } // ───────────────────── Core ───────────────────── /** * @notice Split msg.value: burnBps/10000 to AereFeeBurnVault, remainder to `validator`. * @param validator The address to receive the unburned portion. For validator- * initiated calls this is typically tx.origin or msg.sender; * the caller decides where their rebate goes. */ function splitAndDistribute(address payable validator) external payable nonReentrant { require(msg.value > 0, "Splitter: zero amount"); require(validator != address(0), "Splitter: zero validator"); uint256 burnAmount = (msg.value * burnBps) / BPS; uint256 distributeAmount = msg.value - burnAmount; // Send burnAmount to AereFeeBurnVault. if (burnAmount > 0) { IBurnVault(burnVault).burn{ value: burnAmount }(); totalBurned += burnAmount; burnedBy[msg.sender] += burnAmount; emit Burned(msg.sender, burnAmount, totalBurned); } // Distribute remainder back to validator. if (distributeAmount > 0) { (bool ok, ) = validator.call{ value: distributeAmount }(""); require(ok, "Splitter: distribute failed"); totalDistributed += distributeAmount; distributedTo[validator] += distributeAmount; emit Distributed(validator, distributeAmount, totalDistributed); } } /// Convenience: validator calls this with no validator argument — distributes back to msg.sender. function splitToSelf() external payable nonReentrant { require(msg.value > 0, "Splitter: zero amount"); uint256 burnAmount = (msg.value * burnBps) / BPS; uint256 distributeAmount = msg.value - burnAmount; if (burnAmount > 0) { IBurnVault(burnVault).burn{ value: burnAmount }(); totalBurned += burnAmount; burnedBy[msg.sender] += burnAmount; emit Burned(msg.sender, burnAmount, totalBurned); } if (distributeAmount > 0) { (bool ok, ) = payable(msg.sender).call{ value: distributeAmount }(""); require(ok, "Splitter: distribute failed"); totalDistributed += distributeAmount; distributedTo[msg.sender] += distributeAmount; emit Distributed(msg.sender, distributeAmount, totalDistributed); } } /// Read the live cumulative burn statistics for /network-status display. function burnStats() external view returns ( uint256 lifetimeBurned, uint256 lifetimeDistributed, uint256 currentBurnBps ) { return (totalBurned, totalDistributed, burnBps); } }