The position state tracking in PerpetualVault can become inconsistent when the positionIsClosed flag changes without a corresponding update to curPositionKey. This breaks the invariant that an open position must always have a valid position key, potentially leading to incorrect position management.
Whenever the position state (open/closed) changes, the position key must also change accordingly. The main issue arises in PerpetualVault.sol where position state transitions can occur in multiple functions like afterOrderExecution(), afterLiquidationExecution(), and _updateState(). The contract fails to maintain synchronized updates between positionIsClosed and curPositionKey.
In PerpetualVault.sol, position state is tracked through two key variables
positionIsClosed: Boolean indicating if a position is currently open
curPositionKey: bytes32 hash identifying the current position on GMX
The violation occurs when these get out of sync, particularly during complex flows involving GMX callbacks. For example, in afterOrderExecution(), the position state might change without properly updating or clearing the position key.
This state inconsistency could lead to:
Incorrect position tracking
Failed operations due to invalid position keys
Potential loss of position control if the key becomes invalid while the position is marked as open
Two critical pieces must always move in sync: the positionIsClosed flag and the curPositionKey. Think of these as the lock and key to a safety deposit box, they should never be out of alignment.
But when you see how the system handles position transitions through the GMX protocol. When a user opens a position, the vault creates a unique position key and marks positionIsClosed as false. When closing, both should reset, but here's where things get absorbing.
The error is in the complex flow of callbacks and state updates. Let's walk through the scenario:
A vault has an open position (positionIsClosed = false, valid curPositionKey)
During liquidation handling in afterLiquidationExecution(), the position gets closed on GMX's side
The vault updates positionIsClosed to true, but curPositionKey remains unchanged
This means that subsequent operations might reference an invalid or stale position key while thinking the position is still open. It's like having a key to a lock that's already been changed.
During normal operations, a trader deposits USDC, the keeper executes their desired leverage strategy, and the vault maintains a careful record of position states. But here's where things get tricky, during liquidations or failed GMX callbacks, the position key can become invalidated while the vault still thinks the position is open.
This means that subsequent operations could reference a phantom position. Think about a trader trying to adjust their leverage or withdraw funds, they'd be operating on position data that doesn't actually exist on GMX anymore. The vault would be like a GPS showing you're still on the highway when you've already taken the exit.
The impact goes beyond simple state confusion. With position keys and states misaligned, the vault could:
Execute trades against invalid positions
Calculate incorrect share values during withdrawals
Fail to properly distribute funding fees among depositors
Looking at the code, we can see exactly where this happens:
My observation is that position state changes can happen through three different paths:
Market order execution (via GMX callback)
Liquidation handling
Normal state updates
Only the third path properly synchronizes positionIsClosed with curPositionKey. The other two paths can leave these critical state variables misaligned.
This interaction between PerpetualVault.sol and GmxProxy.sol (via callbacks) creates the vulnerability surface, while VaultReader.sol provides the position data that influences these state transitions.
Manual
Consider treating position state updates as atomic operations, ensuring the vault's view of positions always matches reality on GMX. This maintains the critical invariant that "PositionKey is 0 when no position exists" while preserving the vault's ability to manage leveraged positions safely.
To ensure that position state changes are always handled atomically through a dedicated function, maintaining consistency between positionIsClosed and curPositionKey. The _updatePositionState function becomes the single source of truth for position state transitions.
Position state changes are always atomic through _syncPositionState
All paths that modify position state use the same synchronized update mechanism
The invariant "PositionKey is 0 when no position exists" is enforced at the implementation level
The interaction between PerpetualVault and GmxProxy remains unchanged, but now position state transitions are guaranteed to maintain consistency.
There is no real proof, concrete root cause, specific impact, or enough details in those submissions. Examples include: "It could happen" without specifying when, "If this impossible case happens," "Unexpected behavior," etc. Make a Proof of Concept (PoC) using external functions and realistic parameters. Do not test only the internal function where you think you found something.
"// keep the positionIsClosed value so that let the keeper be able to create an order again with the liquidated fund" Liquidation can send some remaining tokens. No real impact here.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.