DeFiFoundry
50,000 USDC
View results
Submission Details
Severity: low
Invalid

Flow State Race Condition Enables Cross-Operation Interference

Summary

The flow state transitions in PerpetualVault can be manipulated to bypass expected state management. It's specifies that flow states should only change from NONE, to NONE, or through cancelFlow(), but there are paths where this is violated. This breaks the safety assumptions about position management and fund security.

flow state transitions must follow strict patterns

flowAfter != flowBefore => (
flowBefore == PerpetualVault.FLOW.NONE ||
flowAfter == PerpetualVault.FLOW.NONE ||
f.selector == sig:cancelFlow().selector
);

The flow state (NONE, DEPOSIT, SIGNAL_CHANGE, WITHDRAW, COMPOUND, LIQUIDATION) tracks critical vault operations.

The PerpetualVault contract manages these flows through various functions like deposit(), withdraw(), and run(). Each operation should:

  1. Start from FLOW.NONE

  2. Execute its logic

  3. Return to FLOW.NONE

However, the current implementation allows state transitions that violate this pattern. For example, during position management, a flow could transition directly between states without proper resolution.

Core Vulnerability Surface (Primary)

PerpetualVault.sol
├─ State Management
│ ├─ flow: FLOW (public)
│ ├─ nextAction: Action (public)
│ └─ _gmxLock: bool (private)
└─ Key Functions
├─ deposit() (external)
├─ withdraw() (public)
├─ run() (external)
├─ runNextAction() (external)
└─ afterOrderExecution() (external)

Integration Points (Secondary)

GmxProxy.sol
├─ Handles GMX interactions
└─ Callbacks to PerpetualVault
├─ afterOrderExecution()
└─ afterOrderCancellation()

Supporting Infrastructure (Tertiary)

VaultReader.sol
├─ Provides market data
└─ Position calculations
Interface Contracts
├─ IPerpetualVault.sol
├─ IGmxProxy.sol
├─ IVaultReader.sol
└─ IGmxReader.sol

The vulnerability flow typically starts in PerpetualVault's state management functions, propagates through GmxProxy's callbacks, and can affect position data reading through VaultReader. The interface contracts define the boundaries where these interactions occur.

When flow states transition incorrectly:

  • Multiple operations could execute simultaneously

  • Funds could be locked in incomplete states

  • Position management could become corrupted

  • User deposits/withdrawals might be blocked

Vulnerability Details

The Perpetual Vault system manages complex trading operations through state flows. The flow states (NONE, DEPOSIT, SIGNAL_CHANGE, WITHDRAW, COMPOUND, LIQUIDATION) represent different critical operations in the vault. Each operation should be like a complete transaction, start from idle (NONE), execute, and return to idle. But there's a catch in how these transitions are managed.

// Public State Variables
flow: FLOW // Tracks current operation state
nextAction: Action // Stores next operation to execute
curPositionKey: bytes32 // Current GMX position identifier
// External Entry Points
deposit(uint256 amount) external nonReentrant payable {
_noneFlow(); // Validates FLOW.NONE
flow = FLOW.DEPOSIT; // Critical state change
// ... position logic
}
withdraw(address recipient, uint256 depositId) public payable nonReentrant {
_noneFlow(); // Validates FLOW.NONE
flow = FLOW.WITHDRAW; // Critical state change
// ... withdrawal logic
}
// Keeper Operations
run(...) external nonReentrant {
_noneFlow();
flow = FLOW.SIGNAL_CHANGE; // Critical state change
// ... position management
}
// GMX Callbacks
afterOrderExecution(...) external nonReentrant {
_gmxLock = false; // State unlock
// ... state transitions based on order type
}
afterOrderCancellation(...) external {
_gmxLock = false; // State unlock
// ... error handling state changes
}

We can see in the interactions between these state transitions, particularly in runNextAction() where multiple state changes can occur without proper NONE state resolution.

What Actually Happens is that the PerpetualVault contract allows users to deposit USDC and take leveraged positions through GMX. When a user initiates an operation, the contract should:

  1. Start from FLOW.NONE

  2. Execute the operation (like deposit or position management)

  3. Return to FLOW.NONE

But here's the error, the contract can transition between active states without properly completing operations. This means a deposit flow could directly transition to a signal change flow, breaking the core safety assumption that operations must complete atomically.

Think of it like multiple people trying to drive through an intersection simultaneously when the traffic light malfunctions. In the vault's case, this could mean:

  • A deposit operation could collide with position management

  • Withdrawal locks could be bypassed

  • Position leverage could be manipulated during state transitions

In this case the vault can enter multiple operational states without proper resolution.

Impact

The vulnerability manifests in the interaction between runNextAction() and position management functions. When a user deposits funds or requests a position change, the keeper executes a series of actions through GMX. During these operations, the flow state can transition incorrectly, bypassing the NONE state that should separate distinct operations.

This means that invariants around share accounting and position management can be violated. For example, during a deposit flow, if the state transitions directly to SIGNAL_CHANGE without proper resolution, the protocol's assumption about atomic operations breaks down. The keeper system, which executes these actions asynchronously, compounds this risk by introducing timing complexities.

Users' share values could be miscalculated during these improper transitions, violating the "Depositor Share Value Preservation" invariant. With leveraged positions up to 3x, even small accounting errors can have amplified effects on user positions.

Tools Used
Manual

Recommendations

Implementing strict state machine controls that enforce proper transitions through the NONE state, ensuring the protocol maintains its core invariants around share accounting and position management.

// Core State Management
enum FLOW { NONE, DEPOSIT, WITHDRAW, SIGNAL_CHANGE, COMPOUND, LIQUIDATION }
flow: FLOW public
nextAction: Action public
_gmxLock: bool private
// State Transition Guards
modifier enforceFlowTransition() {
require(flow == FLOW.NONE, "Invalid flow transition");
_;
// Ensure completion returns to NONE
flow = FLOW.NONE;
}
// Protected Entry Points
deposit() external nonReentrant enforceFlowTransition {
flow = FLOW.DEPOSIT;
try {
// Position logic
} catch {
flow = FLOW.NONE;
revert();
}
}
// GMX Integration Points (GmxProxy.sol)
afterOrderExecution() external {
// Validate callback source
require(msg.sender == address(gmxProxy));
// Reset locks and handle state
_gmxLock = false;
_completeFlowTransition();
}

Supporting Contract Roles:

  • VaultReader.sol: Position data validation

  • GmxProxy.sol: Order execution callbacks

  • Interface contracts: Define strict state transition boundaries

Updates

Lead Judging Commences

n0kto Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

Suppositions

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.

n0kto Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

Suppositions

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.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.