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.
147 lines
6.4 KiB
Solidity
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;
|
|
}
|
|
}
|