IMPERIAL CODEX
— LEX SCRIPTA —ROMAN LEGION is an on-chain game running on a single Uniswap v4 pool. Every 1 ROME token held is paired 1:1 with a Legion Upeg: buying ROME mints them, selling burns them. Stake your Legion to earn a share of the daily emission, and every buy enters a lottery for the Spoils Of War pot.
THE GAME LOOP
- I
ACQUIRE
Swap ETH for ROME via the canonical Uniswap v4 pool. Each whole unit of ROME you receive mints a new deferred Legion Upeg into your wallet (seed = 1, class = pending). A flat 1% buy tax is burned on the spot.
- II
REVEAL
Two blocks after the buy, the Legion's class and mining power are deterministically derived from a commit-reveal entropy mix (see Commit-Reveal below). You can force it with
revealUpeg(id), or just wait — every swap on the pool auto-anchors up to 5 from the global queue. - III
ENLIST
Stake your anchored Legion (up to 32 per tx) to claim a share of the daily emission proportional to your total mining power. The pot is 80% of emissions — the other 20% feeds the Spoils Of War lottery.
- IV
CLAIM
Withdraw your accrued wages whenever you want. A 10% claim tax is split back across every other staker as a “refined-ore boost” — the longer you stay staked, the more it compounds. Unstaking auto-claims.
- V
CONQUER
Every buy ≥ 0.01 ETH rolls for the Spoils Of War pot. Odds scale linearly from 0.016% at the floor up to a capped 1.00% at 1 ETH. A win is split 50/50 between the buyer and a power-weighted random staker.
TOKEN & EMISSIONS
| Initial supply | 10,000 ROME | Premined to the hook for liquidity bootstrapping. |
| Day-1 emission | 1,000 ROME / 24h | Streamed per-second to the staker pool. |
| Daily decay | × 0.99 | Each day's emission is 99% of the previous. |
| Half-life | ~69 days | Daily emission halves every ~69 days. Decay is geometric, never zero. |
| Emission split | 80 · 20 | 80% to stakers · 20% to Spoils Of War pot. |
| Buy tax | 1% | Taken on ROME out and immediately burned. |
| Sell tax | 1% | Taken on ETH out, funds the buyback bucket. |
| Claim tax | 10% | Redistributed to other stakers via the refined-ore boost. |
| Per-tx buy cap | maxBuy() | Starts at 2.5% of supply, +3bps every second after launch. |
Constants & formulas
UNIT_PER_UPEG = 1e18 // Rome.sol:21
INITIAL_SUPPLY = 10_000e18 // Rome.sol:22
DAY_1_EMISSION = 1_000e18 // Rome.sol:25
DAILY_DECAY_WAD = 0.99e18 // Rome.sol:26
SPOILS_OF_WAR_PCT = 20 // Rome.sol:114
TAX_BPS (hook) = 100 (= 1%) // RomeHook.sol:49
currentEmissionPerSec
= DAY_1_EMISSION * DAILY_DECAY_WAD^d / WAD / 86_400
where d = (now - launchTimestamp) / 86_400THE FIVE CLASSES
Every revealed Legion is assigned a class (1 of 5) and a hash value in [1, 100]. Mining power is the product. Higher-class Legion are rarer but multiply your stake share dramatically.
| Rank | Class | Odds | × | Mining power |
|---|---|---|---|---|
| I | Tiro | 60% | ×1 | 1 – 100 |
| II | Miles | 25% | ×2 | 2 – 200 |
| III | Centurio | 10% | ×3 | 3 – 300 |
| IV | Tribunus | 4% | ×4 | 4 – 400 |
| V | Legatus | 1% | ×5 | 5 – 500 |
Derivation
class: r = keccak256(upegId, seed, "ROME_CLASS") % 10_000
Tiro [ 0, 5_999]
Miles [6_000, 8_499]
Centurio [8_500, 9_499]
Tribunus [9_500, 9_899]
Legatus [9_900, 9_999]
hash: h = (keccak256(upegId, seed, "ROME_HASH") % 100) + 1
power: miningPower = h × multiplierOf(class)All artwork (helmet, armor, weapon, face, beard, earring, …) is rendered 100% on-chain as composed SVG. No IPFS, no off-chain dependency.
COMMIT-REVEAL — ANTI-SNIPE
A Legion's class isn't known at mint time — it's sealed by a two-block commitment that no single proposer can grind. Until revealed, every new Upeg shows seed = 1 (the deferred sentinel) and a power of zero.
- 1
Mint (block N)
Buy mints the Upeg with
seed = 1and recordsmintBlockOf[id] = N. - 2
Wait (≥ 2 blocks)
Two block hashes (
N+1andN+2) need to exist before the reveal can proceed. This binds the outcome to a multi-block commitment. - 3
Reveal
Entropy =
keccak256(blockhash(N+1), blockhash(N+2), hookSeed, "ROME_ANCHOR"). Class and hash are derived from this seed (see Classes). If 256 blocks have passed since mint and the hashes are pruned, a deterministic fallback fires using the latest blockhash.
revealUpeg(id)
Force-reveal a single Upeg. ~100k gas. Anyone can call. Use it the moment you want your Legion active.
revealUpegsBatch(ids[])
Reveal many in one tx; cheaper per-Upeg because the hook's random seed is read once.
anchorPending(N)
Walks the global queue from the front, anchoring up to N ≤ 20 deferred Upegs per call. Cheap way to clear the backlog for everyone.
Auto-sweep
Every swap on the canonical pool auto-anchors up to 5 Upegs and auto-resolves up to 2 Spoils rolls — so high pool activity keeps the queue short for free.
STAKING & REFINED-ORE BOOST
Stake any number of revealed Legion (up to 32 per transaction). Your share of the staker pool is your total mining power divided by global staked power, paid per second.
Refined-ore boost
When other stakers claim, 10% of their pending ROME is taxed at the contract level. That tax is not burned — it's redistributed pro-rata to every active staker (including the one claiming) by scaling the accRomePerPower accumulator. Effectively: the longer you stay staked, the more of other people's claim taxes compound into your share.
- Unstake auto-claims. Withdrawing a Legion settles its accrued ROME first, taking the 10% tax.
- Only anchored Upegs stake. A deferred Upeg (seed = 1) has zero power until revealed.
- Selling burns from the held set first. Staked Legion are safe from auto-burn on a sell — the lowest-id held Upegs get burned to rebalance your ROME count.
- Auto-service. Every stake/unstake/claim opportunistically anchors up to 3 Upegs from the global queue and resolves up to 2 Spoils rolls — paying that cost back to the community while you transact.
Reward math
pending(user)
= userTotalPower × (accRomePerPower − userRewardDebt) / WAD
On every settlement:
emission = secondsSinceLast × currentEmissionPerSec
spoilsAdd = emission × 20 / 100
stakerEmission = emission − spoilsAdd
accRomePerPower += stakerEmission × WAD / totalStakedPower
On claim:
payout = pending × 90 / 100
tax = pending − payout // distributed to other stakers
via taxScalar / accRomePerPower bumpSPOILS OF WAR
A jackpot that accumulates 20% of every emission tick and is rolled for by every qualifying buy. Half goes to the buyer who wins, half goes to a random staker weighted by power.
When does a buy roll?
- Buy must include ≥ 0.01 ETH of ETH value.
- You're cooldown-locked for 5 blocks after a roll (anti-spam).
- The roll resolves once two later blockhashes are available — same commit-reveal trick as Upeg minting.
Odds table
| ETH paid | Hit probability |
|---|---|
0.01 | 0.0160% |
0.10 | 0.1054% |
0.50 | 0.5030% |
1.00 | 1.0000% (cap) |
> 1.00 | 1.0000% (capped) |
If you win
The pot is split 50/50:
- Your half is minted directly to your wallet.
- The other half goes to a staker chosen at random with probability proportional to their staked power (Fenwick tree). If there are no eligible stakers, that half returns to the pot for the next roll.
Anyone can call resolveSpoilsOfWar(maxToResolve) (~800k gas hint) to clear pending rolls if the auto-sweep is lagging.
Roll formula
threshold = min(
ROLL_HIT_INTERCEPT + (ethPaid × ROLL_HIT_SLOPE) / 1 ETH,
ROLL_HIT_CAP
)
hit ⇔ keccak256(blockhash(N+1), blockhash(N+2),
buyer, poolSnapshot) % ROLL_DENOM < threshold
ROLL_DENOM = 1_000_000 // Rome.sol:108
ROLL_HIT_INTERCEPT = 60 // Rome.sol:109
ROLL_HIT_SLOPE = 9_940 // Rome.sol:110
ROLL_HIT_CAP = 10_000 // Rome.sol:111 (= 1.0%)BUYBACK ENGINE
The 1% sell tax on every ROME → ETH swap accumulates as ETH in the hook's buyback bucket. When it crosses a configurable threshold, anyone can trigger a buyback and the protocol uses the ETH to buy ROME back on the same pool — every token bought is then burned.
- Default threshold: 0.01 ETH (range 0.01–10 ETH, adjustable by the owner).
- Call:
buybackIfReady(uint256 minRomeOut)— anyone, with their own slippage protection. - Owner can pause buybacks via
setBuybackPaused(true)as a kill-switch. - Every buyback is logged on-chain — the dashboard's Buybacks panel reads from
buybackAt(i).
SAFETY & ADMIN
- No pause on ROME transfers or staking. The token and staking are non-custodial and always live.
- Owner-only knobs are minimal: set the hook once, point the canonical pool once, adjust buyback threshold, pause buybacks, sweep stranded non-bucket ETH from the hook. None of these can drain user balances or staked Legion.
- Upeg art is 100% on-chain. No IPFS dependency, no gateway can break the metadata.
- Quoter is display-only. It returns a first-order spot quote — never use it for slippage protection. The trade panel runs a depth-aware simulation via
eth_simulateV1before bindingminOut.
DEVELOPER REFERENCE
— FOR INTEGRATORS —Below is a non-exhaustive cheat-sheet of public functions on each contract. ABIs ship in @rome/abis and are bundled with the front-end — read them straight from there for an integration-ready source of truth.
Rome.sol — token, staking, claims
// Token function balanceOf(address) view returns (uint256) function transfer(address to, uint256 amount) returns (bool) function approve(address spender, uint256 amount) returns (bool) // Staking function stakeForMining(uint256 upegId) external function stakeForMiningBatch(uint256[] upegIds) external function unstakeFromMining(uint256 upegId) external function unstakeFromMiningBatch(uint256[] upegIds) external // Claim & rewards function claim() external function pendingRewards(address user) view returns (uint256) // Reveal & spoils function revealUpeg(uint256 upegId) external function revealUpegsBatch(uint256[] upegIds) external function anchorPending(uint8 maxToAnchor) external function resolveSpoilsOfWar(uint8 maxToResolve) external // Reads function getClass(uint256 upegId) view returns (uint8) function getHash(uint256 upegId) view returns (uint8) function getMiningPower(uint256 upegId) view returns (uint16) function currentEmissionPerSec() view returns (uint256) function maxBuy() view returns (uint256)
RomeHook.sol — v4 hook, buybacks
function buybackIfReady(uint256 minRomeOut) external function randomSeed() view returns (uint256) function qualifyingBuyMin() view returns (uint256) // owner-only function setCanonicalPool(PoolKey key) external onlyOwner function setBuybackThreshold(uint128) external onlyOwner function setBuybackPaused(bool) external onlyOwner
RomeSwapRouter.sol — buy / sell entry
function buy(uint256 minRomeOut) external payable returns (uint256) function sell(uint256 romeIn, uint256 minEthOut) external returns (uint256)
RomeQuoter.sol — display-only price
function currentSqrtPriceX96() view returns (uint160) function currentPrice() view returns (uint256) function quoteBuy(uint256 ethIn) view returns (uint256) // first-order function quoteSell(uint256 romeIn) view returns (uint256) // first-order
RomeLens.sol — read aggregator
function getGlobalStats() view returns (GlobalStats) function getUserStats(address) view returns (UserStats) function getOwnedUpegs(address, page, pageSize) view returns (UpegInfo[]) function getStakedUpegs(address, page, pageSize) view returns (UpegInfo[]) function getStakerPage(page, pageSize) view returns (StakerInfo[]) function getRecentSpoilsOfWarHits(limit) view returns (SpoilsOfWarHitRecord[]) function getRecentBuybacks(limit) view returns (BuybackRecord[])
Contract addresses are listed on the dashboard.