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.
190 lines
7.6 KiB
Solidity
190 lines
7.6 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.19;
|
|
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
|
|
|
/**
|
|
* @title AereStaking
|
|
* @notice Delegated stake pool for AERE Network validators
|
|
* @dev Token holders delegate AERE to validators and earn 8% APY (matches genesis rewardRate).
|
|
* Validators earn block rewards for producing blocks; a portion is shared with delegators.
|
|
*
|
|
* Whitepaper alignment:
|
|
* - 100k AERE proposal threshold for governance (used elsewhere)
|
|
* - 7-day voting window
|
|
* - 5% slashing rate for misbehaving validators
|
|
* - 500 AERE minimum self-stake (genesis-defined)
|
|
*/
|
|
contract AereStaking is Ownable, ReentrancyGuard {
|
|
uint256 public constant MIN_VALIDATOR_SELF_STAKE = 500 ether; // 500 AERE
|
|
uint256 public constant MIN_DELEGATION = 1 ether; // 1 AERE
|
|
uint256 public constant UNBONDING_PERIOD = 7 days;
|
|
uint256 public constant ANNUAL_REWARD_RATE_BPS = 800; // 8% APY
|
|
uint256 public constant SLASH_RATE_BPS = 500; // 5%
|
|
|
|
struct Validator {
|
|
bool active;
|
|
uint256 selfStake;
|
|
uint256 totalDelegated;
|
|
uint256 commissionBps; // out of 10000
|
|
uint256 lastRewardBlock;
|
|
uint256 accumulatedRewards;
|
|
}
|
|
|
|
struct Delegation {
|
|
uint256 amount;
|
|
uint256 stakedAt;
|
|
uint256 lastClaimBlock;
|
|
}
|
|
|
|
struct UnbondRequest {
|
|
uint256 amount;
|
|
uint256 readyAt;
|
|
}
|
|
|
|
mapping(address => Validator) public validators;
|
|
mapping(address => mapping(address => Delegation)) public delegations; // validator => delegator => stake
|
|
mapping(address => UnbondRequest[]) public unbondQueue;
|
|
|
|
address[] public validatorList;
|
|
uint256 public totalStaked;
|
|
|
|
event ValidatorRegistered(address indexed validator, uint256 selfStake, uint256 commissionBps);
|
|
event ValidatorDeactivated(address indexed validator);
|
|
event Delegated(address indexed validator, address indexed delegator, uint256 amount);
|
|
event UnbondInitiated(address indexed validator, address indexed delegator, uint256 amount, uint256 readyAt);
|
|
event UnbondClaimed(address indexed delegator, uint256 amount);
|
|
event RewardsClaimed(address indexed delegator, uint256 amount);
|
|
event ValidatorSlashed(address indexed validator, uint256 amount, string reason);
|
|
|
|
/**
|
|
* @notice Register as a validator. Sender deposits self-stake (msg.value).
|
|
* @param commissionBps Validator's cut of delegator rewards (e.g. 1000 = 10%)
|
|
*/
|
|
function registerValidator(uint256 commissionBps) external payable nonReentrant {
|
|
require(msg.value >= MIN_VALIDATOR_SELF_STAKE, "AereStaking: insufficient self-stake");
|
|
require(commissionBps <= 5000, "AereStaking: max commission 50%");
|
|
require(!validators[msg.sender].active, "AereStaking: already registered");
|
|
|
|
validators[msg.sender] = Validator({
|
|
active: true,
|
|
selfStake: msg.value,
|
|
totalDelegated: 0,
|
|
commissionBps: commissionBps,
|
|
lastRewardBlock: block.number,
|
|
accumulatedRewards: 0
|
|
});
|
|
validatorList.push(msg.sender);
|
|
totalStaked += msg.value;
|
|
emit ValidatorRegistered(msg.sender, msg.value, commissionBps);
|
|
}
|
|
|
|
/**
|
|
* @notice Delegate AERE to a validator. msg.value is the delegation amount.
|
|
*/
|
|
function delegate(address validator) external payable nonReentrant {
|
|
require(msg.value >= MIN_DELEGATION, "AereStaking: below minimum delegation");
|
|
require(validators[validator].active, "AereStaking: validator not active");
|
|
|
|
Delegation storage d = delegations[validator][msg.sender];
|
|
if (d.amount == 0) {
|
|
d.stakedAt = block.timestamp;
|
|
d.lastClaimBlock = block.number;
|
|
}
|
|
d.amount += msg.value;
|
|
validators[validator].totalDelegated += msg.value;
|
|
totalStaked += msg.value;
|
|
emit Delegated(validator, msg.sender, msg.value);
|
|
}
|
|
|
|
/**
|
|
* @notice Initiate unbonding. Funds release after 7 days.
|
|
*/
|
|
function unbond(address validator, uint256 amount) external nonReentrant {
|
|
Delegation storage d = delegations[validator][msg.sender];
|
|
require(d.amount >= amount, "AereStaking: insufficient delegation");
|
|
|
|
d.amount -= amount;
|
|
validators[validator].totalDelegated -= amount;
|
|
totalStaked -= amount;
|
|
|
|
unbondQueue[msg.sender].push(UnbondRequest({
|
|
amount: amount,
|
|
readyAt: block.timestamp + UNBONDING_PERIOD
|
|
}));
|
|
emit UnbondInitiated(validator, msg.sender, amount, block.timestamp + UNBONDING_PERIOD);
|
|
}
|
|
|
|
/**
|
|
* @notice Claim all matured unbonding requests.
|
|
*/
|
|
function claimUnbonded() external nonReentrant {
|
|
uint256 total = 0;
|
|
UnbondRequest[] storage queue = unbondQueue[msg.sender];
|
|
uint256 i = 0;
|
|
while (i < queue.length) {
|
|
if (queue[i].readyAt <= block.timestamp) {
|
|
total += queue[i].amount;
|
|
queue[i] = queue[queue.length - 1];
|
|
queue.pop();
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
require(total > 0, "AereStaking: nothing to claim");
|
|
(bool ok, ) = msg.sender.call{value: total}("");
|
|
require(ok, "AereStaking: transfer failed");
|
|
emit UnbondClaimed(msg.sender, total);
|
|
}
|
|
|
|
/**
|
|
* @notice Pending rewards for a delegator (linear approximation: 8% APY).
|
|
*/
|
|
function pendingRewards(address validator, address delegator) public view returns (uint256) {
|
|
Delegation memory d = delegations[validator][delegator];
|
|
if (d.amount == 0) return 0;
|
|
uint256 blocksElapsed = block.number - d.lastClaimBlock;
|
|
// 100ms blocks → 315,360,000 blocks/year. Reward per block = stake * 8% / 315360000.
|
|
uint256 grossReward = (d.amount * ANNUAL_REWARD_RATE_BPS * blocksElapsed) / (315360000 * 10000);
|
|
// Subtract validator commission
|
|
uint256 commission = (grossReward * validators[validator].commissionBps) / 10000;
|
|
return grossReward - commission;
|
|
}
|
|
|
|
function claimRewards(address validator) external nonReentrant {
|
|
uint256 reward = pendingRewards(validator, msg.sender);
|
|
require(reward > 0, "AereStaking: nothing to claim");
|
|
delegations[validator][msg.sender].lastClaimBlock = block.number;
|
|
// In production, rewards come from the protocol-level mining supply (2.52B AERE).
|
|
// Until that's wired up via consensus, this contract must be funded by foundation.
|
|
require(address(this).balance >= reward, "AereStaking: pool insufficient (needs foundation top-up)");
|
|
(bool ok, ) = msg.sender.call{value: reward}("");
|
|
require(ok, "AereStaking: transfer failed");
|
|
emit RewardsClaimed(msg.sender, reward);
|
|
}
|
|
|
|
/**
|
|
* @notice Slash a validator (only callable by AereConsensus or owner).
|
|
*/
|
|
function slash(address validator, string calldata reason) external onlyOwner {
|
|
Validator storage v = validators[validator];
|
|
require(v.active, "AereStaking: validator not active");
|
|
uint256 slashAmount = (v.selfStake * SLASH_RATE_BPS) / 10000;
|
|
v.selfStake -= slashAmount;
|
|
totalStaked -= slashAmount;
|
|
if (v.selfStake < MIN_VALIDATOR_SELF_STAKE) {
|
|
v.active = false;
|
|
emit ValidatorDeactivated(validator);
|
|
}
|
|
emit ValidatorSlashed(validator, slashAmount, reason);
|
|
}
|
|
|
|
function validatorCount() external view returns (uint256) {
|
|
return validatorList.length;
|
|
}
|
|
|
|
/// @notice Allow foundation to seed the reward pool
|
|
receive() external payable {}
|
|
}
|