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.
269 lines
10 KiB
Solidity
269 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/ERC20/IERC20.sol";
|
|
|
|
interface IBurnVaultV2 {
|
|
function burn() external payable;
|
|
}
|
|
|
|
interface IAereSinkV2 {
|
|
function flush(address token, uint256 amount) external;
|
|
}
|
|
|
|
interface IWAERE {
|
|
function deposit() external payable;
|
|
function approve(address guy, uint256 wad) external returns (bool);
|
|
}
|
|
|
|
/**
|
|
* @title AereCoinbaseSplitterV2
|
|
* @notice V2 of the validator coinbase splitter. Replaces V1's 2-way split
|
|
* (burn / validator-rebate) with a 3-way split that ADDS the AereSink
|
|
* bucket so a portion of every validator reward feeds the sAERE
|
|
* flywheel.
|
|
*
|
|
* Three buckets, all in basis points, owner-configurable within hard
|
|
* caps:
|
|
* burnBps — direct burn at AereFeeBurnVault (default 3750 = 37.5%)
|
|
* sinkBps — routed through AereSink (default 1500 = 15.0%)
|
|
* rebateBps — back to validator (default 4750 = 47.5%, derived)
|
|
*
|
|
* The 3-way default lifts the protocol-level deflation rate from 37.5%
|
|
* to ~52.5% net (burn bucket plus most of the sink — see saere_architecture
|
|
* for AereSink's internal 15/40/45 split which makes 55% of sink output
|
|
* a burn).
|
|
*
|
|
* @dev Owner can adjust bps within these limits:
|
|
* - burnBps ∈ [0, 5000]
|
|
* - sinkBps ∈ [0, 3000]
|
|
* - rebateBps = 10000 - burnBps - sinkBps (auto-derived; revert if < 0)
|
|
* The sink address itself is owner-settable but rate-limited: a
|
|
* setSink call has a 7-day timelock to give validators warning before
|
|
* a sink swap.
|
|
*
|
|
* This is the "in-place upgrade" that Week-1 references. The V1
|
|
* contract stays deployed and any AERE still flowing through it
|
|
* continues to honor the V1 burn behaviour. Validators migrate to
|
|
* V2 by switching the forwarder daemon's target address (off-chain
|
|
* config). Once migration is complete, V1 burnBps can be set to 0
|
|
* and the V1 contract retired.
|
|
*
|
|
* Funds-safety: V2 cannot custody AERE across calls; every
|
|
* splitAndDistribute / splitToSelf fully dispatches msg.value.
|
|
* The sink bucket is wrapped via WAERE → IAereSink.flush() in one
|
|
* transaction; if the sink call reverts, the whole tx reverts and
|
|
* the validator can retry.
|
|
*/
|
|
contract AereCoinbaseSplitterV2 is Ownable, ReentrancyGuard {
|
|
|
|
/* --------------------------------- state --------------------------------- */
|
|
|
|
address payable public burnVault;
|
|
address public sink; // AereSink
|
|
address public immutable WAERE_ADDR;
|
|
|
|
uint256 public burnBps = 3750; // 37.5%
|
|
uint256 public sinkBps = 1500; // 15.0%
|
|
// rebateBps = 10000 - burnBps - sinkBps (computed live)
|
|
|
|
uint256 public constant BPS = 10_000;
|
|
uint256 public constant SINK_CHANGE_TIMELOCK = 7 days;
|
|
uint256 public constant MAX_BURN_BPS = 5000;
|
|
uint256 public constant MAX_SINK_BPS = 3000;
|
|
|
|
// Cumulative lifetime accounting.
|
|
uint256 public totalBurned;
|
|
uint256 public totalSentToSink;
|
|
uint256 public totalRebated;
|
|
|
|
mapping(address => uint256) public burnedBy;
|
|
mapping(address => uint256) public sentToSinkBy;
|
|
mapping(address => uint256) public rebatedTo;
|
|
|
|
// Sink rotation timelock.
|
|
address public pendingSink;
|
|
uint256 public pendingSinkEarliestSetTs;
|
|
|
|
/* --------------------------------- events --------------------------------- */
|
|
|
|
event Burned(address indexed contributor, uint256 amount, uint256 newTotalBurned);
|
|
event SinkRouted(address indexed contributor, uint256 amount, uint256 newTotalSentToSink);
|
|
event Rebated(address indexed validator, uint256 amount, uint256 newTotalRebated);
|
|
event BpsChanged(uint256 oldBurnBps, uint256 oldSinkBps, uint256 newBurnBps, uint256 newSinkBps);
|
|
event BurnVaultChanged(address oldVault, address newVault);
|
|
event PendingSinkProposed(address indexed newSink, uint256 earliestSet);
|
|
event SinkChanged(address oldSink, address newSink);
|
|
event PendingSinkCancelled();
|
|
|
|
/* --------------------------------- errors --------------------------------- */
|
|
|
|
error ZeroAddress();
|
|
error BpsOutOfRange();
|
|
error TimelockNotElapsed(uint256 nowTs, uint256 earliest);
|
|
error NoPendingProposal();
|
|
error DistributeFailed();
|
|
error ZeroAmount();
|
|
|
|
/* ------------------------------- constructor ------------------------------ */
|
|
|
|
constructor(address payable _burnVault, address _waere) {
|
|
if (_burnVault == address(0) || _waere == address(0)) revert ZeroAddress();
|
|
burnVault = _burnVault;
|
|
WAERE_ADDR = _waere;
|
|
// sink starts unset — until owner calls proposeSink+acceptSink, V2
|
|
// behaves like V1 (no sink bucket; the 15% sinkBps default would
|
|
// also be skipped, with that 15% rebated instead).
|
|
}
|
|
|
|
/* ------------------------------- admin (rate) ----------------------------- */
|
|
|
|
/// Owner can adjust burn/sink bps within caps. rebateBps is derived.
|
|
function setBps(uint256 _burnBps, uint256 _sinkBps) external onlyOwner {
|
|
if (_burnBps > MAX_BURN_BPS) revert BpsOutOfRange();
|
|
if (_sinkBps > MAX_SINK_BPS) revert BpsOutOfRange();
|
|
if (_burnBps + _sinkBps > BPS) revert BpsOutOfRange();
|
|
emit BpsChanged(burnBps, sinkBps, _burnBps, _sinkBps);
|
|
burnBps = _burnBps;
|
|
sinkBps = _sinkBps;
|
|
}
|
|
|
|
function setBurnVault(address payable _vault) external onlyOwner {
|
|
if (_vault == address(0)) revert ZeroAddress();
|
|
emit BurnVaultChanged(burnVault, _vault);
|
|
burnVault = _vault;
|
|
}
|
|
|
|
/* ------------------------------- admin (sink) ----------------------------- */
|
|
|
|
function proposeSink(address _sink) external onlyOwner {
|
|
if (_sink == address(0)) revert ZeroAddress();
|
|
pendingSink = _sink;
|
|
pendingSinkEarliestSetTs = block.timestamp + SINK_CHANGE_TIMELOCK;
|
|
emit PendingSinkProposed(_sink, pendingSinkEarliestSetTs);
|
|
}
|
|
|
|
function acceptSink() external onlyOwner {
|
|
if (pendingSink == address(0)) revert NoPendingProposal();
|
|
if (block.timestamp < pendingSinkEarliestSetTs) {
|
|
revert TimelockNotElapsed(block.timestamp, pendingSinkEarliestSetTs);
|
|
}
|
|
address old = sink;
|
|
sink = pendingSink;
|
|
delete pendingSink;
|
|
delete pendingSinkEarliestSetTs;
|
|
emit SinkChanged(old, sink);
|
|
}
|
|
|
|
function cancelPendingSink() external onlyOwner {
|
|
if (pendingSink == address(0)) revert NoPendingProposal();
|
|
delete pendingSink;
|
|
delete pendingSinkEarliestSetTs;
|
|
emit PendingSinkCancelled();
|
|
}
|
|
|
|
/* ----------------------------------- core --------------------------------- */
|
|
|
|
/**
|
|
* @notice Split msg.value 3-ways. Anyone can call. Sink bucket is skipped
|
|
* (added to rebate) when sink is unset.
|
|
*/
|
|
function splitAndDistribute(address payable validator) external payable nonReentrant {
|
|
if (msg.value == 0) revert ZeroAmount();
|
|
if (validator == address(0)) revert ZeroAddress();
|
|
|
|
uint256 burnAmt = (msg.value * burnBps) / BPS;
|
|
uint256 sinkAmt = (sink == address(0)) ? 0 : (msg.value * sinkBps) / BPS;
|
|
uint256 rebateAmt = msg.value - burnAmt - sinkAmt;
|
|
|
|
if (burnAmt > 0) {
|
|
IBurnVaultV2(burnVault).burn{value: burnAmt}();
|
|
totalBurned += burnAmt;
|
|
burnedBy[msg.sender] += burnAmt;
|
|
emit Burned(msg.sender, burnAmt, totalBurned);
|
|
}
|
|
|
|
if (sinkAmt > 0) {
|
|
// Wrap msg.value's sink share into WAERE, then approve+flush.
|
|
IWAERE(WAERE_ADDR).deposit{value: sinkAmt}();
|
|
IWAERE(WAERE_ADDR).approve(sink, sinkAmt);
|
|
IAereSinkV2(sink).flush(WAERE_ADDR, sinkAmt);
|
|
totalSentToSink += sinkAmt;
|
|
sentToSinkBy[msg.sender] += sinkAmt;
|
|
emit SinkRouted(msg.sender, sinkAmt, totalSentToSink);
|
|
}
|
|
|
|
if (rebateAmt > 0) {
|
|
(bool ok, ) = validator.call{value: rebateAmt}("");
|
|
if (!ok) revert DistributeFailed();
|
|
totalRebated += rebateAmt;
|
|
rebatedTo[validator] += rebateAmt;
|
|
emit Rebated(validator, rebateAmt, totalRebated);
|
|
}
|
|
}
|
|
|
|
function splitToSelf() external payable nonReentrant {
|
|
_splitToValidator(payable(msg.sender));
|
|
}
|
|
|
|
function _splitToValidator(address payable validator) internal {
|
|
if (msg.value == 0) revert ZeroAmount();
|
|
|
|
uint256 burnAmt = (msg.value * burnBps) / BPS;
|
|
uint256 sinkAmt = (sink == address(0)) ? 0 : (msg.value * sinkBps) / BPS;
|
|
uint256 rebateAmt = msg.value - burnAmt - sinkAmt;
|
|
|
|
if (burnAmt > 0) {
|
|
IBurnVaultV2(burnVault).burn{value: burnAmt}();
|
|
totalBurned += burnAmt;
|
|
burnedBy[msg.sender] += burnAmt;
|
|
emit Burned(msg.sender, burnAmt, totalBurned);
|
|
}
|
|
|
|
if (sinkAmt > 0) {
|
|
IWAERE(WAERE_ADDR).deposit{value: sinkAmt}();
|
|
IWAERE(WAERE_ADDR).approve(sink, sinkAmt);
|
|
IAereSinkV2(sink).flush(WAERE_ADDR, sinkAmt);
|
|
totalSentToSink += sinkAmt;
|
|
sentToSinkBy[msg.sender] += sinkAmt;
|
|
emit SinkRouted(msg.sender, sinkAmt, totalSentToSink);
|
|
}
|
|
|
|
if (rebateAmt > 0) {
|
|
(bool ok, ) = validator.call{value: rebateAmt}("");
|
|
if (!ok) revert DistributeFailed();
|
|
totalRebated += rebateAmt;
|
|
rebatedTo[validator] += rebateAmt;
|
|
emit Rebated(validator, rebateAmt, totalRebated);
|
|
}
|
|
}
|
|
|
|
/* ----------------------------------- views -------------------------------- */
|
|
|
|
function rebateBps() external view returns (uint256) {
|
|
return BPS - burnBps - sinkBps;
|
|
}
|
|
|
|
function splitStats() external view returns (
|
|
uint256 lifetimeBurned,
|
|
uint256 lifetimeSentToSink,
|
|
uint256 lifetimeRebated,
|
|
uint256 currentBurnBps,
|
|
uint256 currentSinkBps,
|
|
uint256 currentRebateBps,
|
|
address currentSink
|
|
) {
|
|
return (
|
|
totalBurned,
|
|
totalSentToSink,
|
|
totalRebated,
|
|
burnBps,
|
|
sinkBps,
|
|
BPS - burnBps - sinkBps,
|
|
sink
|
|
);
|
|
}
|
|
}
|