Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dazzling Pastel Gibbon - the “creating a new checkpoint” process #61

Open
sherlock-admin3 opened this issue Jan 31, 2025 · 0 comments
Labels
Sponsor Disputed The sponsor disputed this issue's validity

Comments

@sherlock-admin3
Copy link

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

@sherlock-admin3 sherlock-admin3 added the Sponsor Disputed The sponsor disputed this issue's validity label Feb 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Sponsor Disputed The sponsor disputed this issue's validity
Projects
None yet
Development

No branches or pull requests

1 participant