aere-contracts/contracts/AereCoinbaseSplitter.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

145 lines
6.4 KiB
Solidity

// 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% of the VALIDATOR
* COINBASE REWARD, not of transaction fees and not a base-fee burn),
* 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 realized
* burn in O(1) reads.
*
* MEASURED REALITY, and read this before quoting the 37.5% anywhere: the
* burn is a percentage of validator coinbase revenue, and that revenue is
* currently ZERO on chain 2800. AereFeeBurnVault holds approximately 0.137
* AERE against a 2.8 billion fixed supply (eth_getBalance at block
* 10,571,949). 37.5% is a CONDITIONAL RATE on future revenue, not a
* statement that tokens are being destroyed today. AERE is NOT deflationary
* today, and this contract should never be cited as evidence that it is.
*/
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);
}
}