the polyleverage protocol architecture
Polylayer is a trading protocol and platform for crypto, real-world assets, and prediction markets. Polyleverage is the part of it that offers leverage. It is a native Solana program that lets a trader take a leveraged position on a market's price without the protocol ever holding more risk than the collateral the trader posted.
That last clause is the whole design. Most leverage venues are structured so that the protocol, or a shared liquidity pool, is the counterparty of last resort. When a position moves far enough and fast enough, the protocol absorbs the gap between the liquidation price and the price actually achieved, and a cascade of liquidations during a sharp move can drain a shared pool faster than keepers can close positions. Offering high leverage on that structure means underwriting a tail risk that is difficult to bound.
Polyleverage removes the shared pool. Every position is a contract between exactly two traders, each of whom has posted equal collateral, and the contract is fully collateralized for its entire life. This post describes how that works, how positions are matched and settled, how the design was extended from prediction markets to equities and commodities, and how the program is tested.
bounded loss by construction
A polyleverage position is called a PMLC, a pairwise matched leverage
contract. It has exactly two owners, a long and a short. At the
moment of matching, each side locks the same amount of collateral,
call it c. Nothing else is at stake. The long's maximum loss is
c; the short's maximum loss is c; and whatever one side loses,
the other side gains.
The settlement math enforces this directly. Each side's equity is
computed and then clamped to the range [0, 2c]:
equity_long = clamp(c + pnl_long, 0, 2c)
equity_short = 2c - equity_long
The clamp is the safety property. It holds three invariants no matter how far or how fast the price moves, and no matter the leverage:
┌────────────────────────────────────────────────────────────┐
│ 0 ≤ equity_long ≤ 2c │
│ 0 ≤ equity_short ≤ 2c │
│ equity_long + equity_short = 2c (always) │
│ │
│ most a trader can lose = c (their own collateral) │
│ most a trader can win = c (the counterparty's c) │
│ amount the protocol can be asked to cover = 0 │
└────────────────────────────────────────────────────────────┘
No value is created or destroyed at settlement, and there is no
amount the protocol can be asked to cover. Because the loss is
bounded by construction rather than by a liquidation engine winning a
race, leverage as high as 1000x is safe to offer. At 1000x a price
move of a tenth of a percent against a position consumes the entire
c, but that is the trader's c and nobody else's.
Leverage, then, is not a property of a margin account. It is a property baked into the contract at match time, and the contract is a closed system from that point forward.
the objects
Four kinds of account carry the protocol's state.
ProgramConfig (singleton)
│
├── InstrumentConfig ──── IntentBook (one order book)
│ │
│ └── PMLC, PMLC, PMLC, ... (matched positions)
│
└── MarginAccount (one per trader, per collateral mint)
A ProgramConfig is the singleton root. It records the admin authority, the public key the program accepts attestations from, and a global pause flag.
An InstrumentConfig describes one tradeable market at one leverage and one collateral size. It is keyed by a tuple: a data source byte, a 32-byte market identifier, the leverage, the collateral bucket, and a TWAP window. The program treats the market identifier as an opaque blob; what it identifies is the off-chain attestor's concern, not the program's.
A MarginAccount is one trader's collateral ledger for one collateral mint. It separates three balances, and value only ever moves between them, never in or out except by deposit and withdraw:
deposit withdraw
│ ▲
▼ │
┌───────────────────┐ PostIntent ┌────────────────┐
│ free │ ─────────────► │ reserved │
│ withdrawable, │ ◄───────────── │ committed to a │
│ usable for posts │ cancel/prune │ resting intent │
└───────────────────┘ └───────┬────────┘
▲ │ match
│ settle ▼
│ ┌────────────────┐
└────────────────────────────── │ locked │
│ committed to a │
│ live position │
└────────────────┘
A PMLC is a live position. Every field is fixed at match time except the status:
PMLC (one account per matched position)
┌──────────────────────────────────────────────────────┐
│ status LIVE → LIQUIDATED | RESOLVED | CLOSED │
│ long_owner pubkey │
│ short_owner pubkey │
│ entry_price midpoint of the crossing prices │
│ collateral c, locked from each side │
│ leverage fixed for the life of the contract │
│ size notional × 1e18 / entry │
└──────────────────────────────────────────────────────┘
intents and the order book
A trader does not take a position directly. A trader posts an
intent: a statement of the side they want, a limit price (the
worst entry they will accept: a ceiling for a long, a floor for a
short), a number of contracts, and an expiration slot. Posting an
intent moves contracts × collateral_bucket from the trader's free
balance to reserved. The collateral is committed but not yet at
risk.
Intents rest in an IntentBook, one expandable account per instrument. The book is a single contiguous account holding a header and a pool of fixed-size 96-byte nodes. A node is one of three things: an intent, a trader's seat, or a free-list slot. Intents are held in two red-black trees, one per side, sorted by price. The trees are keyed so that the most aggressive order, the highest bid or the lowest ask, is the leftmost node, so the matcher reaches the top of either side of the book in logarithmic time. This is a standard single-price limit order book.
IntentBook account
┌──────────────────────────────────────────────────────┐
│ header tree roots · free-list head · counters │
├──────────────────────────────────────────────────────┤
│ node 1 │ node 2 │ node 3 │ ........ │ node N │
│ each node, 96 B = intent | seat | free slot │
└──────────────────────────────────────────────────────┘
long tree ─┐ ┌─ short tree
▼ ▼
(red-black, price-sorted; best-priced order leftmost)
Keeping the book in one account, rather than one account per order, keeps matching within a single transaction's account budget and makes the book's rent a fixed, expandable cost rather than a per-order one.
matching
A long and a short cross when the long's limit price is at least the short's: the long will pay at least what the short will accept. The position's entry price is the midpoint of the two limit prices, so any surplus between the bid and the ask is split evenly.
long intent (bid) 0.61 willing to pay up to 0.61
short intent (ask) 0.60 willing to sell down to 0.60
────
cross: 0.61 >= 0.60
entry = midpoint = 0.605
When a pair is found, the match resolves into a PMLC in one transaction:
long intent short intent
│ │
│ reserved → locked │ reserved → locked
└─────────┬──────────┘
▼
┌────────────────┐
│ new PMLC │ entry = midpoint of the prices
│ status LIVE │ size = notional × 1e18 / entry
└────────────────┘ notional = collateral × leverage
Matching can happen two ways. A keeper can name an explicit pair, or anyone can call the permissionless matcher, which crosses the best bid against the best ask. Both paths run the same core logic, so the economics are identical regardless of who triggered the match.
A fee applies to the taker, the intent that arrived later. The fee is
volume-tiered: each trader's rolling 30-day volume is tracked in a
small per-trader account, and a fee schedule maps volume to a fee in
basis points. Posters reserve a fee buffer when they post; the buffer
is consumed if they end up the taker and released back to free if
they end up the maker.
settlement
A PMLC ends in one of three ways, and all three move locked collateral, so all three are gated by an attestation.
Liquidation closes a position whose price has moved far enough that one side's equity has reached the instrument's liquidation threshold. The price used is not the price at the moment the liquidation transaction lands. It is a historical mark: the price at the moment the position first became liquidatable, computed off-chain by the attestor against the market's price history. Settling at the historical mark removes a path dependency. If settlement used the current price, a position that breached the threshold and then recovered before a keeper got around to it would un-liquidate, and a slow keeper would change the outcome. Binding settlement to the historical breach mark makes the result independent of submission timing.
Resolution closes a position when the underlying market resolves to an outcome. For a prediction market this is the binary settlement: the market resolved yes, no, or fifty-fifty.
Mutual close closes a position by agreement.
In every case the program does not trust the caller for the price. It trusts an attestation: a fixed 104-byte message, signed by the key registered in ProgramConfig.
attestation message, 104 bytes, Ed25519-signed by the attestor
offset 0 4 5 8 40 48 56 104
┌─────┬────┬──────┬────────────┬───────┬─────────┬────────┐
│ATT1 │type│ resv │ market_id │ ts │ nonce │payload │
│ 4 B │1 B │ 3 B │ 32 B │ 8 B │ 8 B │ 48 B │
└─────┴────┴──────┴────────────┴───────┴─────────┴────────┘
a liquidation payload additionally binds one PMLC by pubkey, so the
same signed price cannot be replayed against a different position
The signature is checked in an unusual but deliberate way. Solana exposes an Ed25519 signature-verification primitive as a precompiled program. The caller places an Ed25519-verify instruction immediately before the settlement instruction in the same transaction. The settlement instruction then reads the preceding instruction back out of the instructions sysvar, confirms it targeted the Ed25519 precompile, and confirms the public key it verified against is the registered attestor and the message is the attestation being acted on.
settlement transaction
┌─────────────────────────────────────────────────────┐
│ ix[0] Ed25519 precompile │
│ verifies sig over the 104-byte attestation │
├─────────────────────────────────────────────────────┤
│ ix[1] Liquidate / Resolve │
│ reads ix[0] back from the instructions │
│ sysvar, and confirms: │
│ program == Ed25519 precompile │
│ signer == registered attestor │
│ message == this attestation │
└─────────────────────────────────────────────────────┘
Freshness and replay are handled by a per-market nonce account. Each market keeps the last accepted attestation nonce; an attestation must carry a strictly greater nonce, and a timestamp within a staleness bound. An old signed attestation cannot be replayed, and an attestation for one market cannot be used on another.
The attestor's signing key is itself rotatable, behind a timelock. Changing it is a two-step governance action: a proposal, then an execution that is rejected until a fixed delay has elapsed, with a cancellation path in between. The delay gives the system's users a window to observe a pending change to the most security-sensitive parameter in the program.
exiting a position
A PMLC has no fixed expiry. Once matched it stays live until it settles. The full lifecycle of a position looks like this:
long intent short intent
└──── match ──────┘
│ prices cross
▼
┌──────────────┐ Novate / Substitute
│ PMLC │ ◄──── owners change,
│ status LIVE │ the PMLC stays LIVE
└──────┬───────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
Liquidate Resolve CloseMutual
│ │ │
▼ ▼ ▼
LIQUIDATED RESOLVED CLOSED
└────────────┼────────────┘
▼
ClosePmlc
(account closed,
rent refunded)
The settlement paths above are one way a position ends. But a trader rarely wants to wait for the underlying market to resolve. Three mechanisms let an owner leave a live position early.
The simplest is mutual close: both owners of a PMLC agree to tear it down, and each takes back collateral adjusted by the position's current mark.
Novation transfers one side of a PMLC to a named new owner. The exiting owner's collateral unlocks, the new owner's collateral locks in, and the new owner inherits the position at its original entry price. Novation needs a specific counterparty who has agreed to take the position over.
The most important of the three, because it does not need a named counterparty, is substitution. It is how a trader exits into the open market, and it reuses machinery the trader already understands. To leave a long position, the owner does exactly what they would do to open a short from scratch: they post a short intent. The intent rests in the same order book as every other intent. When the matcher finds a new trader whose long intent crosses it, the substitution instruction fires. The crucial point is that it does not open a second PMLC. It swaps the exiting owner out of their existing PMLC and swaps the new trader in.
before: PMLC = ( long: Alice , short: Bob )
Alice posts a short intent ─┐
├── matched in the order book
Carol posts a long intent ─┘
│
▼
after: PMLC = ( long: Carol , short: Bob )
Alice's collateral c unlocks → free
Carol's collateral c locks in ← reserved
Bob, the untouched side, is not affected
Substitution can settle the exiting owner's profit on-chain. The
position has an entry price e1; the substituting match has its own
midpoint e2. In the settling variant, the new trader pays the
exiting owner a premium equal to the owner's mark-to-market profit
between e1 and e2. The exiting owner walks away with their
collateral plus realized profit, or minus realized loss; the new
trader holds a position whose on-chain entry is still e1 but whose
economic entry, after the premium, is e2. The counterparty on the
other side of the PMLC is not touched: only owners change, and the
bilateral collateral sum is preserved throughout.
Because an exit is just an ordinary intent in the same book as an entry, exiting and entering are the same action viewed from two sides, and a position can be rolled rather than simply unwound. An intent can additionally carry a reentry flag, which is recorded on the PMLC it matches into; when that PMLC later closes, the flagged side can re-post an intent at the saved price without a fresh signed action, so a closing position can roll directly into the next one.
from prediction markets to equities and commodities
Polyleverage began on prediction markets, where a price is a
probability and lives in the open interval (0, 1). Extending it to
equities, commodities, and crypto majors meant settling positions on
assets quoted in dollars: a share at two hundred dollars, gold at
several thousand, bitcoin in the tens of thousands.
The program's price math turned out to be ready for this in one
respect and not in another. It was ready because the math is
ratio-based. A position's profit is notional × (mark - entry) / entry, which is notional times a return. Multiplying every price
in an instrument by a constant leaves the return unchanged. So a
dollar price can be normalized into the program's (0, 1)
fixed-point by dividing by a per-instrument reference ceiling, and
the normalization is economically exact: it scales out of every
number that matters.
Pyth feed BTC/USD = $66,849.09 (raw 6_684_909_218_226, expo -8)
per-instrument reference ceiling $1,000,000
price_fp = price / reference × 1e18
= $66,849.09 / $1,000,000 × 1e18
= 66,849,092,182,260,000
┌────────────────────────────────────────────────────────────────┐
│ 0 ····························· price_fp ··············· 1e18 │
│ ▲ │
│ ≈ 0.0668, well inside (0, 1) │
└────────────────────────────────────────────────────────────────┘
It was not ready in another respect. The program validated that every
price was strictly inside (0, 1), and it gated instrument creation
on two hardcoded allowlists, one of accepted data sources and one of
accepted leverage values. A dollar price normalizes cleanly into
(0, 1), so the price bound was satisfied by the normalization
convention with no code change. The allowlists were not. Admitting a
Pyth-sourced instrument, and admitting the higher leverage buckets,
required extending both lists. That was the on-chain change the
multi-asset work needed, and it was a small one.
Prices for the non-prediction-market assets come from Pyth. The attestor sources a price from Pyth, normalizes it against the instrument's reference ceiling, and signs the same attestation format the program already understood. The on-chain verifier did not change. The launch asset set is three crypto majors, two metals, and eleven tokenized equities, the xStocks issued by Backed Finance, each carried by Pyth as a continuously-priced feed.
testing a program that moves money
A program that custodies collateral and moves it on settlement has to be tested to a higher standard than an ordinary application. A wrong state transition is not a bug report; it is a loss of funds. The question is how to exercise the real program, not a model of it, against the full range of inputs an adversary would try.
The approach taken is to run the real compiled program in an in-process virtual machine. The program is built to SBF bytecode with the same toolchain that produces a deployable artifact. That bytecode is loaded into litesvm, a Solana virtual machine that runs inside the test process with no validator daemon and no network. Every test sends real transactions: real instruction encoding, real program derived addresses, real cross-program invocations into the SPL token program and the Ed25519 precompile, and real compute-unit metering.
The choice of an in-process VM over a local validator is deliberate. A validator advances its clock in real time, which makes the governance timelock, a delay measured in hours, impractical to exercise in a test. litesvm allows the clock to be set directly, so a timelock is warped forward instantly and deterministically. A full end-to-end test completes in milliseconds.
What the harness simulates, by design, is the off-chain operators. The attestor, in production a key held inside a trusted execution environment, is represented by a local Ed25519 keypair. This is a genuine simulation rather than a mock: the keypair produces real Ed25519 signatures over the exact attestation byte layout, and the on-chain verifier checks them for real. A forged attestation fails because the signature genuinely does not verify against the registered key, not because a mock returned false. Oracle prices come from a small Pyth feeder. The program, the VM, and every transaction are real; only the actors around the program stand in for their production counterparts.
the harness
The test harness, polyleverage-simulator, is a small Rust crate.
Underneath the tests sits the real program, not a model of it:
polyleverage-simulator
┌───────────────────────────────────────────────────────────┐
│ tests/ end-to-end · adversarial · perf · multi-asset │
├───────────────────────────────────────────────────────────┤
│ driver · attestor · scenario · pricing │
├───────────────────────────────────────────────────────────┤
│ litesvm in-process Solana VM, no validator, │
│ no network, clock set directly │
├───────────────────────────────────────────────────────────┤
│ polyleverage.so real SBF bytecode (cargo build-sbf) │
└───────────────────────────────────────────────────────────┘
The driver loads the program, derives addresses, funds accounts, frames instructions, and submits transactions. It exposes one typed helper per instruction, so a test reads as a sequence of intent ("post a long, match it, liquidate it") rather than as account-list plumbing.
The attestor module is the simulated signer. It frames and signs the attestation types and builds the Ed25519 precompile instruction, using the program crate's own layout constants so the harness cannot silently drift from the on-chain wire format.
The scenario builder assembles the substrate every settlement test needs: a configured program, a fee schedule, a collateral mint and vault, an instrument, and two funded traders. It can drive a long and a short into a matched position, so each test writes only the part that is actually under test.
The pricing module performs the off-chain price normalization, the same integer formula the Pyth feeder applies, so the multi-asset tests can open positions at a normalized real price.
what the suite covers
The suite is thirty-six test functions in four layers.
┌─────────────┬──────────────────────────────────────────────┐
│ end-to-end │ deposit · withdraw · create instrument · │
│ │ post · match · liquidate · resolve · close · │
│ │ novate · substitute · timelock · pause │
├─────────────┼──────────────────────────────────────────────┤
│ adversarial │ forged signer · wrong attestation type · │
│ │ wrong PMLC · replayed nonce · wrong market · │
│ │ missing attestation · malformed intents │
├─────────────┼──────────────────────────────────────────────┤
│ performance │ every instruction metered, hard CU ceiling │
├─────────────┼──────────────────────────────────────────────┤
│ multi-asset │ leverage / bucket extremes · normalized │
│ │ real Pyth price through a full lifecycle │
└─────────────┴──────────────────────────────────────────────┘
The end-to-end layer walks the protocol's lifecycle. Each flow is checked on its success path and on at least one rejection path.
The adversarial layer encodes the threat model directly. Settlement is the trust boundary, so it receives the most attention. A separate test confirms the rejection of each forgery vector: an attestation signed by an unregistered key, an attestation of the wrong type, an attestation bound to a different position, a replayed nonce, an attestation for the wrong market, and a settlement transaction carrying no attestation at all. The permissionless intent surface is covered the same way: zero-contract intents, invalid side bytes, expired intents, out-of-range prices, posting without collateral, and matching two same-side intents. Each adversarial test differs from a known-good call in exactly one way, so a rejection isolates the vector under test rather than passing for an unrelated reason.
The performance layer meters every instruction for compute units and asserts a hard ceiling, so a compute regression fails the suite.
The multi-asset layer drives the program at the leverage and margin bucket extremes and runs a full lifecycle on a normalized real Pyth price.
performance
Every instruction sits well within Solana's per-instruction compute budget:
instruction compute units
──────────────────────────────────────────────────
ClosePmlc 1,356
PostIntent (long / short) 5,071 / 6,774
Novate 9,633
Resolve (incl. attestation verify) 11,360
Withdraw 15,408
Deposit 15,553
Liquidate (incl. attestation verify) 16,302
MatchPair 37,623 ◄ hot path
──────────────────────────────────────────────────
Solana per-instruction limit 200,000
The heaviest is matching, because a match does the most work in one transaction: it resolves both intents, runs the matching math, lazily creates the fee and volume accounts on first use, and allocates the position account. Everything else is well under seventeen thousand. The protocol has substantial headroom.
orderbook performance
The order book is the one part of the protocol whose cost depends on its size, so it is worth characterizing precisely rather than asserting. Two kinds of operation run against it.
Match discovery, finding the best crossing pair, is O(log n). The
two trees are keyed so that the highest bid and the lowest ask are
each the leftmost node of their side; the permissionless matcher
reaches both in logarithmic time and crosses them if the bid is at
least the ask. If those two do not cross, no pair does.
Identity lookups, finding the node holding a given intent id, and the
opportunistic prune that PostIntent performs, scan the node pool
linearly. MatchPair resolves two intents by id, so it carries two
such scans.
A linear scan invites concern, so it was measured rather than
assumed. The scaling benchmark grows a book to a target node capacity
and meters PostIntent and MatchPair against it. The scan visits
every slot in the pool, so the cost tracks the book's provisioned
capacity rather than how many intents are currently resting.
book capacity PostIntent CU MatchPair CU
────────────────────────────────────────────
16 5,580 33,271
64 6,209 39,955
256 11,705 42,259
1,024 20,189 48,475
4,096 58,625 91,339
8,192 111,873 150,991
────────────────────────────────────────────
MatchPair ~ 33,000 CU + 13 CU per node
Both grow linearly, at roughly thirteen compute units per node, and
the linear fit gives the ceilings directly. MatchPair stays within
Solana's 200,000 CU default per-instruction limit up to a book of
about 12,000 node slots. Raising the transaction's compute budget to
the 1,400,000 CU maximum, which costs a single ComputeBudget
instruction, extends that to about 95,000 slots. That is also near
the point at which the book account reaches Solana's 10 MiB size cap,
so the compute ceiling and the storage ceiling roughly coincide.
In practice this is substantial headroom. The protocol shards by instrument: every (asset, leverage, bucket) is its own book account, processed independently. A single instrument would need on the order of 12,000 intents resting at once before matching even required a raised compute budget, which is far above realistic depth for one per-bucket market. Throughput across the protocol scales with the number of instruments, and that axis of scale costs nothing.
One structural constraint shapes how a book is provisioned. Solana
caps the growth of an account's data at about 10 KiB per transaction,
so a book cannot be created deep in a single instruction. It is
created small and grown with repeated ExpandIntentBook calls, each
adding about a hundred slots. Provisioning a deep market is therefore
a one-time sequence of expansion transactions.
Should a single instrument ever need a book beyond these bounds, the
linear scans are removable without touching the trees. A caller that
has read the book already knows each intent's node index, so it can
pass the index and let the program validate it against the intent id,
turning the O(n) lookup into an O(1) array access and flattening
MatchPair to its fixed base. The measurements indicate that change
is not needed at any depth the protocol will realistically reach.
the whole picture
Flattened into one diagram, a position's life runs from two traders' deposits to a settled, closed account:
Trader A Trader B
│ deposit │ deposit
▼ ▼
MarginAccount A MarginAccount B
│ PostIntent (long) │ PostIntent (short)
│ free → reserved │ free → reserved
▼ ▼
┌────────────────────────────────────────────────────────┐
│ IntentBook (per instrument) │
│ long tree ◄────── matcher ──────► short tree │
└───────────────────────────┬────────────────────────────┘
│ prices cross
│ both sides: reserved → locked
▼
┌──────────────────┐
│ PMLC │ entry = midpoint of prices
│ long:A short:B │ each side locks c
│ status LIVE │
└────────┬─────────┘
│
┌──────────────────┼───────────────────┐
▼ ▼ ▼
attestation attestation opposite intent
(breach mark) (resolution) matched in book
│ │ │
▼ ▼ ▼
LIQUIDATED RESOLVED owners swapped
│ │ (PMLC stays LIVE)
└──── equity clamped to [0, 2c] ────┘
locked → free, settled
The attestor sources prices, normalizes them, and signs; the
on-chain program verifies the signature and moves locked collateral
within the [0, 2c] bound. No price move, at any leverage, asks the
protocol to cover a gap.