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

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 {}
}