Perennial V2 Update #4 — disclosed vulnerability reports and payouts
Every publicly disclosed and closed bug bounty report we hold for Perennial V2 Update #4, with our own summary of each finding and a link to the original disclosure.
- Reports indexed
- 7
- Total paid
- $0
- Critical
- 0
- Largest payout
- $0
This page collects the 7 closed, publicly disclosed Perennial V2 Update #4 reports indexed on Coin Buggie. Nothing here is active or unpatched — every entry was published by the programme or the researcher after remediation.
The findings concentrate in Logic error, across EVM-Solidity. Reading a single protocol end to end is the fastest way to see which assumptions its codebase repeatedly gets wrong, which is usually a better predictor of where the next finding lives than the category alone.
Use the vulnerability class links below to compare these findings against the same bug class in other protocols.
What reviewers look for
- Logic error findings disclosed against Perennial V2 Update #4
Curated highlights
The largest disclosed payouts in this group, with our own summary of each. Every report links back to the original disclosure.
Perennial V2 Update #4: When account is liquidated (protected), liquidator can increase account's position to any value up to `2**62 - 1` breaking all market accounting and stealing all market funds.
Perennial V2 Update #4 removed the invariant that forced a liquidated account's position to only decrease during liquidation. In the current code only a pending-close-state condition gates liquidation, and because collateral and position-size checks are skipped during liquidation, a liquidator can inflate an account's position to the maximum storable value (2**62 - 1). Any subsequent price move then produces an enormous phantom profit or loss, which the attacker can withdraw as market collateral, draining all market funds. The flaw sits in InvariantLib.sol line 121 and was fixed in equilibria-xyz/perennial-v2 PR #554 by restoring the requirement that liquidation may only shrink a position.
Perennial V2 Update #4: Some accounts using Intents to trade might be liquidated while healthy or be unliquidatable while being unhealthy.
Perennial V2's signed-intent trading lets users place pending orders at a user-specified intent price. When the liquidation/protection health check runs in InvariantLib.validate, it only counts settled collateral and ignores the guaranteed PnL embedded in pending intent orders — even though that same price adjustment is added to collateral when the margin to open the order is checked. This asymmetry means a liquidator can unfairly liquidate a healthy account when price moves against the user but the guaranteed intent profit should have kept it safe, and, in reverse, an unhealthy account with a pending intent loss can appear solvent so liquidation reverts. The protocol team confirmed the report and fixed it in PR #565 of the equilibria-xyz/perennial-v2 repository.
Perennial V2 Update #4: Anyone can steal all funds from the `market` due to incorrect health accounting for pending pnl from difference of intent price and market price when multiple intents are used.
Perennial's market invariant check only reflects the unrealized price-dependent PnL of the single newest pending signed intent when validating an account's collateral, silently ignoring all earlier intents still awaiting settlement in the same epoch. Because the intent price is user-controlled within the protocol's max-price-deviation bound off the latest oracle price, an attacker can submit the same overpriced intent repeatedly; each submission independently appears healthy while the combined settlement drives the collateral deeply negative. The counterparty account books equal opposite profit, and after settlement the attacker withdraws the entire market collateral balance. The maintainers fixed this by summing the price adjustments of all pending guarantees when checking the margined requirement (PR #553).
Perennial V2 Update #4: Vault.settle(account=coordinator) will lose profitShares
Perennial V2's Vault.settle() accrues profitShares to the protocol coordinator while processing checkpoints, but the credit path writes directly to storage through _credit() without updating the in-memory context.local.shares copy. When settle() is invoked with the coordinator as the settlement account, the final _saveContext() call persists the stale in-memory share count, overwriting the just-credited profitShares. Any caller can reach this by choosing the coordinator as the settlement account, causing the coordinator to permanently lose the profitShares it should have received. The protocol team confirmed the finding and fixed it in PR #563 by mirroring Market.sol's _credit() behavior, which mutates context.local.shares when the credited account equals the settlement account.
Perennial V2 Update #4: Intent orders are guaranteed to execute, but fees from these orders are not accounted in collateral, allowing user to withdraw all collateral ignoring these pending fees.
Perennial V2's Intent orders are guaranteed to execute (invalidation=0), unlike regular pending orders which can still be invalidated by a bad oracle commit. However, trade fees on those guaranteed orders are only debited from collateral when checkpoints advance after the epoch price is committed, so during the pending window the position settles but the fees do not. An attacker can therefore open and close a position through Intents, withdraw the full deposited collateral before fees are booked, then claim referral fees after the price commits — leaving an abandoned zero-collateral account as bad debt. Because the whole sequence fits in one transaction, a flash loan is sufficient, allowing the attacker to drain the market by fabricating referral fee claims.
Perennial V2 Update #4: `InvariantLib` uses current position for margin check allowing to withdraw collateral while the position decrease is only pending and can cause unexpected immediate user liquidation.
Perennial V2's invariant validation uses two different position values for what should be the same solvency check. Normal update margins are validated against the pending expected position (currentPosition), while the liquidation maintenance check evaluates the last committed position (latestPosition). Whenever a user has pending position changes these diverge, so a user who reduces position and withdraws collateral to a healthy level for the pending position is still immediately liquidatable against the stale, larger committed position. Beyond unfairly liquidating honest users, an attacker can close a full position, withdraw all collateral against a zero pending position, then self-liquidate to farm liquidation fees and drain market funds when those fees exceed trading fees. The protocol acknowledged the issue and merged fix PR 565.