Smart Contracts
Five Solidity contracts encode the spec's economic rules on-chain. They live in
evm/contracts/, compile with solc 0.8.35 (EVM version cancun), and run inside
the EVM execution layer. Each maps to a numbered section of the spec.
These are faithful, runnable skeletons that prove the mechanics: ERC-20 cores, fee/relock/extension policies, and hardening are trimmed. They compile cleanly and pass the demos below, but they have not been audited. Treat them as the executable spec, not production code.
The suite
| Contract | Spec | Role |
|---|---|---|
| LiquaToken | §4/§5 | the ERC-20 — minted only by chain emission |
| VeLock | §2 | vote-escrow lock — boost + decaying weight |
| LiquidityLocker | §3 | proof-of-lock list for any token / LP |
| Governor98 | §6 | 98%-of-attesting-weight governor |
| RewardEscrow | §5 | reward-in-instance escrow + gated claim |
| LiquaPresaleToken | sale | pLIQUA — transferable presale receipt |
| LiquaSwap | sale | pLIQUA → LIQUA 1:1, gated to the ship date |
| SelfMineLiquidity | exch | self-mine → zap → time-locked AMM LP |
They reference each other in a small graph — the token's only minter is the escrow; the lock is governed by the
governor; the governor reads weight from the lock. Because some references are circular, deployment uses
predictAddress to pre-compute addresses before wiring.
RewardEscrow ──mint()──▶ LiquaToken ◀──transferFrom──┐
▲ │
only minter VeLock ──veWeight()──▶ Governor98
▲ onlyGov │ CHANGE.call()
└────────────────────┘
LiquaToken §4 / §5
A minimal ERC-20. The defining property: new supply is minted only by the RewardEscrow as block emission — there is no owner mint and no premine function.
| Member | Behaviour |
|---|---|
name·symbol·decimals | "Liqua" · "LIQUA" · 18 (constants) |
rewardEscrow | immutable — the only address allowed to mint |
transfer · approve · transferFrom | standard ERC-20; transferFrom honours infinite allowance |
mint(to, v) | require(msg.sender == rewardEscrow) — chain emission only (§5) |
Events: Transfer, Approval. Supply rises only at reward claim — see RewardEscrow.
VeLock §2
Vote-escrow LIQUA on a continuous sliding scale, 0 … DMAX (4 years). A longer remaining lock means a
higher boost and more weight; both decay linearly with remaining time (veCRV-style), so a fresh 4-year
lock starts at the cap and melts toward 1.0× as it approaches expiry.
| Member | Behaviour |
|---|---|
DMAX | 4 * 365 days — the sliding-scale maximum (constant) |
boostMaxBps | 5000 = +50% at the cap ("slightly higher") · governed (§6), max 20000 |
createLock(amount, duration) | escrows amount until now + duration (≤ DMAX); one lock per address |
increase(addAmount, newDuration) | add to and/or extend a lock — never shortens |
withdraw() | require(block.timestamp >= end) → reverts LOCKED_UNTIL_EXPIRY before expiry |
remainingBps(u) | remaining-time fraction in bps (0 once expired) |
boostBps(u) | BPS + boostMaxBps · remaining/BPS → 1.0× … 1.5× |
veWeight(u) | amount · remaining/DMAX — the weight Governor98 reads |
withdraw() reverts with
LOCKED_UNTIL_EXPIRY until t ≥ end. There is no early-exit path in the contract — this is the
§2 no-early-exit rule, enforced in code. Verified on-chain: a 100-LIQUA / 4-year lock reads veWeight 100 and boost
150.00%.
LiquidityLocker §3
Locks any token or LP position for a fixed term and exposes the locks as a public, paginated, chain-verifiable list. The §3 proof-of-lock dashboard renders straight from this — the list is the chain state, not an assertion.
| Member | Behaviour |
|---|---|
locks (Row[]) | append-only public list: owner · token · amount · lockedAt · unlockAt · withdrawn |
lock(token, amount, unlockAt) | escrows until unlockAt (must be future); returns the row id |
withdraw(id) | owner-only, after the gate — reverts LOCKED before unlockAt |
totalLockedOf(token) | live locked amount per token |
page(start, n) · ownerLocks(o) · count() | views for the marketing surface |
Events: Locked, Unlocked. The locked-liquidity dashboard
is exported from this state by evm/locker-demo.mjs.
Governor98 §6
Two powers — ROLLBACK (R9) and CHANGE (parameter update) — each require 98% agreement. The denominator is attesting ve-weight from a rolling window, supplied by the consensus layer each block (summing the online set on-chain would be unbounded gas), so idle stake can't freeze governance. Full how-to on the Governance page.
| Member | Behaviour |
|---|---|
QUORUM_BPS | 9800 = 98% (constant) |
attestingWeight | the rolling-window denominator; only the chain sets it via setAttestingWeight (onlyChain) |
propose(kind, payload) | kind ∈ {ROLLBACK, CHANGE}; payload is abi-encoded (height, or target+calldata) |
castVote(id) | adds your ve.veWeight(you) once; reverts if you have no weight or already voted |
quorumMet(id) | forWeight · BPS ≥ QUORUM_BPS · attestingWeight |
execute(id) | reverts BELOW_98 unless met; CHANGE does target.call(data), ROLLBACK emits for the chain to enact |
RewardEscrow §5
The heart of reward-in-instance. A block's emission is not credited as a balance — it is
escrowed to the instance address the chain derives from the block's own SOMA fingerprint, and the
miner claims it by proving control of that instance (R6 · instance = auth), only after the block has
confirmGate confirmations.
// the instance the reward is escrowed to:
rewardSeed = blake3(soma ∥ minerPub ∥ height) // chain/soma.mjs §5
instance = address derived from rewardSeed
// emission is minted to that instance ONLY at claim, ONLY after the gate
| Member | Behaviour |
|---|---|
confirmGate | 12 — confirmations required before a claim · governed (§6) |
head | current chain height (the confirmations oracle), set by head_() (onlyChain) |
seal(seed, instance, amount, height) | at block production — escrow the emission to the derived instance (onlyChain) |
claim(seed) | the miner's L2 getting-tool — see the gates below |
claim() enforces two rules in code:
require(msg.sender == e.instance)→ revertsNO_INSTANCE_NO_AUTH(R6 instance = auth)require(head >= e.height + confirmGate)→ revertsGATED_AWAIT_CONFIRMATIONS
Only when both pass does it token.mint(instance, amount) — so emission enters supply at claim, by the
rightful instance, after finality. Events: Sealed, Claimed.
The seam around the contract is now built. evm/reward-instance.mjs derives the dual-seed instance —
public rewardSeed = blake3(soma ∥ minerPub ∥ height) (the escrow key) and secret
instPriv = blake3(minerSecret ∥ rewardSeed), which only the miner can derive.
evm/claim-tool.mjs is the miner's L2 getting-tool: it re-derives the instance and calls
claim() once the gate clears, with an optional sweep to the miner's wallet
(node evm/claim-tool.mjs --miner-priv … --soma … --height … prints the targets).
Verified end-to-end by npm run claim:test (PASS): emission
escrowed → claim-before-gate reverts → wrong-secret reverts (R6 · instance = auth)
→ the instance claims → minted + swept → double-claim reverts, conserved to the wei. The one
remaining toggle is making seal() the default emission path in the live miner — today
produceBlock credits the coinbase directly so the pool/validator keep native liquidity, and the §5
escrow path runs alongside it.
Presale & swap pLIQUA → LIQUA
Two contracts handle the presale → launch token swap. Contributors receive a transferable receipt now and redeem it 1:1 for LIQUA at the ship date (Aug 1, 2026).
| Member | Behaviour |
|---|---|
LiquaPresaleToken (pLIQUA) | standard ERC-20 minted to contributors by the minter; burnFrom gated to the swap. Transferable, so it can trade pre-launch. |
swapOpensAt | the ship date as a unix timestamp — 1785542400 (Aug 1, 2026 UTC) |
swap(amount) | reverts SWAP_NOT_OPEN before the ship date; otherwise burns amount pLIQUA and transfers amount LIQUA 1:1 from the reserve |
reserve() | LIQUA held by the swap — funded from the 2% studio allocation; the swap is 1:1 reserve-backed |
Each redeem burns the pLIQUA, so total economic supply is conserved across the swap. Verified by
npm run swap:test: a pre-ship swap reverts, then after the ship date the buyer redeems 1:1, the pLIQUA is
burned, and the reserve drops by exactly the swapped amount.
SelfMineLiquidity exchange
Lives on the orderbook-DEX (orderbook-dex/contracts/), not the evm/ solc suite above — it's
the exchange-side locker for self-mining. It takes a miner's emission, zaps
it into the LIQUA/mUSDC AMM pool (swap ~half → add liquidity → mint LP), and time-locks the LP on a
rolling term. Only the miner withdraws, only after the gate; the emitter can deliver emission and nothing else.
| Member | Behaviour |
|---|---|
enroll(term) · setTerm · disable | miner self-service; MIN_TERM 1d … MAX_TERM 1460d (4y) |
accrueFor(miner, amount) | emitter-only; pulls LIQUA, zaps it, locks the LP, refreshes unlockAt = now + term (rolling) |
withdraw() | after disable + term → transfers the InfinityPool LP token to the miner |
positionOf · page · previewZap | proof-of-lock reads — position, the roll, a zap preview |
Single-sided invariant: the LIQUA reserve grows by exactly the mined amount; the quote reserve is borrowed for the
swap and returned by the mint (net unchanged). Verified by npx hardhat test in orderbook-dex/
(12 self-mine cases · 68 passing total).
Compile & run
node evm/compile.mjs # solc 0.8.35 · evmVersion cancun → evm/artifacts/
node evm/suite-demo.mjs # Token + VeLock: mint → approve → createLock (ve-lock verified)
node evm/gov-escrow-demo.mjs# Governor98 retunes VeLock via 98% CHANGE · RewardEscrow gated→claim
node evm/locker-demo.mjs # LiquidityLocker → exports the proof-of-lock dashboard
npm run swap:test # pLIQUA → LIQUA 1:1 · gated to Aug 1 2026 · burn-on-swap · conserved
Artifacts land in evm/artifacts/; evm/abi.mjs is a minimal selector + word codec (no
ethers dependency). All contracts compile with zero errors.