Reproduced Exploit

Vertex PerpEngine — settlement pays the whole AMM instead of the LP share

getSettlementState adds the AMM's entire base and quote into the trader's PnL. Only amountLp / supply of that inventory belongs to the trader. With price 1, 100 base, 100 quote, and a 10 of 100 LP position, settlement pays 200 quote. The fair figure is 20. A second engine that scales by the LP shar…

Jan 2024•Other•logic•3 min read

Chain

Other

Category

logic

Date

Jan 2024

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: 48378-improper-settlement-calculation-ottersec-none-vertex-protoco. Standalone Foundry PoC and full write-up: 48378-improper-settlement-calculation-ottersec-none-vertex-protoco_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/logic/price-calculation · vuln/logic/wrong-condition

Key info#

ProtocolVertex PerpEngine
FindingOtterSec, Shiva Shankar, AuditVault 48378
Vulnerable codePerpEngine.settle position PnL
HarmPaid 200 quote. The trader's 10 percent LP share is fairly 20. Extra 180.
Chainlocal synthetic

TL;DR#

getSettlementState adds the AMM's entire base and quote into the trader's PnL. Only amountLp / supply of that inventory belongs to the trader. With price 1, 100 base, 100 quote, and a 10 of 100 LP position, settlement pays 200 quote. The fair figure is 20. A second engine that scales by the LP share pays 20.

The vulnerable code#

SOLIDITY
// @> VULN: adds the entire AMM base and quote, not amountLp/supply of the pool
positionPnlX18 = priceX18.mul(balance.amountX18 + ammBaseX18) + balance.vQuoteBalanceX18 + ammQuoteX18;

The fix multiplies ammBaseX18 and ammQuoteX18 by amountLp / supply before adding them.

Root cause#

The settlement formula treats pool inventory as if the settler owned all of it. Every LP who settles is credited with the same full pool value, so the quote vault can be drained down to the last settler's full-pool claim.

Preconditions#

The trader has a non-zero LP balance that is less than the total supply, and the AMM holds base and quote. No oracle update is required. Price is 1.0 in this run so the quote units are the raw inventory.

Attack walkthrough#

  1. The trader deposits 10 LP against a supply of 100. Position base and vQuote are 0.
  2. The pool holds 100 base and 100 quote.
  3. settle at price 1 computes 1 * (0 + 100) + 0 + 100 = 200 and transfers 200 quote.
  4. The fair PnL is 1 * (0 + 10) + 10 = 20.
  5. The control engine pays 20.

Diagrams#

flowchart TD A["Trader owns 10 of 100 LP"] --> B["Pool holds 100 base and 100 quote"] B --> C["VULN: PnL adds the whole pool"] C --> D["Settlement pays 200 quote"] D --> E["Fair share is 20 and the control pays 20"]

Impact#

The settler receives 200 quote instead of 20. The extra 180 is other LPs' inventory. Repeating the settle for every LP would credit the full pool once per LP.

Remediation#

Scale ammBase and ammQuote by amountLp / supply before adding them to the position PnL.

How to reproduce#

BASH
cd 48378-improper-settlement-calculation-ottersec-none-vertex-protoco_exp
forge test -vv

Sources#


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.