Skip to content

Latest commit

 

History

History
90 lines (72 loc) · 4.22 KB

File metadata and controls

90 lines (72 loc) · 4.22 KB

Dazzling Pastel Gibbon

High

the “creating a new checkpoint” process

When the contract calls _update(), which then invokes _checkpoint(context), it correctly generates an updated context.currentCheckpoint and stores it in _checkpoints[newId]. However, it never writes this new checkpoint’s ID back to the global state (context.global.current).

As a result, the subsequent settlement logic (_settle())—which relies on iterating through context.global.current and context.global.latest—fails to detect the newly created checkpoint. This ultimately isolates the new checkpoint, making it impossible to be processed properly.

https://github.com/sherlock-audit/2025-01-perennial-v2-4-update/blob/2381b47e69b17fe892b1d1d0f467c358cf950143/perennial-v2/packages/vault/contracts/Vault.sol#L297 Detailed Explanation of the Bug

  1. A new checkpoint is created but context.global.current is not updated
function _checkpoint(Context memory context) private view {
    // 1) Fetch the "current" context.global.current
    context.currentId = context.global.current;
    context.currentCheckpoint = _checkpoints[context.currentId].read();

    // 2) If now > current checkpoint.timestamp, increment currentId
    //    and modify the new checkpoint in memory
    if (context.currentTimestamp > context.currentCheckpoint.timestamp) {
        context.currentId++;
        context.currentCheckpoint.next(context.currentTimestamp, context.global);
    }
}
•	Here, context.currentId may be incremented, but this change only happens locally.
•	The new context.currentId is never written back to context.global.current.
  1. The new checkpoint is stored, but context.global.current remains unchanged
function _saveContext(Context memory context, address account) private {
    if (account != address(0)) _accounts[account].store(context.local);
    _accounts[address(0)].store(context.global);
    
    // The new checkpoint is stored in _checkpoints
    _checkpoints[context.currentId].store(context.currentCheckpoint);

    // However, context.currentId is NOT written back to context.global.current
    // This means the global "current" ID does not advance.
}
•	The contract correctly saves the new checkpoint in _checkpoints[...].
•	However, it never updates context.global.current, meaning the global state does not recognize that a new checkpoint was created.
  1. _settle() relies on context.global.current > context.global.latest to process new checkpoints
while (
    context.global.current > context.global.latest &&
    ...
) {
    // Only then does it proceed to call nextCheckpoint.complete(...)
}
•	_settle() only processes checkpoints that context.global.current acknowledges.
•	Since context.global.current was never updated, _settle() never sees the newly created checkpoint.
•	As a result, the settlement logic skips the new checkpoint, leaving it orphaned and preventing it from being finalized.

Fixing the Issue

A standard fix is to update _checkpoint() or _saveContext() so that the incremented context.currentId is written back to context.global.current. This ensures that the global state is aware of the newly created checkpoint.

Fixed Version of _checkpoint() and _saveContext()

function _checkpoint(Context memory context) private view {
    context.currentId = context.global.current;
    context.currentCheckpoint = _checkpoints[context.currentId].read();

    if (context.currentTimestamp > context.currentCheckpoint.timestamp) {
        context.currentId++;
        context.currentCheckpoint.next(context.currentTimestamp, context.global);
    }
}

function _saveContext(Context memory context, address account) private {
    if (account != address(0)) _accounts[account].store(context.local);
    
    // FIX: Write context.currentId back to context.global.current
    context.global.current = context.currentId;
    _accounts[address(0)].store(context.global);

    _checkpoints[context.currentId].store(context.currentCheckpoint);
}

Why This Fix Works 1. The new checkpoint’s ID is now stored in context.global.current, making it globally recognized. 2. _settle() can now detect the new checkpoint and process it correctly. 3. The contract no longer leaves checkpoints orphaned and ensures proper settlement