# Denaria Finance: Virtual AMM Manipulation via Unprotected `realizePnL`

On April 5, 2026, Denaria Finance, a perpetual DEX on Linea, (block 30,067,821) suffered a virtual AMM manipulation attack that drained approximately **165,618 USDC** from the protocol’s Vault. The attacker flash-loaned 60,000 USDC from Aave V3, deployed pairs of ephemeral LP and Trader contracts, and exploited the absence of any manipulation guard in `realizePnL`: a large synthetic long trade ( `trade(long, 100K vStable)`) heavily skewed the virtual AMM’s `globalLiquidityStable`/ `globalLiquidityAsset` ratio, causing `_calcPnL` to return a wildly inflated PnL for the LP — roughly 183,283 USDC profit from a 30,000 USDC collateral deposit. The inflated PnL was immediately materialized via `addPnlToCollateral` and withdrawn, then a second round drained the remaining reserves.

## Root Cause

### Vulnerable Contract

– **Name**: PerpPair (Diamond Proxy)
– **Proxy address**: `0xb68396dd4230253d27589e2004ac37389836ae17`
– **Pattern**: Diamond proxy (EIP-2535). The `realizePnL` function executes in the proxy’s storage context via `DELEGATECALL` to facets:
– `0x0ef31752a4d7bef5a46b378873613f706255b9cd`(UtilMath — contains `_calcPnL`)
– `0x78197fe93999e34d5a688e1819923c66dcf8f4db`(CurveMath — contains `computeShortReturn`)
– `0xf1b5180b7aeabd55a9c57e53e31685a6e6ef7ae8`(FeeManager)
– `0x95776062d4e200a6ea701ea8114ab32b871c8f00`(MatrixMath)
– **Source type**: verified

Secondary vulnerable contract:

– **Name**: Vault
– **Address**: `0x61ce9b51010ba52f701444f0f3d1e563f6ae8d91`
– **Source type**: verified
– **Role**: holds USDC collateral; unconditionally applies inflated PnL via `addPnlToCollateral`

### Vulnerable Function

– **Function**: `realizePnL(bytes memory unverifiedReport)`
– **Selector**: `0x1cb794ea`
– **File**: `src/perpModules/internalPerpLogic.sol`

Internally calls:

– `calcPnL(user, getPrice())` → `UtilMath._calcPnL(…)` → `CurveMath.computeShortReturn(…)`
– `IVault(vault).addPnlToCollateral(user, pnl, pnlSign)`— in `Vault.sol`

### Vulnerable Code

“`
// internalPerpLogic.sol /// @dev Moves the PnL of the user to the user’s Collateral. function realizePnL(bytes memory unverifiedReport) external nonReentrant returns(uint256, bool){ IOracleMiddleware(oracle).verifyReportIfNecessary(unverifiedReport); address user = _msgSender(); VirtualTraderPosition storage pos = userVirtualTraderPosition[user]; (uint256 pnl, bool pnlSign) = calcPnL(user, getPrice()); // <– VULNERABILITY: reads live AMM state require(pnlSign || pnl **Note**: Decoded call indices corrected; the `decoded_calls.json` file contains 342 total call entries.

**Vault `totalCollateral` slot** ( `0x18c3a1f7…`): Pre = `0x5f5e100` = 100,000,000 (1e8 = 100 USDC in oracle units? Cross-checked with funds_flow: pre-tx vault USDC balance was 165,647.74 USDC, post-tx = 0)

**Receipt status**: Transaction succeeded (confirmed by `funds_flow.json` showing all transfers).

**Selector verification**:

– `realizePnL(bytes)` → `cast sig “realizePnL(bytes)”` = `0x1cb794ea` ✓
– `addPnlToCollateral(address,uint256,bool)` → `cast sig “addPnlToCollateral(address,uint256,bool)”` = `0x9fd8c908` ✓
– `trade(bool,uint256,uint256,uint256,address,uint8,bytes)` → `cast sig “trade(bool,uint256,uint256,uint256,address,uint8,bytes)”` = `0x5cd38b42` ✓
– `computeShortReturn(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)` → `0xc374f93a` ✓

## Related URLs

– Transaction: https://lineascan.build/tx/0xcb0744a0d453e5556f162608fae8275dabd14292bffbfcd8394af4610c606447
– PerpPair contract: https://lineascan.build/address/0xb68396dd4230253d27589e2004ac37389836ae17
– Vault contract: https://lineascan.build/address/0x61ce9b51010ba52f701444f0f3d1e563f6ae8d91
– Attacker EOA: https://lineascan.build/address/0x8d6778d7fae00ad2e0bc12194cf03b756fed9db3