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 (
MRVBridgeLiquidityVaultsuccessor): holds ERC-20 MRV. - Minerva vault (
MinervaNativeBridgeVaultsuccessor): 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:
balance(vault) == LP + ESCROW + MIGRATION + RESERVED_WITHDRAWAL + RESERVED_RELEASE + PROTOCOLon each chain, checked by an invariant test on every state change.available = LP + PROTOCOL - RESERVED_WITHDRAWAL - RESERVED_RELEASE. Onlyavailablemay be released to a bridge recipient.ESCROWandMIGRATIONare never release capacity.- A release on chain X moves
amountfromavailable_Xto the recipient and, on finalization of the matching lock on chain Y, moves the lock'sESCROW_YintoLP_Y(pro rata to shares) orPROTOCOL_Y. This is the step that gives LPs a claim on the inventory that accumulates on the other chain. Today nothing performs it;confirmOutboundExecutedhas no callers. - 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:
MIGRATIONonly 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 + finalityMarginand one of the two conditions in 4.3 holds. skewMargincovers clock drift between chains (recommended 15 minutes).finalityMargincovers 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:
- 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 toCancelled, thenRefunded. - 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
LPtoRESERVED_WITHDRAWAL, which reducesavailableimmediately (as today). - The queue head is visible: position, amount, earliest completion, and whether
availableon that chain currently covers everything ahead of it. - If
LP_Xis 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% ofavailableper 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:
ESCROWis made whole first fromPROTOCOL, then from a published insurance reserve.MIGRATIONis never used to cover losses.- Remaining loss is borne by
LPpro rata to shares on the affected chain. REWARDSare 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 tonow, walking at mostepochsSinceCursorepoch boundaries. Each step is O(1). A cap ofkepochs per call (recommended 52) bounds gas; the user calls again to continue. finalizeEpoch(e)requiresnow >= end_e, settles global share-seconds toend_e, and freezestotalShareSeconds(e). O(1).claim(user)sumsF_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, andclaimhave 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.
