Skip to content
Minerva. Post-quantum, proof of work.

Documentation · Bridge

Bridge Accounting, Cancellation, and ETH Rewards Specification (Draft 0.1)

docs/03-bridge-accounting-and-rewards-spec.md · commit e561109 · 2026-10-23

Status: draft for decision. This document keeps the chosen model:

Existing, non-mintable Ethereum MRV ↔ native Minerva coin, using real liquidity on both sides, with ETH epoch rewards for liquidity providers. No wMRV.

It replaces the undefined parts of that model with explicit ledgers, invariants, a cancellation protocol that cannot double-pay, and a reward formula users can verify. Findings F5 to F8 in 01-review-verification.md (01-review-verification.md, not published) are the motivation; this document is the specification that resolves them.

1. Model

Two vaults, one message protocol, one set of ledgers kept in sync by finalized events:

  • Ethereum vault (MRVBridgeLiquidityVault successor): holds ERC-20 MRV.
  • Minerva vault (MinervaNativeBridgeVault successor): holds native coin.
  • Transfers move value by releasing from one vault's available inventory after a lock or deposit is final on the other chain.

A 1:1 quote is offered while capacity permits. Capacity is a published number derived from the ledgers below; it is never "the vault balance".

2. Ledgers

Every unit of value in either vault belongs to exactly one category at any time. Categories are on-chain storage, not indexer opinions.

Category Ethereum vault Minerva vault Owner
LP LP-owned ERC-20 MRV, by shares LP-owned native coin, by shares (if native LPs exist, DECISION) liquidity providers
ESCROW user deposits awaiting Minerva release or safe cancellation user locks awaiting Ethereum release or safe cancellation the depositing user
MIGRATION n/a native coin reserved for the 1,000,000 MRV one-way migration migration claimants
RESERVED_WITHDRAWAL LP inventory committed to queued withdrawals same the requesting LP
RESERVED_RELEASE inventory reserved for an in-flight inbound message same the inbound recipient
PROTOCOL protocol-owned inventory (fees retained, treasury seeding) same the protocol
REWARDS ETH only, in the reward distributor n/a LPs, by share-seconds

Rules:

  1. balance(vault) == LP + ESCROW + MIGRATION + RESERVED_WITHDRAWAL + RESERVED_RELEASE + PROTOCOL on each chain, checked by an invariant test on every state change.
  2. available = LP + PROTOCOL - RESERVED_WITHDRAWAL - RESERVED_RELEASE. Only available may be released to a bridge recipient. ESCROW and MIGRATION are never release capacity.
  3. A release on chain X moves amount from available_X to the recipient and, on finalization of the matching lock on chain Y, moves the lock's ESCROW_Y into LP_Y (pro rata to shares) or PROTOCOL_Y. This is the step that gives LPs a claim on the inventory that accumulates on the other chain. Today nothing performs it; confirmOutboundExecuted has no callers.
  4. LP share value on chain X is LP_X / totalShares_X. The cross-chain LP position is the pair (shares_X, shares_Y) plus any unsettled claims in 3. The interface shows the pair, not a single number.

3. Invariants

Written as the Foundry invariant handlers the implementation must ship.

  • I1 Conservation: for each chain, the sum of categories equals the vault balance.
  • I2 No overdraft: every release leaves available >= 0.
  • I3 Single owner: a message hash appears in at most one of ESCROW, RESERVED_RELEASE, or the executed set on each chain.
  • I4 No double payout: for every message, executed_dest + refunded_src <= 1.
  • I5 Withdrawal honesty: a withdrawal request burns the shares immediately and moves the payout, valued at the request-time price, out of the inventories into RESERVED_WITHDRAWAL. The requester has economically exited at request time; the cooldown only delays collection. Later releases or drift affect neither the requester nor the remaining LPs. (Decision taken while implementing: this is simpler and fairer than completion-time pricing, and it makes queued withdrawals untouchable by releases by construction.)
  • I6 Migration segregation: MIGRATION only decreases by successful migration claims and never by releases, refunds, or withdrawals.
  • I7 Rewards are not principal: ETH in the reward distributor never flows into any vault category, and no vault category ever funds a reward.

4. Transfer state machine and safe cancellation

4.1 States

Per message hash, on the source chain:

Locked → (Executed | Cancelled → Refunded)

On the destination chain:

Unknown → Reserved → Released or Unknown → Expired

4.2 Deadlines

A message carries deadline (source-chain timestamp). Rules:

  • Destination release is valid only while destClock < deadline - skewMargin. Enforced on-chain on both sides (today the Ethereum side does not check it).
  • Source refund is valid only after srcClock > deadline + skewMargin + finalityMargin and one of the two conditions in 4.3 holds.
  • skewMargin covers clock drift between chains (recommended 15 minutes). finalityMargin covers destination finality (recommended the destination's confirmation depth in time, with a floor of one hour on Ethereum).

4.3 Cancellation conditions

A refund requires evidence, never a timer alone. Exactly one of:

  1. Non-execution attestation: a guardian-threshold signature over (messageHash, "NOT_EXECUTED", destChainId, destBlockNumber) produced after the destination deadline passed, asserting the message was never released. Guardians produce it only by reading finalized destination state. The source vault verifies the threshold and moves the lock to Cancelled, then Refunded.
  2. Destination expiry record: after the deadline the destination registry allows anyone to mark the message Expired (a cheap on-chain record proving it can never be released, because release checks != Expired). A relayer or the user then presents the expiry via the normal message channel to the source chain, which refunds.

Design 2 is preferable because it needs no trusted statement about absence, only a destination state transition that is itself final. Design 1 is the fallback when the destination chain cannot host the record. DECISION: ship 2, keep 1 for emergency recovery under a timelock.

4.4 Execution confirmation

When the destination releases, the destination registry marks Released. The relayer is obliged to present the release receipt to the source chain, which moves the lock to Executed and settles the escrow per section 2 rule 3. If the relayer fails, anyone may present it; the source chain pays a bounty from PROTOCOL (DECISION on amount). Without this step the escrow sits unsettled, which is safe (it is never release capacity) but reduces LP yield.

4.5 Symmetric verification

The Ethereum vault must verify messages and guardian attestations on-chain exactly as the Minerva vault does, and must enforce the deadline. A relayer role may submit, but cannot authorize. Finding F6 is closed when releaseToBridgeRecipient takes the full message and attestations.

5. Withdrawal queue and capacity

  • Withdrawals enter a FIFO queue with a cooldown. Entering the queue moves the estimated amount from LP to RESERVED_WITHDRAWAL, which reduces available immediately (as today).
  • The queue head is visible: position, amount, earliest completion, and whether available on that chain currently covers everything ahead of it.
  • If LP_X is below the queued total (one-sided drain), the interface shows "withdrawal capacity on Ethereum: N MRV; N' MRV accumulating on Minerva awaiting settlement" using the section 2 ledgers. It does not show "100,000 MRV available".
  • Conservative limits: per-release cap and daily cap are expressed as a percentage of available, not as absolute amounts (today the daily cap is half the token supply). Recommended starting values: 10% of available per release, 30% per day.

6. Loss allocation policy

If a vault loses value (contract bug, guardian compromise, operational error), the policy is written before launch, not improvised:

  1. ESCROW is made whole first from PROTOCOL, then from a published insurance reserve.
  2. MIGRATION is never used to cover losses.
  3. Remaining loss is borne by LP pro rata to shares on the affected chain.
  4. REWARDS are not used to cover principal.

Published as part of the LP terms and enforced by the same invariant handlers.

7. ETH epoch rewards

7.1 Formula

For epoch e with funded ETH F_e:

reward_u(e) = F_e × shareSeconds_u(e) / totalShareSeconds(e)

where shareSeconds_u(e) = Σ shares_u(t) · dt over the eligible time in e. An LP supplying the same shares for half the epoch earns half the weight of one supplying them for the whole epoch.

7.2 Eligibility rules

Event Effect on share-seconds
Deposit starts earning at the deposit block timestamp
Withdrawal request stops earning at the request timestamp for the requested shares
Withdrawal cancellation resumes earning from the cancellation timestamp; no retroactive credit
Withdrawal completion no effect (shares already stopped)
Funding arrives mid-epoch credited to the epoch in which it arrives; weighted over the whole epoch, so a deposit just before funding earns only the seconds after it
Epoch with zero total share-seconds F_e rolls into F_{e+1}; it is never assigned to a later single depositor at the instant of arrival
Funding after epoch end but before finalization belongs to the current epoch, not the ended one

7.3 Rounding

Integer arithmetic with PRECISION = 1e27, rounding down per user. Dust from rounding accumulates in unallocated and rolls into the next epoch. A user's lifetime claim is monotonic.

7.4 Bounded-gas implementation

No function loops over depositors. Per user, keep (shares, accruedShareSeconds, lastTouch, epochCursor). Per epoch, keep (start, end, funded, totalShareSeconds, finalized). Globally keep (totalShares, totalShareSecondsAccrued, lastTouch).

  • On any share change or claim, touch(user) first settles the user's share-seconds up to now, walking at most epochsSinceCursor epoch boundaries. Each step is O(1). A cap of k epochs per call (recommended 52) bounds gas; the user calls again to continue.
  • finalizeEpoch(e) requires now >= end_e, settles global share-seconds to end_e, and freezes totalShareSeconds(e). O(1).
  • claim(user) sums F_e × userShareSeconds(e) / totalShareSeconds(e) for finalized epochs up to the cursor. O(epochs claimed).
  • Checkpoint tests: a user with constant shares across N epochs claims the same as one who touches every block.

7.5 Interface labels

Three numbers, never one:

Label Meaning
Funded ETH ETH actually in the distributor for this epoch
Estimated allocation the user's current share-seconds fraction applied to funded ETH; changes until the epoch ends
Claimable ETH finalized, computed, withdrawable now

Any ETH that originates from the treasury or an incentive budget is labelled "incentive", never "bridge revenue". Fee revenue routed to rewards is labelled "fee revenue" with the source period.

8. Security boundary

  • Ethereum-side contracts are secured by Ethereum-side keys. A PQ-controlled Minerva account does not make the Sepolia Safe post-quantum. State this on the status page next to the Safe's threshold and owners. Migrate Ethereum-side admin to a timelocked Safe with published owners and a PQ-aware policy when Ethereum offers one; until then it is a classical boundary.
  • Guardians: threshold, membership, rotation procedure, and the key types used on each chain are published. Minerva-side guardian keys are ML-DSA; Ethereum-side guardian keys are ECDSA until Ethereum supports a PQ verifier precompile.
  • Relayers hold no authority that is not also verified on-chain.

9. Migration vs. bridge

The 1,000,000 MRV migration reserve is a separate contract with its own MIGRATION ledger, a one-way claim path, and an explicit proof-of-control rule for Ethereum MRV. It is never bridge liquidity. The supply statement lists: genesis allocations, migration reserve and claims to date, mined issuance, burned base fees, and bridge inventory per category per chain.

10. Required tests before mainnet

  • Invariant fuzzing of I1 to I7 across random sequences of deposit, lock, release, expire, refund, withdraw, cancel, and fund actions, including one-sided flows.
  • The double-payout scenario from F5 as a regression test that must fail to pay twice.
  • Clock-skew tests where the destination clock leads and lags the source by skewMargin.
  • Reward tests: half-epoch depositor earns half; deposit one second before funding earns one second; zero-liquidity epoch rolls forward; rounding dust is conserved.
  • Gas tests: touch, finalizeEpoch, and claim have constant upper bounds independent of the number of depositors.

11. Implementation status (2026-10-19)

Implemented in bridge/contracts (32 Foundry tests) and bridge/services/bridge-relayer:

Section Status
4.2 deadlines on both chains done: both vaults refuse releases after the deadline; markInboundExpired after deadline + expiryMargin (15 minutes default)
4.3 cancellation evidence done, design 1 with design 2's record: the destination records Expired on-chain, guardians attest cancelHash(messageHash, destinationChainId), the source cancels only with that attestation; no timer path remains (MinervaNativeBridgeVault.cancelOutbound, SepoliaMRVBridgeDepositAdapter.cancelDeposit)
4.4 execution confirmation done for the relayer (confirmSourceExecutions); the permissionless bounty variant is open
4.5 symmetric verification done: MRVBridgeLiquidityVault.releaseToBridgeRecipient(message, attestations) verifies message, guardians, deadline, and registry; configureBridge wires it
2 ledgers done: lpInventory / protocolInventory / pendingWithdrawalMRV / reservedMRV on Ethereum, lpNativeInventory / protocolNativeInventory / escrow on Minerva; releases draw protocol inventory first and report the split; settlement (confirmOutboundExecuted, confirmDepositExecuted) is guardian-attested over executedHash(messageHash, destinationChainId, lpPortion) and attributes the escrow the same way; settled deposits replenish vault inventory without minting shares (sweepSettledToVault, replenish)
3 invariants I1, I3, I4 and share conservation fuzzed by LedgerInvariants.t.sol (48 runs x 32 calls per handler, three handlers); I2 follows from I1 with checked arithmetic; I5 taken as burn-at-request (below); I6 awaits the migration contract; I7 holds by construction (rewards are a separate contract)
5 withdrawal queue done in the vault: a request burns shares and reserves the payout outside the inventories at the request-time price, withdrawalCovered and ledger views expose it; percentage caps (maxReleaseBpsOfAvailable, dailyReleaseBpsOfAvailable) added; the FIFO position view is UI work
6 loss allocation partly: protocol inventory absorbs utilisation before LP inventory on both chains; the written policy for contract-level losses remains to publish
7 rewards done: TimeWeightedETHRewardDistributor (share-seconds per epoch, rollover of empty epochs, bounded-gas lazy settlement, preview equals claim)
8 security boundary documented in bridge/docs/security-assumptions.md; Minerva-side guardians still verify ECDSA signatures in threshold mode
10 required tests double-payout regression, expiry exclusivity, guardian threshold and wrong-message rejection, escrow sweep, half-epoch and late-deposit rewards, rollover, preview-equals-claim; clock-skew and full invariant fuzzing are open

Deployment note: the Sepolia deployment recorded in bridge/deployments/sepolia.json predates these changes and must be redeployed (new vault release signature, registry vault role, adapter guardian wiring, new reward distributor). The Minerva-side contracts are installed at genesis by scripts/minerva/install-native-bridge-genesis.mjs, now defaulting to the minerva-devnet profile.