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