Reproduced Exploit

Perennial: a protected liquidation stores a 262-1 long and takes the maker cash

_validateProtection returns true when pending.negative equals the latest magnitude. The old "position may only decrease" check is gone, so a liquidator replaces a size-10 pending close with a long of 262-1. Doubling the price marks that notional for the entire maker deposit. Settlement pays it out.…

Jan 2025•Ethereum•logic•3 min read

Loss

262-1 units of maker cash paid to the liquidated account

Chain

Ethereum

Category

logic

Date

Jan 2025

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 45979-h-4-when-account-is-liquidated-protected-liquidator-can-incr. Standalone Foundry PoC and full write-up: 45979-h-4-when-account-is-liquidated-protected-liquidator-can-incr_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/logic/wrong-condition · vuln/logic/liquidation-logic

Key info#

FieldValue
Loss2**62-1 units of maker cash paid to the liquidated account
Vulnerable contractMarket.liquidate / _validateProtection
Attacker EOA0x4597900000000000000000000000000000000001
Attack contractExploit
Attack txExploit.run()
Chain / block / dateEthereum model · block 0 · 2025-01
Compilersolc 0.8.27, cancun, via IR
Bug classPending full close passes protection, and the new long is not required to shrink

TL;DR#

_validateProtection returns true when pending.negative equals the latest magnitude. The old "position may only decrease" check is gone, so a liquidator replaces a size-10 pending close with a long of 2**62-1. Doubling the price marks that notional for the entire maker deposit. Settlement pays it out. The fixed path rejects any larger long and the cash stays in the market.

Background#

Sherlock's January 2025 Perennial V2 update contest, issue 32, reported by panprog. A pending full close used to be liquidated by closing. The audited check only asks whether the pending negative equals the latest magnitude, and protected orders skip margin checks. Equilibria restored the decrease requirement in perennial-v2 pull 554.

The vulnerable code#

SOLIDITY
if (!context.pendingLocal.neg().eq(context.latestPositionLocal.magnitude())) return false; // no pending zero-cross, liquidate with full close
if (L.maintained) return false;
// @> VULN: pending.negative == latest magnitude passed the check, so the liquidation stores a larger long
L.latestMagnitude = newLong;
L.entryPrice = price;

newLong is 2**62-1. The entry price is still 1e18. The next commit sets the price to 2e18, and pnl is the full magnitude.

Root cause#

Equality with the pending close is treated as permission to liquidate, and nothing after that compares the new long with the old one. Collateral and position-size checks are skipped because the order is protected.

Preconditions#

  • The account's pending negative equals its latest magnitude. Here both are 10, so the close is fully pending.
  • The account is not maintenance-collateralized.
  • Makers have already deposited 2**62-1 units of cash.

Attack walkthrough#

  1. The liquidator calls liquidate with 2**62-1 on the pending close.
  2. The equality check returns true. The huge long is stored at price 1e18.
  3. The price is committed at 2e18.
  4. Settlement computes pnl equal to the whole maker balance and transfers it.
  5. The fixed market returns false on newLong > latestMagnitude. The cash never moves.

Diagrams#

flowchart TD A["Pending close equals the latest magnitude of 10"] --> B["Protection returns true"] B --> C["VULN stores long 2**62-1 at the old price"] C --> D["Price doubles"] D --> E["PnL equals the maker deposit"] E --> F["Maker cash is paid to the liquidated account"]

Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.