Skip to content
This repository has been archived by the owner on Oct 6, 2024. It is now read-only.

Trust - Anyone can freeze future withdrawals and any L2->L1 messaging due to mismatch between the VM-viewed block number and the user supplied block number. #206

Closed
sherlock-admin2 opened this issue Apr 4, 2024 · 0 comments
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label Medium A valid Medium severity issue Reward A payout will be made for this issue Sponsor Confirmed The sponsor acknowledged this issue is valid Won't Fix The sponsor confirmed this issue will not be fixed

Comments

@sherlock-admin2
Copy link
Contributor

sherlock-admin2 commented Apr 4, 2024

Trust

high

Anyone can freeze future withdrawals and any L2->L1 messaging due to mismatch between the VM-viewed block number and the user supplied block number.

Summary

Anyone can freeze future withdrawals and any L2->L1 messaging due to mismatch between the VM-viewed block number and the user supplied block number.

Vulnerability Detail

When a game is created, the proposer passed the L2 block number. It is stored as the third Clone parameter:

function l2BlockNumber() public pure returns (uint256 l2BlockNumber_) {
    l2BlockNumber_ = _getArgUint256(0x40);
}

If a game is defended correctly, the anchor will use this function to commit a trusted OutputRoot:

anchors[gameType] = OutputRoot({ l2BlockNumber: game.l2BlockNumber(), root: Hash.wrap(game.rootClaim().raw()) });

This root can at that point be used by proofs.
Note that if the l2BlockNumber() is not fresh, meaning we have a root for a later block number, the anchor state is not updated:

// No need to update anything if the anchor state is already newer.
if (game.l2BlockNumber() <= anchors[gameType].l2BlockNumber) {
    return;
}
// Must be a game that resolved in favor of the state.
if (game.status() != GameStatus.DEFENDER_WINS) {
    return;
}
// Actually update the anchor state.
anchors[gameType] = OutputRoot({ l2BlockNumber: game.l2BlockNumber(), root: Hash.wrap(game.rootClaim().raw()) });

Thus, if it was possible to prove an honest root for a dishonest l2BlockNumber, an attacker could choose a block number far in the future and make it the anchor. From that point, all true proofs will suffer from the early return and won't update the rootClaim. Thus, any new withdrawals will not be accepted (they are not part of the trusted root).

In fact, this is exactly the case, there is no limit to how large the passed blockNumber is. On Game creation, it just checks it is not smaller than the already trusted root:

// Do not allow the game to be initialized if the root claim corresponds to a block at or before the
// configured starting block number.
if (l2BlockNumber() <= rootBlockNumber) revert UnexpectedRootClaim(rootClaim());

Furthermore, this parameter is invisible in the eyes of the Fault Proof VM. In addLocalData(), we can see which inputs are sent to the VM for processing.

IPreimageOracle oracle = VM.oracle();
if (_ident == LocalPreimageKey.L1_HEAD_HASH) {
    // Load the L1 head hash
    oracle.loadLocalData(_ident, uuid.raw(), l1Head().raw(), 32, _partOffset);
} else if (_ident == LocalPreimageKey.STARTING_OUTPUT_ROOT) {
    // Load the starting proposal's output root.
    oracle.loadLocalData(_ident, uuid.raw(), starting.raw(), 32, _partOffset);
} else if (_ident == LocalPreimageKey.DISPUTED_OUTPUT_ROOT) {
    // Load the disputed proposal's output root
    oracle.loadLocalData(_ident, uuid.raw(), disputed.raw(), 32, _partOffset);
} else if (_ident == LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER) {
    // Load the disputed proposal's L2 block number as a big-endian uint64 in the
    // high order 8 bytes of the word.
    // We add the index at depth + 1 to the starting block number to get the disputed L2
    // block number.
    uint256 l2Number = startingOutputRoot.l2BlockNumber + disputedPos.traceIndex(SPLIT_DEPTH) + 1;
    oracle.loadLocalData(_ident, uuid.raw(), bytes32(l2Number << 0xC0), 8, _partOffset);
} else if (_ident == LocalPreimageKey.CHAIN_ID) {
    // Load the chain ID as a big-endian uint64 in the high order 8 bytes of the word.
    oracle.loadLocalData(_ident, uuid.raw(), bytes32(L2_CHAIN_ID << 0xC0), 8, _partOffset);
} else {
    revert InvalidLocalIdent();
}

The global block number is never passed. The VM is only concerned with the disputed block number, which it calculates from the anchor block number and the trace index of the disputed position. That's insufficient, because the traceIndex() calculates on an assumed block number according to the game depth.
This has been also separately verified with the sponsor, to ensure that no extra details were missed.

The attack path is therefore:

  • Propose a valid root for the blockNumber which the VM sees (anchor number + 1 + 2^SPLIT_DEPTH)
  • Attach a max value for l2BlockNumber
  • After the game is won, resolve it
  • All future games are guaranteed to not update the anchor root.

Impact

Anyone can freeze future withdrawals and any L2->L1 messaging.

Code Snippet

Tool used

Manual Review

Recommendation

The l2BlockNumber passed should be santized to line with the block number calculated by the VM.

Duplicate of #90

@sherlock-admin2 sherlock-admin2 added the Sponsor Disputed The sponsor disputed this issue's validity label Apr 4, 2024
@github-actions github-actions bot added High A valid High severity issue Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label labels Apr 11, 2024
@sherlock-admin2 sherlock-admin2 added Sponsor Confirmed The sponsor acknowledged this issue is valid Won't Fix The sponsor confirmed this issue will not be fixed and removed Sponsor Disputed The sponsor disputed this issue's validity labels Apr 11, 2024
@sherlock-admin3 sherlock-admin3 changed the title Tart Ultraviolet Kitten - Anyone can freeze future withdrawals and any L2->L1 messaging due to mismatch between the VM-viewed block number and the user supplied block number. Trust - Anyone can freeze future withdrawals and any L2->L1 messaging due to mismatch between the VM-viewed block number and the user supplied block number. Apr 23, 2024
@sherlock-admin3 sherlock-admin3 added the Reward A payout will be made for this issue label Apr 23, 2024
@Evert0x Evert0x added Medium A valid Medium severity issue and removed High A valid High severity issue labels May 15, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label Medium A valid Medium severity issue Reward A payout will be made for this issue Sponsor Confirmed The sponsor acknowledged this issue is valid Won't Fix The sponsor confirmed this issue will not be fixed
Projects
None yet
Development

No branches or pull requests

3 participants