aere-contracts/contracts/AereMiningDistributor.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
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.
2026-07-20 01:02:37 +03:00

147 lines
6.4 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
/**
* @title AereMiningDistributor
* @notice Merkle-claim payout distributor for AireFlow / mining-subscription
* epoch rewards.
*
* Workflow each epoch (default = 30 days, matches AereMiningSubscription):
*
* 1. Foundation runs an off-chain scheduler that walks all active
* subscriptions, computes each subscriber's reward share (a function
* of tier + days-active), produces a Merkle tree of
* (subscriber, amount) leaves and a root.
* 2. Foundation funds this contract with the epoch's total AERE payout
* by plain transfer or calling `fundEpoch{value:X}()`.
* 3. Foundation calls `postEpoch(root, totalAllocated, claimDeadline)`.
* The new epoch id is returned. The root is immutable for that epoch.
* 4. Each subscriber claims via `claim(epochId, index, amount, proof)`.
* The contract pays them in native AERE.
* 5. After `claimDeadline`, Foundation may `sweepUnclaimed(epochId)`
* to recover unclaimed funds (rolled into the next epoch's bucket).
*
* Funding source: AereMiningReserve wallet (whitepaper §3.2, 1.4B AERE
* earmarked over the network's life).
*
* This is not in custody of subscribers — they pull, contract never pushes.
* Single-claim guard: bitmap per epoch indexed by leaf index.
*/
contract AereMiningDistributor is Ownable, ReentrancyGuard {
struct Epoch {
bytes32 merkleRoot;
uint256 totalAllocated; // funds reserved for this epoch's leaves
uint256 totalClaimed;
uint64 claimDeadline;
bool swept;
}
Epoch[] public epochs;
/// epochId => packed bitmap (1 bit per leaf index claimed)
mapping(uint256 => mapping(uint256 => uint256)) private claimedBitmap;
event EpochPosted(uint256 indexed epochId, bytes32 merkleRoot, uint256 totalAllocated, uint64 claimDeadline);
event Funded(address indexed from, uint256 amount);
event Claimed(uint256 indexed epochId, uint256 indexed index, address indexed account, uint256 amount);
event Swept(uint256 indexed epochId, uint256 unclaimed);
receive() external payable { emit Funded(msg.sender, msg.value); }
function fundEpoch() external payable { emit Funded(msg.sender, msg.value); }
function epochCount() external view returns (uint256) { return epochs.length; }
function postEpoch(bytes32 merkleRoot, uint256 totalAllocated, uint64 claimDeadline)
external onlyOwner returns (uint256 epochId)
{
require(merkleRoot != bytes32(0), "MiningDist: zero root");
require(totalAllocated > 0, "MiningDist: zero total");
require(claimDeadline > block.timestamp, "MiningDist: deadline past");
require(address(this).balance >= _liveFunds() + totalAllocated, "MiningDist: underfunded");
epochs.push(Epoch({
merkleRoot: merkleRoot,
totalAllocated: totalAllocated,
totalClaimed: 0,
claimDeadline: claimDeadline,
swept: false
}));
epochId = epochs.length - 1;
emit EpochPosted(epochId, merkleRoot, totalAllocated, claimDeadline);
}
function isClaimed(uint256 epochId, uint256 index) public view returns (bool) {
uint256 wordIndex = index / 256;
uint256 bitIndex = index % 256;
uint256 word = claimedBitmap[epochId][wordIndex];
return (word & (1 << bitIndex)) != 0;
}
function _setClaimed(uint256 epochId, uint256 index) private {
uint256 wordIndex = index / 256;
uint256 bitIndex = index % 256;
claimedBitmap[epochId][wordIndex] |= (1 << bitIndex);
}
function claim(uint256 epochId, uint256 index, address account, uint256 amount, bytes32[] calldata proof)
external nonReentrant
{
require(epochId < epochs.length, "MiningDist: bad epoch");
Epoch storage e = epochs[epochId];
require(block.timestamp <= e.claimDeadline, "MiningDist: expired");
require(!isClaimed(epochId, index), "MiningDist: already claimed");
bytes32 leaf = keccak256(abi.encodePacked(index, account, amount));
require(_verify(proof, e.merkleRoot, leaf), "MiningDist: bad proof");
_setClaimed(epochId, index);
e.totalClaimed += amount;
require(e.totalClaimed <= e.totalAllocated, "MiningDist: over-allocated");
(bool ok, ) = account.call{ value: amount }("");
require(ok, "MiningDist: payout failed");
emit Claimed(epochId, index, account, amount);
}
function sweepUnclaimed(uint256 epochId) external onlyOwner nonReentrant {
require(epochId < epochs.length, "MiningDist: bad epoch");
Epoch storage e = epochs[epochId];
require(!e.swept, "MiningDist: already swept");
require(block.timestamp > e.claimDeadline, "MiningDist: too early");
uint256 unclaimed = e.totalAllocated - e.totalClaimed;
e.swept = true;
if (unclaimed > 0) {
(bool ok, ) = msg.sender.call{ value: unclaimed }("");
require(ok, "MiningDist: sweep failed");
}
emit Swept(epochId, unclaimed);
}
/// Funds still reserved by every un-swept epoch, INCLUDING epochs whose
/// claimDeadline has passed but that have not yet been swept. An expired
/// epoch still physically holds its unclaimed AERE (sweepUnclaimed sends
/// exactly that reserve to the owner), so it must keep reserving those
/// funds until it is actually swept — otherwise the same wei could both
/// back a freshly posted epoch AND be swept to the owner, double-counting
/// it and stranding the new epoch's honest claimants. Reservation leaves
/// _liveFunds exactly once: when sweepUnclaimed sets `swept = true`.
function _liveFunds() internal view returns (uint256 reserved) {
for (uint256 i = 0; i < epochs.length; i++) {
Epoch storage e = epochs[i];
if (!e.swept) {
reserved += e.totalAllocated - e.totalClaimed;
}
}
}
function _verify(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
bytes32 hash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
bytes32 p = proof[i];
hash = hash < p ? keccak256(abi.encodePacked(hash, p)) : keccak256(abi.encodePacked(p, hash));
}
return hash == root;
}
}