// SPDX-License-Identifier: MIT pragma solidity 0.8.26; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; interface IStakeToken { function mint(address to, uint256 amount) external; } interface IStakeMinter { function launchTime() external view returns (uint64); } /// @title Ambrosia staking /// @notice Stake $AMBR, earn HYPE from every round (3%) and every trade (0.5%), plus 50,000,000 $AMBR spread /// evenly over the 90 days after launch. Unstaking waits 3 days and earns nothing while it waits. /// HYPE that arrives while nobody is staked waits for the first staker. No owner. contract AmbrosiaStaking is ReentrancyGuard { using SafeERC20 for IERC20; uint256 public constant REWARD_TOKENS = 50_000_000 ether; uint256 public constant REWARD_PERIOD = 90 days; uint256 public constant COOLDOWN = 3 days; uint256 private constant ACC = 1e36; IERC20 public immutable ambr; IStakeMinter public immutable minter; uint256 public totalStaked; uint256 public accHype; // HYPE per staked token, scaled by 1e36 uint256 public accAmbr; // $AMBR per staked token, scaled by 1e36 uint256 public lastAmbrUpdate; uint256 public waitingHype; struct Staker { uint256 amount; uint256 hypeDebt; uint256 ambrDebt; uint256 owedHype; uint256 owedAmbr; uint256 cooling; uint64 coolingUntil; } mapping(address => Staker) public stakers; event Staked(address indexed who, uint256 amount); event UnstakeRequested(address indexed who, uint256 amount, uint256 until); event Withdrawn(address indexed who, uint256 amount); event Rewarded(address indexed who, uint256 hype, uint256 ambr); event HypeIn(address indexed from, uint256 amount); error Zero(); error TooMuch(); error Cooling(uint256 until); error SendFailed(); constructor(IERC20 ambr_, IStakeMinter minter_) { ambr = ambr_; minter = minter_; } receive() external payable { _hypeIn(msg.value); } function notifyHype() external payable { _hypeIn(msg.value); } function _hypeIn(uint256 v) private { if (v == 0) return; emit HypeIn(msg.sender, v); if (totalStaked == 0) { waitingHype += v; return; } accHype += (v + waitingHype) * ACC / totalStaked; waitingHype = 0; } // ------------------------------------------------------------------ views function rewardWindow() public view returns (uint256 start, uint256 end) { uint256 l = minter.launchTime(); if (l == 0) return (0, 0); return (l, l + REWARD_PERIOD); } function _accAmbrNow() private view returns (uint256 acc, uint256 at) { (uint256 start, uint256 end) = rewardWindow(); acc = accAmbr; at = block.timestamp; if (start == 0 || totalStaked == 0) return (acc, at); uint256 from = lastAmbrUpdate < start ? start : lastAmbrUpdate; uint256 to = at < end ? at : end; if (to > from) acc += (to - from) * REWARD_TOKENS / REWARD_PERIOD * ACC / totalStaked; } function earned(address a) public view returns (uint256 hype, uint256 tokens) { Staker storage s = stakers[a]; (uint256 accA,) = _accAmbrNow(); hype = s.owedHype + s.amount * accHype / ACC - s.hypeDebt; tokens = s.owedAmbr + s.amount * accA / ACC - s.ambrDebt; } // ---------------------------------------------------------------- actions function stake(uint256 amount) external nonReentrant { if (amount == 0) revert Zero(); _update(msg.sender); ambr.safeTransferFrom(msg.sender, address(this), amount); Staker storage s = stakers[msg.sender]; s.amount += amount; totalStaked += amount; _sync(s); uint256 w = waitingHype; if (w > 0) { waitingHype = 0; accHype += w * ACC / totalStaked; } emit Staked(msg.sender, amount); } function requestUnstake(uint256 amount) external nonReentrant { Staker storage s = stakers[msg.sender]; if (amount == 0) revert Zero(); if (amount > s.amount) revert TooMuch(); _update(msg.sender); s.amount -= amount; totalStaked -= amount; s.cooling += amount; s.coolingUntil = uint64(block.timestamp + COOLDOWN); _sync(s); emit UnstakeRequested(msg.sender, amount, s.coolingUntil); } function withdraw() external nonReentrant { Staker storage s = stakers[msg.sender]; uint256 amt = s.cooling; if (amt == 0) revert Zero(); if (block.timestamp < s.coolingUntil) revert Cooling(s.coolingUntil); s.cooling = 0; ambr.safeTransfer(msg.sender, amt); emit Withdrawn(msg.sender, amt); } function claim() external nonReentrant { _update(msg.sender); Staker storage s = stakers[msg.sender]; uint256 h = s.owedHype; uint256 t = s.owedAmbr; s.owedHype = 0; s.owedAmbr = 0; if (t > 0) IStakeToken(address(ambr)).mint(msg.sender, t); if (h > 0) { (bool ok,) = msg.sender.call{value: h}(""); if (!ok) revert SendFailed(); } emit Rewarded(msg.sender, h, t); } // -------------------------------------------------------------- internals function _update(address a) private { (uint256 acc, uint256 at) = _accAmbrNow(); accAmbr = acc; lastAmbrUpdate = at; Staker storage s = stakers[a]; s.owedHype += s.amount * accHype / ACC - s.hypeDebt; s.owedAmbr += s.amount * acc / ACC - s.ambrDebt; } function _sync(Staker storage s) private { s.hypeDebt = s.amount * accHype / ACC; s.ambrDebt = s.amount * accAmbr / ACC; } }