Jun 22, 2026

Auto reverse-engineering the Hyperliquid risk engine, with some agentic help

Perps allow traders to leverage beyond their collateral, until the market turns abruptly and losses are clawed back. We auto-reverse engineer Hyperliquid's risk engine to show how it ranks and deleverages winning users under the solvency–fairness–revenue trilemma.

Heading image of Auto reverse-engineering the Hyperliquid risk engine, with some agentic help

Perpetuals

A perpetual is a bet on a price, usually leveraged, settled in stablecoins, with no expiry: a row in the exchange's ledger, backed by margin (a fraction of the position's notional), where one side's gains are the other side's losses on the same book.

What does that even have to do with reverse engineering?

Hyperliquid

Hyperliquid is a decentralized perpetuals exchange. It runs on its own L1, released publicly for anybody to run, but closed source. The order book and the risk engine live on-chain, and thus inside of the L1 node binary, sitting between users and the shared balance sheet they're all betting against.

The node is tasked with a very hard problem: keeping the books balanced when leverage makes it possible for one side of every trade to lose more than their margin covers.

Implementing a correct algorithm that solves this question is exactly what keeps the exchange solvent: this is also why peering into the actual implementation is very intriguing from a security standpoint.

Beyond that, the hl-node binary also offers an additional technical thrill: it is written in Rust, which is notoriously hard to reverse engineer.

Armed with some curiosity, a good amount of inference, and the right tooling, we'll take a deep dive using an interactive interface, which will hopefully make the reconstructed code easier to understand for anybody, while also giving reverse engineers something to learn and a fun tool to play around with.

The Million Dollar Question

Let's say a terrible trader opens a 50x leveraged position on Hyperliquid. How does the exchange avoid losing money from their eventual downfall?

When the market turns against them, the first check they would encounter is the traditional liquidation: their position is automatically put up on the market on the opposite side of the book. This happens whenever the equity of the position drops below the maintenance margin, which is spelled out exactly in the compute_margin_requirement_by_mode function.

Let's consider the simplest case, that being isolated positions, which are processed in this branch:

Tip

To get a clearer view of the logic of reconstructed snippets, click on them to click through each action performed by our Rust reverse engineering agent. Return to the regular view by clicking T.

Loading interactive snippet…

The view here displays the reconstructed source code, through various stages starting from raw pseudo-C, with several verified agentic "actions" applied to it by our Rust decompiler. For example, you can see how some tentative Rust types, like OptionUnitQty, are recovered: based on our harness, we can be sure that those are compatible with the original source types.

As you can see, the position's equity is its isolated margin plus its own PnL, and the threshold reduces to:

What If Nobody Wants to Buy the Position?

The market order from the previous section is just a regular order, and it still needs somebody on the other side of the book to fill it. If the book is thin or the move was violent enough that every passive bid got swept, only part of it (or none) gets filled and equity will possibly keep on going down.

When that happens, the exchange is basically left with a hot potato to get rid of: what should it even do with the debt?

The Risk Engine

And now we circle back to the risk engine: in order to keep all of these terrible traders from driving the system into ruin, we need some automatic way to prevent and clear bad debt.

Clearing Up Bad Debt

The Hyperliquid docs explain that positions below 2/3 maintenance margin are backstop-liquidated into the HLP liquidator vault. When even that can't keep the book solvent, auto-deleveraging closes the position against opposing traders. What does that even mean?

To get a better idea of what's going on here, we turn to the binary itself and trace the actual logic. A simple testnet sync gives us a lot of context: within the node's ABCI state, we can see a pretty complex structure named Clearinghouse, a struct containing all liquidation-related info, nested within a bigger Exchange data structure. This is an example of what it might hold when serialized as MessagePack at runtime.

This data will come in handy both for us and the agent.

clearinghouse.json
JSON

Now, when does the risk engine actually check positions? We can answer that question by playing around with xrefs starting from the known field strings in Clearinghouse. Some renaming later, we can infer something like this:

Loading interactive snippet…

We have one huge function handling all clearinghouse operations, which we call clearinghouse_adl_orchestrator. It is invoked at the end of each block by a top-level method (called indirectly elsewhere, most likely a trait method) that we call exchange_end_block. So, at the end of each block we run some checks.

Tip

Click on any address or line on the left side to get a detailed view of the decompilation or reconstructed source.

ADL is meant as an emergency measure; unlike liquidations, it is not always operational. Note the branch on the recovered clearinghouse.perform_auto_deleveraging flag.

Loading interactive snippet…

Most importantly, for it to act it must actually be needed. What does that mean according to the Clearinghouse?

Threshold Condition

The natural connection between liquidations and ADL is shortfall: by how much a position, or the entire system (!), is underwater.

If we could liquidate every position in time against willing buyers of the debt, there would be no systemic shortfall; ADL is triggered whenever that's not the case.

Who Is Underwater?

In order to determine that shortfall (or sum of bad debt), we process a useful tree of losing positions, built at the end of each block and passed to the clearinghouse_adl_orchestrator, starting from all user positions, which are stored in clearinghouse.user_states, as we can also see from the serialization.

Tip

A graph can also branch, you know? Click around to view all possible paths from one function to another.

Loading interactive snippet…

The adl_init_user_position_iterators essentially turns that into an Iter, then, inside of build_adl_candidate_set each entry is accounted either as a cross or isolated position, depending on the value of the AdlIterEntry field.

Loading interactive snippet…

Note that in storage, Isolated positions have their own independent Entry in the position iterators, and Cross positions have a single common Entry, which will be spread across several perps. We will see many more branches to handle the two kinds of positions.

Loading interactive snippet…

Ultimately, we get an iterator of underwater users filtered with the following code path, meaning they couldn't be liquidated by the end of the block according to the earlier threshold.

Loading interactive snippet…

After iterating over users, if the total losses are less than a predefined constant (hardcoded to $5M in the testnet state), we spare cross-margin positions and log Not performing auto-deleveraging because shortfall={} is acceptable.

Note that this insurance fund is not applied to every asset: while this isn't documented anywhere, markets referred to as only_isolated (or strict_isolated in MessagePack dumps) are added to a separate queue, deferred_queue, which triggers ADL regardless of the system's total_shortfall. This is dictated by this branch.

Loading interactive snippet…

Very interestingly, some of the assets that have this flag, at least on testnet, include HYPE (and other relevant tokens like ZRO, as well as JELLYJELLY from the March 2025 incident). Historical metadata for Hyperliquid is very hard to come by, so take this with a pinch of salt when thinking about mainnet.

How Do We Get Rid of Debt?

Now we know whenever we have shortfall. What is to be done in that case? This is where different risk engines make different design choices: Hyperliquid chooses to apply a queue-based ADL (auto-deleveraging), meaning they forcefully close some winning positions in order to clear the debt of the losing traders.

Thus, if the debt is insurmountable, the deferred_queue is overwritten by all the users marked by build_adl_candidate_set, otherwise we keep it and only holders of underwater strict_isolated positions from the earlier loop are considered for ADL.

Tip

Note how verbose a simple key lookup can be when compiled in Rust.

Loading interactive snippet…

For each underwater position, we do a B-tree lookup on clearinghouse.user_states[position.user], adding them to a Vec of users (you would be amazed at how long the compilation of such a simple statement is). Also note the branch below, again on cross and isolated margin positions:

Loading interactive snippet…

The logic for cross margin positions is more complicated, because we're essentially asking the question of how to split a bankrupt user's total shortfall across their individual positions, so that we can absorb the right proportion of each position, which logically should be:

We do that here:

Loading interactive snippet…

The inner loop above spans all (asset_idx, direction) held in the cross margin position.

For isolated positions the computation is trivial, and we directly write the single position, with its shortfall, to the adl_output b-tree, which is the output of this transformation for both isolated and cross margin positions, containing both position_id and position_shortfall.

Loading interactive snippet…

Afterwards we can finally loop over the B-tree containing (position_id, cut), which essentially tells us exactly how much of each position needs to be closed, to be later deleveraged from a winning position.

The final phase of clearinghouse_adl_orchestrator iterates it, and for each key it builds a counterparty array, initially including all users holding positions, sorting them based on a per-asset ADL ranking score.

Loading interactive snippet…

How are positions chosen (sorted) to be deleveraged?

Who Do We Deleverage?

This question is essential to the solvency and fairness of a perp platform. It is clear why solvency is a priority here.

But what do we mean by fairness? Colloquially, we can say that the relative wealth of accounts should not be affected by deleveraging.

More formally, fairness can be given multiple related definitions; see this paper. We want to prove that the algorithm used in HL is not axiomatically fair in its implementation (as defined in prop. 6.1 of the paper).

So, is Hyperliquid fair? Is it always solvent?

ADL Score Computation

The score function at compute_adl_ranking_score is core to answering this question. It's defined, most likely as an Ord implementation on some 20-byte address representation. A Vec of those holds the possible counterparties to fill against, and the trait is used by the sort routines that order them by ranking score for each underwater position.

Those sort routines are generated by the compiler: since Rust binaries include a lot of metadata by default in .comment, we may even manually take advantage of this to recover the exact source of library functions.

Let's see how this ties back into the clearinghouse_adl_orchestrator:

Loading interactive snippet…

And here's the main call site used for sorting:

Loading interactive snippet…

Ratio 1: Effective Leverage

We can see in the decompilation below, through the SIMD noise, that abs_notional (unsigned), and float.d(rbx) is account_value (signed); the division is abs_notional / account_value.

This tells us the risk multiplier that the user took on for this position.

Loading interactive snippet…

Ratio 2: Profit Ratio

It uses entry notional (r14_2) as the divisor. PNL is clamped to non-negative before division.

This basically tells us how well the user's bet went.

Loading interactive snippet…

There is clamping to avoid rounding issues. Both ratios are clamped to a minimum of 1e-8, in order not to cancel each other out. The final ADL ranking score is effective_leverage * profit_ratio, consistent with both ratio directions and the max clamp on PNL.

So, in essence, we look for risky and profitable positions!

Partial and Total ADL

Finally, note that deleveraging can also be partial, but even then it goes by the order defined by this score.

Here, we fork based on whether ADL is total or not, closing the position in the former case.

So essentially, in pseudocode, the whole latter part of clearinghouse_adl_orchestrator boils down to:

Loading interactive snippet…

Note that fill.pos_szi is the full position size of the insolvent user, not just the position shortfall. This is key to understanding the financial implications of ADL.

Branches

There is still much to be explored here: we could try to take a leap forward and conjecture some financial conclusions from what we've learned so far, or we could take a step back and do some introspection on the tools that made this all possible.

Given the polar opposite direction of those two ends, the author has decided to let the reader choose their own adventure:

Financial Conclusions

Fairness, Revenue, Solvency Trilemma

Aligning with the trilemma proposed in §2.1, prop. 2.5 of the paper mentioned before, let's try to quantify a concrete estimate for each axis of the trilemma:

  • Solvency: can the platform pay every trader out?
  • Fairness: we will use the axiomatic fairness definition per prop. 3.4.
  • Revenue: the fraction of total winner PNL that survives after deleveraging.

Let's borrow these definitions from the paper:

(: mark price from different sources; : insurance fund, roughly the 5M on testnet HL; is PNL for longs, negated for shorts; is the positive part of PNL.)

And let's make the trilemma definitions concrete:

  • Solvency = — how much of the total bad debt was actually covered. means fully solvent (), means nothing was recovered.
  • Fairness = where for each winner with — how uniformly the haircut burden is distributed across profitable traders. means everyone loses the same fraction of their PNL. means a few get wiped while most are untouched.
  • Revenue = is the haircut capacity (total positive PNL) after going through a policy

We can now simulate these values for both Hyperliquid and Percolator, a new pro-rata based perp engine developed by Anatoly Yakovenko.

aeyakovenko/percolator
Repository on GitHub

Given a fixed a priori price, which will simulate both a crash followed by a recovery, and a crash with an equal crash after (this is the most common reason for deleveraging, but the opposite could also happen).

One prediction that we can make is that Percolator is perfectly fair according to this scoring: the are the same for everyone, thus the Gini coefficient must be 0.

We are ignoring the a posteriori price impact that deleveraging causes (read about the Oct 10 crash analysis in that paper). Cross-margin leverage is also interesting because it introduces more correlation between cross-traded assets, while with Percolator we have one risk engine per asset (slab).

Hyperliquid also has an interesting caveat we showed earlier, as it operates with a conditional insurance fund: we will simulate a single asset for now, but this influences the equation when we have multiple assets being traded in either isolated or cross-margin mode.

Simulations

Let's consider the following price scenarios, as we said:

Price scenarios for the simulation

Running this simulation of both systems with a Python reimplementation yields these results:

Trilemma radar comparing HL and Percolator

Percolator Is More "Optimistic"

The key difference is what happens to positions. ADL permanently closes them: if the market keeps moving in your favor, tough luck, you're already out and have to re-enter at the new price. Percolator only reduces what you can withdraw, but the position stays open. If conditions improve, Residual goes up, h climbs back toward 1, and you get your PNL back without doing anything.

We can see this in a short squeeze scenario (60% short market, price +10%). HL's queue closes 8 of the 27 winning longs — those traders get zero if the rally continues. Under Percolator, all 27 keep their full positions (only the withdrawable profit is reduced by the uniform haircut) and all participate in the continued move. Note that HL's surviving positions are at full size, so they individually capture more per position, though the tradeoff is that fewer traders get to participate.

Rebound opportunity under Percolator vs HL ADL

Some Points to Be Made

Percolator Is Indifferent to Leverage

We can prove that this is quite the opposite for Hyperliquid, as we've already traced the implementation of the ADL algorithm, which disproportionately targets high leverages.

Leverage indifference

Fairness

A point made in the same paper is that Hyperliquid is "antifair": by this point we should have proven that in the implementation of the node itself, in the way positions to be closed are chosen (descend from the highest-scored one to lower; don't touch anything else).

Haircut distribution

The paper already states in prop. 8.1 that queue-based algos (like HL) reach solvency faster. We can see that by looking at how ADL is triggered: in clearinghouse_adl_orchestrator we start by fully closing the worst position against the best-performing trader.

Strengths and Limitations of Agents for Reverse Engineering

In general, the lion's share of the work is done by the underlying model, save for very straightforward tasks: attempts to overly aid the model in "search problems", meaning having it pick among a possible set of conclusions, usually lead to diminished performance.

Hooks

What tools are excellent at, on the other hand, is checking the agent's work via hooks: the agent proposes a solution to a very guessy problem, for example type recovery, and we automatically use the compiler scaffolding to check whether the given type would have compatible offsets and nested pointers compared to what is really used at the assembly level. A similar but simpler reasoning was applied to data flow in the reconstructed Rust snippets.

At the same time, there's a very thin line between getting a readable output with no validation, and getting something that's unreadable but technically "passes" according to the hook: in general, hooks trade time and readability for concrete properties to be sure of on the output of each stage, for example whether the Rust sample compiles and uses variables that are mappable 1:1 to the disassembly.

Scale

The second lesson to be learned is that agents really shine at scale: newer models can hold more context than the best reverse engineers out there, so using them for bulk looped tasks (like renaming many functions) is optimal.

That being said, agents are also far more overconfident than the best reverse engineers. In setups where the agent has tools that "write" to the decompiler view, wrong guesses compounded across recovery stages; tools prevented that by prompting agents that failed their scrutiny to give up and circle back to harder questions with more data, and could thus handle longer runs better.

What's next?

This is by no means a strict scientific verdict, but rather some field notes. The public source code for the agents and their tools can be found over at this GitHub repo:

renato-osec/patina-research
Repository on GitHub

The analyzed binary can be found here:

hl-node
File • 51.1 MB

Closing Words

As much as it would be exciting to say that everything can be cracked open and studied in the age of LLMs, there are still some gaps to bridge.

Flagship models got us 60% of the way there, but a lot of manual work is still required: on one hand, documenting patterns in the Rust compiler and distilling that knowledge onto agents and tools; on the other, understanding the "whys" behind a program, beyond the technical question of the "what".

Still, there is no doubt that LLMs have greatly accelerated progress in the field, and the dream of throwing a well-armed swarm of agents at an ugly blob of machine code to extract meaningful—and, most importantly, correct—representations and source code is nearing.

We hope to be part of that future, where even complex and obfuscated systems will be verifiable at a glance.

Subscribe to our blogs