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

Chain Split/Reorg Vulnerability in State Machine

[CRITICAL-1] Chain Split/Reorg Vulnerability in State Machine

Location

PerpetualVault.sol -> afterOrderExecution() function

Description

The protocol's state machine can become permanently corrupted during Arbitrum chain reorganizations due to the following critical issues:

  1. Asynchronous State Updates:

    • The vault updates its state in response to GMX callbacks

    • These updates occur across multiple blocks and transactions

    • During a chain reorg, some transactions may be dropped while others remain

  2. Incomplete State Recovery:

    • When a chain reorg occurs, the vault has no mechanism to detect that its state is now inconsistent

    • The state machine assumes all previous transactions were successful

    • No rollback mechanism exists for partially completed operations

  3. Race Conditions During Reorgs:

    • Multiple keepers can attempt to handle the same position during a reorg

    • The vault cannot distinguish between legitimate and reorged callbacks

    • This leads to state corruption when handling concurrent operations

  4. Permanent State Corruption Scenarios:

    • If GMX execution succeeds but callback fails due to reorg

    • If callback succeeds but position update fails due to reorg

    • If keeper operations are split across the reorg boundary

An attacker could exploit this by:

  1. Monitoring the network for signs of instability or high reorg probability

  2. Opening a large position when network instability is detected

  3. Using the reorg to manipulate which transactions get included

  4. Forcing the vault into an unrecoverable state

Impact

  • Permanent vault lockup requiring contract upgrade

  • Complete loss of functionality for all users

  • Stuck funds with no recovery mechanism

  • System-wide denial of service

Proof of Concept

contract ChainReorgVulnerabilityTest is Test {
PerpetualVault public vault;
GmxRouter public gmxRouter;
address attacker = address(0x1);
function setUp() public {
vault = new PerpetualVault();
gmxRouter = new GmxRouter();
// Setup initial state
vm.deal(attacker, 100 ether);
vm.startPrank(attacker);
vault.deposit{value: 10 ether}(10 ether);
}
function testChainReorgVulnerability() public {
// 1. Create initial position
bytes32 positionKey = vault.createPosition({
isLong: true,
size: 10 ether,
collateral: 1 ether
});
// 2. Simulate start of chain reorg
vm.roll(block.number + 1);
// 3. GMX execution succeeds
gmxRouter.executeOrder(positionKey);
// 4. Simulate chain reorg dropping the callback
vm.rollFork(block.number - 1);
// 5. Attempt new position - should fail due to locked state
vm.expectRevert("Vault locked");
vault.createPosition({
isLong: true,
size: 1 ether,
collateral: 0.1 ether
});
// 6. Verify stuck state
assertTrue(vault.isLocked());
assertEq(vault.getPositionState(positionKey), POSITION_STATE.UNKNOWN);
// 7. Try admin recovery - should fail
vm.startPrank(vault.owner());
vm.expectRevert("Cannot reset during execution");
vault.emergencyReset();
}
function testMultiKeeperReorgRace() public {
// 1. Setup multiple keepers
address keeper1 = address(0x2);
address keeper2 = address(0x3);
vault.addKeeper(keeper1);
vault.addKeeper(keeper2);
// 2. Create position that will be caught in reorg
bytes32 positionKey = vault.createPosition({
isLong: true,
size: 10 ether,
collateral: 1 ether
});
// 3. Simulate keeper1 execution pre-reorg
vm.prank(keeper1);
vault.executeCallback(positionKey);
// 4. Simulate reorg
vm.rollFork(block.number - 1);
// 5. Simulate keeper2 execution post-reorg
vm.prank(keeper2);
vault.executeCallback(positionKey);
// 6. Verify corrupted state
assertTrue(vault.isStateCorrupted());
assertNotEq(vault.getPositionState(positionKey), POSITION_STATE.CLOSED);
}
}

Recommendation

Implement the following safeguards to protect against chain reorganizations:

  1. Transaction Ordering and State Verification:

contract PerpetualVault {
struct ExecutionState {
uint256 nonce;
bytes32 lastCallbackHash;
mapping(bytes32 => bool) processedCallbacks;
uint256 lastExecutionBlock;
bool isProcessing;
}
ExecutionState public executionState;
modifier validateExecution(bytes32 callbackHash) {
require(!executionState.processedCallbacks[callbackHash], "Callback already processed");
require(block.number > executionState.lastExecutionBlock, "Same block execution");
_;
executionState.lastCallbackHash = callbackHash;
executionState.processedCallbacks[callbackHash] = true;
executionState.lastExecutionBlock = block.number;
}
}
  1. Reorg Detection and Recovery:

contract PerpetualVault {
uint256 public constant REORG_THRESHOLD = 20; // blocks
struct ReorgProtection {
uint256 confirmations;
mapping(bytes32 => uint256) positionBlocks;
mapping(bytes32 => bool) finalizedPositions;
}
ReorgProtection public reorgProtection;
function finalizePosition(bytes32 positionKey) external {
require(
block.number >= reorgProtection.positionBlocks[positionKey] + REORG_THRESHOLD,
"Wait for confirmations"
);
reorgProtection.finalizedPositions[positionKey] = true;
}
}
  1. State Checkpointing System:

contract PerpetualVault {
struct StateCheckpoint {
bytes32 stateRoot;
uint256 blockNumber;
mapping(bytes32 => PositionState) positions;
}
mapping(uint256 => StateCheckpoint) public checkpoints;
function createCheckpoint() internal {
uint256 checkpointId = block.number / REORG_THRESHOLD;
checkpoints[checkpointId].stateRoot = calculateStateRoot();
checkpoints[checkpointId].blockNumber = block.number;
}
function reconcileState(uint256 checkpointId) external onlyOwner {
require(
checkpoints[checkpointId].stateRoot != bytes32(0),
"Checkpoint doesn't exist"
);
_reconcileToCheckpoint(checkpointId);
}
}

Additional recommendations:

  1. Implement proper monitoring for chain reorganizations

  2. Add automated circuit breaker triggers

  3. Consider using a timelock for sensitive operations

  4. Maintain comprehensive state checkpoints

  5. Add emergency shutdown capabilities for extreme cases

  6. Implement proper logging and monitoring of reorg events

  7. Consider using a more conservative confirmation threshold for L2s

Updates

Lead Judging Commences

n0kto Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
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.