Virtualized Funding: Gas-Free Settlement at Any Scale

How Perpl settles funding payments for 100,000+ positions with two storage reads instead of 100,000 writes.

Perpetual exchanges use a funding rate to keep the perpetual price anchored to the underlying asset. Periodically — roughly every hour — longs pay shorts (or vice versa) based on the rate and position size. On a centralized exchange this is trivial: a background job debits and credits every account. On a decentralized exchange running on a general-purpose EVM chain, it's a fundamental scaling problem.


The Settlement Problem

With 100,000 open positions, paying out funding every hour requires 100,000 on-chain state writes per funding event. Each write to a position's storage slot costs at least 5,000 gas. The arithmetic is unforgiving: 100,000 positions × 5,000 gas = 500 million gas — roughly 16 full blocks — just to settle one hour of funding. At Monad's block rate of ~2.5 blocks per second, that's six seconds of block space consumed every hour, purely by bookkeeping.

In practice it's worse. Each position update involves reads, balance changes, and event emissions. The real cost is closer to 25,000 gas per position. At 100,000 positions that's 2.5 billion gas per event — 83 blocks. This isn't a scalability inconvenience; it's a protocol design constraint that has shaped every decentralized perp that exists.

CEX
✓ Works
Background job, no gas
Naive DEX
✗ Fails
Loop N positions per event
Appchain
⚠ Costly
Custom opcodes, dedicated chain
Perpl
✓ Works
Virtualized, 2 reads at close

The Insight: Superposition

The key observation is that a position's cumulative funding payment depends only on three things: its lot size, when it was opened, and when it was closed. The lot size and side are fixed between open and close (the contract forces settlement if they change). This means the per-event payment for any position factors cleanly:

Fpayment[j] = Fproduct[j] × L where Fproduct[j] = Pi[j] × Frate[j] — the funding price times the funding rate at event j

The funding product Fproduct[j] is entirely market-level data — it doesn't depend on any individual position. So instead of writing to every position at each event, you can store a single running sum: the funding product sum, Fsum.

Fsum[j] = Σ Fproduct[k] for all k ≤ j One value, updated once per funding event, regardless of how many positions are open
Figure 4: Formula Derivation
Funding payment per event
Fpayment[j] = Pi[j] × Frate[j] × L
Pi[j] = spot index price at event j
Frate[j] = funding rate (10⁻⁶ units)
L = signed lot size (+long, −short)
Define the funding product (market-level)
Fproduct[j] = Pi[j] × Frate[j]
Independent of any individual position.
One value computed per event.
Rewrite payment using product
Fpayment[j] = Fproduct[j] × L
Payment factored into market term × position term.
Define the running sum
Fsum[j] = Σ Fproduct[k], k = 0..j
Cumulative product sum, updated once per event.
Initialized to 0 at contract creation.
Recover any single event from sum
Fproduct[j] = Fsum[j] − Fsum[j−k]
Difference of adjacent sums = one event's product.
Generalize to any holding period
θpnl = (Fsum[j] − Fsum[m]) × L
m = Fsum stored at position open
j = Fsum read at position close
Two reads. Exact. Any duration.
Figure 2: Fsum Accumulation — 8 Funding Events
Event Block Frate (10⁻⁶) Price (USDC) Fproduct = P × rate / 10⁶ Fsum (running) Note
00.000contract genesis
FE18,571+5060,000+3.0003.000
FE217,142+8061,000+4.8807.880← position opens here (stores 7.880)
FE325,713−3060,500−1.8156.065
FE434,284+12062,000+7.44013.505
FE542,855+6063,000+3.78017.285
FE651,426−2062,000−1.24016.045← position closes here
FE759,997+9064,000+5.76021.805
FE868,568+4063,500+2.54024.345
Example: Long 10 lots opened at FE2 (Fsum[m] = 7.880), closed at FE6 (Fsum[j] = 16.045).
θ_pnl = (16.045 − 7.880) × 10 × (−1) = −81.65 USDC (long pays funding — 4 positive rate events minus 1 negative, net positive).
Computed from 2 reads: Fsum[m] stored at open, Fsum[j] read at close.

Two Reads, Any History

When a position opens, the contract records the current Fsum as the position's funding checkpoint. Call it Fsum[m]. When the position closes, the contract reads the current Fsum[j]. The cumulative funding payment — the position's premium PNL — is:

θpnl = (Fsum[j] − Fsum[m]) × L Two reads. Any number of funding events in between. Exact to the satoshi.

It doesn't matter if the position was held through 1 funding event or 1,000. The cumulative effect telescopes into the difference between two stored sums. No loop, no iteration, no per-position writes at event time.

What changes at each funding event: one write — update Fsum.
What happens when a position closes: two reads — fetch Fsum[j] and the stored Fsum[m]. Compute. Done.
Figure 1: Position Lifecycle with Virtual Funding
OPEN POSITION
Store Fsum[m]
as funding checkpoint
1 read + 1 write
(the checkpoint)
FUNDING EVENTS
Fsum[j] += Fproduct[j]
for each event j
Position NOT touched ✓
1 write per event
regardless of positions
SIZE CHANGE
Forces settlement:
θ_pnl realized now
New checkpoint stored
2 reads → compute
reset checkpoint
CLOSE POSITION
θ_pnl = (Fsum[j] −
Fsum[m]) × L
Final settlement
2 reads → exact PNL
any # of events
Key invariant: The contract forces premium PNL realization whenever lot size or side changes. This keeps the checkpoint valid: the stored Fsum[m] always corresponds to the current position parameters. Dormant positions are never touched between open and close — their funding accumulates silently in Fsum.
▶ Try the interactive simulation

Gas Cost: O(1) Per Event

For a naive DEX, settlement gas scales linearly with position count: double the traders, double the gas. Perpl's gas cost for a funding event is constant regardless of how many positions are open. A single storage write to update Fsum, plus whatever overhead the permissioned rate-setter transaction requires. The protocol never loops over positions.

The settlement cost is deferred and amortized. Each position pays its cumulative funding when it changes or closes — folded into the settlement transaction the trader was going to execute anyway. The chain never does work for dormant positions.

Figure 3: Gas Cost vs Position Count (per funding event)
Gas required per funding event settlement █ = Block gas limit (30M)
At 1,000 positions
Naive DEX
25M gas
Perpl ✓
~5k gas — O(1)
At 5,000 positions
Naive DEX
125M gas (4× limit)
Perpl ✓
~5k gas — O(1)
At 100,000 positions
Naive DEX
2.5B gas (83× limit)
Perpl ✓
~5k gas — O(1)
Assumptions: 25,000 gas per position update (SSTORE warm: 5k + reads + events + arithmetic). Block gas limit: 30M. Naive approach: one SSTORE per position per event. Perpl: one SSTORE for Fsum, regardless of position count.

Forced Settlement on Position Change

The contract enforces a constraint: any change to a position's lot size or side triggers immediate premium PNL realization. This is what makes the virtualization safe. If a trader could silently grow or shrink their position without settling, the Fsum checkpoint would become invalid. The forced settlement means the checkpoint is always correct relative to the current position parameters.

Figure 5: On-Chain Storage Layout
Market state (one per perp)
fSumint128updated each funding event
fRateint32current rate (10⁻⁶)
fPriceuint64index price at last event
fundingBlockuint64block of last event
Position record (one per open position)
fSumCheckpointint128Fsum[m] stored at open
lotSizeint64signed (+ long, − short)
entryPriceuint64for delta PNL
deposituint64collateral locked
Settlement on close reads market.fSum and position.fSumCheckpoint — two reads, constant cost. The funding event transaction writes only market.fSum — one write, constant cost.

Figure 6: Settlement System Comparison
Property CEX Naive DEX Appchain (Hyperliquid) ✓ Perpl (Virtualized)
Gas per funding eventNoneO(N) positionsN/A — custom opcodeO(1) — one write
Scales to 100k positions✗ (83 blocks at 100k)
Trustless / on-chainPartially✓ Fully
Requires dedicated chainN/A✓ Required
Settlement cost per positionServer time~25k gas/event~0 (absorbed by chain)~0 deferred to close
Reads at position closeN/AN/AN/A2 reads (exact)
Dormant positions touchedEvery eventEvery eventEvery event (cheap)Never
Multi-event historyServer sumsRequires loopCustom accumulatorTelescopes into 2 reads
▶ Try the interactive simulation