xanoutas/solidity-security-findings huggingface dataset:
The invariant of not exceeding `MAX_CLOCK_DURATION` when placing moves is wrongly protected against
Audit: 2024-07-optimism-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-optimism/blob/2e11e15e7a9a86f90de090ebf9e3516279e30897/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L319-L416


# Vulnerability details


## Proof of Concept

Take a look at https://github.com/code-423n4/2024-07-optimism/blob/2e11e15e7a9a86f90de090ebf9e3516279e30897/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L319-L416

```solidity
    function move(Claim _disputed, uint256 _challengeIndex, Claim _claim, bool _isAttack) public payable virtual {
        // INVARIANT: Moves cannot be made unless the game is currently in progress.
        if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress();

        // Get the parent. If it does not exist, the call will revert with OOB.
        ClaimData memory parent = claimData[_challengeIndex];
..snip
        // When the next position surpasses the split depth (i.e., it is the root claim of an execution
        // trace bisection sub-game), we need to perform some extra verification steps.
        if (nextPositionDepth == SPLIT_DEPTH + 1) {
            _verifyExecBisectionRoot(_claim, _challengeIndex, parentPos, _isAttack);
        }

        // INVARIANT: The `msg.value` must exactly equal the required bond.
        if (getRequiredBond(nextPosition) != msg.value) revert IncorrectBondAmount();

        // Compute the duration of the next clock. This is done by adding the duration of the
        // grandparent claim to the difference between the current block timestamp and the
        // parent's clock timestamp.
        Duration nextDuration = getChallengerDuration(_challengeIndex);
        //@audit
        // INVARIANT: A move can never be made once its clock has exceeded `MAX_CLOCK_DURATION`
        //            seconds of time.
        if (nextDuration.raw() == MAX_CLOCK_DURATION.raw()) revert ClockTimeExceeded();

..snip

        // Deposit the bond.
        WETH.deposit{ value: msg.value }();

        // Emit the appropriate event for the attack or defense.
        emit Move(_challengeIndex, _claim, msg.sender);
    }
```

This function is used for both attack and defend moves, it includes multiple invariants one of which has been highlighted with the @audit tag above.

Issue is that the wrong check is applied for the invariant `==`, instead of `>=` which then means that the invariant would only hold when the  clock is exactly at `MAX_CLOCK_DURATION` and not when it has exceeded `MAX_CLOCK_DURATION` as is expected/documented

## Impact
Core invariant is broken, considering _moves_  can now be made even if the clock has exceeded `MAX_CLOCK_DURATION`.
## Recommended Mitigation Steps
Consider applying these changes:
```diff
    function move(Claim _disputed, uint256 _challengeIndex, Claim _claim, bool _isAttack) public payable virtual {
        // INVARIANT: Moves cannot be made unless the game is currently in progress.
        if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress();

        // Get the
Integer Overflow and Underflow Vulnerabilities in MIPS Add Instruction Emulation
Audit: 2024-07-optimism-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-optimism/blob/70556044e5e080930f686c4e5acde420104bb2c4/packages/contracts-bedrock/src/cannon/MIPS.sol#L920-L933


# Vulnerability details

## Impact
Silent integer overflows and underflows leading to incorrect calculation results.

## Proof of Concept
In the execute function, the add instruction (func 0x20) is implemented as follows:
```
// add
//*@audit-info ------->>> could be overflow 
else if (func == 0x20) {
    return (rs + rt);
}
```

This implementation directly adds the two 32-bit unsigned integers rs and rt without checking for overflow or underflow. In MIPS architecture, the add instruction should trigger an exception on overflow, but this behavior is not replicated in the current implementation.
Moreover, the function is wrapped in an unchecked block, which means that even Solidity's built-in overflow checks (in versions 0.8.0 and above) are disabled.
While unsigned integers cannot underflow in the traditional sense, the lack of proper signed integer handling can lead to unexpected results when dealing with negative numbers in two's complement representation.

``` // sub
                else if (func == 0x22) {
                    return (rs - rt);
                }
```

## Tools Used
manual view
## Recommended Mitigation Steps
pls make sure to prevent underFlow and overflow


## Assessed type

Under/Overflow
Possible underflow if `CLOCK_EXTENSION` is greater than half the `MAX_CLOCK_DURATION`
Audit: 2024-07-optimism-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-optimism/blob/main/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L379-L381


# Vulnerability details

According to Natspec:

*If the remaining clock time has less than `CLOCK_EXTENSION` seconds remaining, grant the potential grandchild's clock `CLOCK_EXTENSION` seconds. This is to ensure that, even if a player has to inherit another team's clock to counter a freeloader claim, they will always have enough time to to respond. This extension is bounded by the depth of the tree. If the potential grandchild is an execution trace bisection root, the clock extension is doubled. This is to allow for extra time for the off-chain challenge agent to generate the initial instruction trace on the native FPVM.* 

```solidity
File: FaultDisputeGame.sol

377:        if (nextDuration.raw() > MAX_CLOCK_DURATION.raw() - CLOCK_EXTENSION.raw()) {
378:            // If the potential grandchild is an execution trace bisection root, double the clock extension.
379:            uint64 extensionPeriod =
380:                nextPositionDepth == SPLIT_DEPTH - 1 ? CLOCK_EXTENSION.raw() * 2 : CLOCK_EXTENSION.raw();
381:            nextDuration = Duration.wrap(MAX_CLOCK_DURATION.raw() - extensionPeriod);
382:        }
```
Here we can see when calculating the `extensionPeriod` that if the first condition is true, the clock extension is doubled.
In the very next line, it can been seen that `extensionPeriod` is subtracted from the `MAX_CLOCK_DURATION` inside the `wrap` function.

However if the value of `CLOCK_EXTENSION.raw()` is greater than half the value of  `MAX_CLOCK_DURATION.raw()`, the calculation inside the `wrap()` underflows.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Add an additional check just in case to mitigate this error.

```solidity
File: FaultDisputeGame.sol

377:        if (nextDuration.raw() > MAX_CLOCK_DURATION.raw() - CLOCK_EXTENSION.raw()) {
378:            // If the potential grandchild is an execution trace bisection root, double the clock extension.
379:            uint64 extensionPeriod =
380:                nextPositionDepth == SPLIT_DEPTH - 1 ? CLOCK_EXTENSION.raw() * 2 : CLOCK_EXTENSION.raw();
+ 381:          if (extensionPeriod > MAX_CLOCK_DURATION.raw()) {
+ 382:                  extensionPeriod = CLOCK_EXTENSION.raw();
+ 383:          }
384:            nextDuration = Duration.wrap(MAX_CLOCK_DURATION.raw() - extensionPeriod);
385:        }
```


## Assessed type

Under/Overflow
Invalid claims at `MAX_GAME_DEPTH` cannot always be disproven with a `step`
Audit: 2024-07-optimism-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-optimism/blob/70556044e5e080930f686c4e5acde420104bb2c4/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L302-L304


# Vulnerability details

## Description
In the fault dispute game, if there is no winner until the game is at `MAX_GAME_DEPTH` there is one more possibility for someone to contest a claim which is doing a `step` in the `MIPS VM`. For this, the attacker/defender needs to provide the `_stateData` for the claim which needs to match with the one specified in the to be contested claim (done by comparing the hash). In order for the game to decide whether the step is a valid attack/defense, it compares the result with a `postState` to check whether the execution resulted in the alleged result:

`FaultDisputeGame::step()`:
```solidity
// [...]
bool validStep = VM.step(_stateData, _proof, uuid.raw()) == postState.claim.raw();
bool parentPostAgree = (parentPos.depth() - postState.position.depth()) % 2 == 0;
if (parentPostAgree == validStep) revert ValidStep();
// [...]
parent.counteredBy = msg.sender; // [1] <----
```

Now the problem is, that the `VM` will gladly revert if it encounters an error such as division by zero. If now the VM reverts instead of returning a state, it is impossible to prove this claim as incorrect as the `step` would need to return successfully with a certain state to prove that the claim is wrong.
More concretely, the issue is, that `FaultDisputeGame::step()` does not handle the case where the call to `VM.step()` reverts.


## Impact
The result of this, is that an incorrect claim cannot be proven to be incorrect, leading to an incorrect game result breaking the invariant that `Dishonest actors should never be able to win the Fault Dispute Game.`


## Proof of Concept

The already existing test `test_div_byZero_fails` shows, that the `VM` will revert if there is an error:

```solidity
function test_div_byZero_fails() external {
    uint32 insn = encodespec(0x9, 0xa, 0x0, 0x1a); // div t1, t2
    (MIPS.State memory state, bytes memory proof) = constructMIPSState(0, insn, 0x4, 0);
    state.registers[9] = 5; // t1
    state.registers[10] = 0; // t2

    vm.expectRevert("MIPS: division by zero");
    mips.step(encodeState(state), proof, 0);
}
```

As pointed out above, in `FaultDisputeGame::step()`, `VM.step()` is called, meaning that if it reverts, the whole call will revert, making it impossible to get to `[1]` where the claim gets set to be `countered`.

Putting this together, a dishonest user can do the following:
* Start a dispute game with a `rootClaim` that contains one invalid `VM` instruction further down the line
* Honest challengers will try to counter it, therefore playing the game as-intended
* Once the game comes to `MAX_GAME_DEPTH`, the dishonest user can make a claim based on the one faulty state transition
* The honest challenger now executes `step`, trying to show that the state transition is incorrect. In a game where the state transitio
No checks for zero size in `loadLocalData`
Audit: 2024-07-optimism-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-optimism/blob/70556044e5e080930f686c4e5acde420104bb2c4/packages/contracts-bedrock/src/cannon/PreimageOracle.sol#L134


# Vulnerability details

## Description
`loadLocalData` in `PreimageOracle` is used to provide local data to the `MIPS VM`. It is one of multiple functions allowing in the `PreimageOracle` enabling storage of data. One of those is `addLeavesLPP` in combination with some more. In `addLeavesLPP` there is a check whether the `claimedSize` (final size) of the content is equal to zero. This is not allowed as we do not want to store zero-size data in the oracle. The problem is, that this check is missing in `loadLocalData` allowing a user to add zero-size content to the oracle.

`loadLocalData`:
```solidity
function loadLocalData(
    uint256 _ident,
    bytes32 _localContext,
    bytes32 _word,
    uint256 _size,
    uint256 _partOffset
)
    external
    returns (bytes32 key_)
{
    // Compute the localized key from the given local identifier.
    key_ = PreimageKeyLib.localizeIdent(_ident, _localContext);

    // Revert if the given part offset is not within bounds.
    if (_partOffset > _size + 8 || _size > 32) {
        revert PartOffsetOOB();
    }

    // Prepare the local data part at the given offset
    bytes32 part;
    assembly {
        // Clean the memory in [0x20, 0x40)
        mstore(0x20, 0x00)

        // Store the full local data in scratch space.
        mstore(0x00, shl(192, _size))
        mstore(0x08, _word)

        // Prepare the local data part at the requested offset.
        part := mload(_partOffset)
    }

    // Store the first part with `_partOffset`.
    preimagePartOk[key_][_partOffset] = true;
    preimageParts[key_][_partOffset] = part;
    // Assign the length of the preimage at the localized key.
    preimageLengths[key_] = _size;
}
```
We can see, that there is no check for `_size == 0`.


## Impact
This results in users being able to add zero-size data which should not be allowed according to this comment in `addLeavesLPP`: `0-size preimages are *not* allowed.`.


## Proof of Concept
In order to execute the PoC, please add it to the tests in `PreimageOracle.t.sol` and execute it with `forge test --match-test test_loadLocalData_zeroSize_succeeds -vv`.

```solidity
function test_loadLocalData_zeroSize_succeeds() public {
    bytes32 word = hex"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";

    bytes32 contextKey = oracle.loadLocalData(0, 0, word, 0, 0);

    // Validate that the pre-image part is set
    bool ok = oracle.preimagePartOk(contextKey, 0);
    assertTrue(ok);
}
```

## Tools Used
Manual review


## Recommended Mitigation Steps
I would recommend changing the line checking whether the `_partOffset` is OOB to the following:

```solidity
if (_size == 0 || _partOffset > _size + 8 || _size > 32) {
    revert PartOffsetOOB();
}
```


## Assessed type

Invalid Validation
`squeezeLPP` and `loadKeccak256PreimagePart` use the same key-prefix possibly leading to content corruption
Audit: 2024-07-optimism-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-optimism/blob/70556044e5e080930f686c4e5acde420104bb2c4/packages/contracts-bedrock/src/cannon/PreimageOracle.sol#L682


# Vulnerability details

## Description
In the `PreimageOracle` and user can add data to it via multiple functions. Two of these are the `LPP` functions and `loadKeccak256PreimagePart`. Both of these use the same key-prefix of `0x02` as seen here:
`loadKeccak256PreimagePart`: `key := or(and(h, not(shl(248, 0xFF))), shl(248, 0x02))`  
`squeezeLPP`: `finalDigest := or(and(finalDigest, not(shl(248, 0xFF))), shl(248, 0x02))`

The key for `loadKeccak256PreimagePart` is based on the user-controlled arbitrarily long input:

```solidity
calldatacopy(ptr, _preimage.offset, size)
// Note that it includes the 8-byte big-endian uint64 length prefix.
// this will be zero-padded at the end, since memory at end is clean.
part := mload(add(sub(ptr, 0x08), _partOffset))
let h := keccak256(ptr, size) // compute preimage keccak256 hash
```

That means that if we know what the hash used by `squeezeLPP` has resulted from, we can possibly add a new entry with the calldata being that content, overwriting a valid `LPP` with different content.


## Impact
The impact of this is quite severe as a user could overwrite data of the oracle which is deemed to be trusted. It is breaking an invariant of the protocol, that noone can overwrite content at a certain key with different content mapped to that key.


## Tools Used
Manual review


## Recommended Mitigation Steps
I would recommend using a different key-prefix for `LPP`s, ensuring that the keys always differ by at least one byte.



## Assessed type

Other
Panic in MIPS VM Could Lead to Unchallengeable L2 Output Root Claim
Audit: 2024-07-optimism-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-optimism/blob/70556044e5e080930f686c4e5acde420104bb2c4/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L883-L895
https://github.com/code-423n4/2024-07-optimism/blob/main/op-node/rollup/derive/channel.go#L169-L205


# Vulnerability details

## Impact
Suppose we have a situation where the MIPS geth goes wrong and always panics. The vmStatus can only be either `UNFINISHED` or `PANIC`. 

In the current execution bisection game system, the `UNFINISHED` state can not be used as the root claim; the `PANIC` state can always be attacked. So we can counter every bisection game at `SPLIT_DEPTH + 1`; none of the claims at `SPLIT_DEPTH` are challenged. Inductively, claims agreeing with `SPLIT_DEPTH` are not challengable.

In the current configuration, `SPLIT_DEPTH` is set to 30, so we can deny any dispute against the root claim at depth 0. If that were to happen, the state of the L2 output root could be hijacked and unrecoverable.

It's unclear if there are easy methods to trigger panic in the MIPS VM.

One possible way is to leverage the privilege, as the batcher is currently controlled by a centralized trusted entity. Ordinary users cannot construct the batches and channels for the rollup, but the batcher can freely set the payload blob to L1.


https://github.com/code-423n4/2024-07-optimism/blob/main/op-node/rollup/derive/channel.go#L169-L205

A malicious batcher could compress a huge amount of zero blobs, upload them to L1, and force the MIPS VM to decompress them. Considering the limited memory resources of the MIPS VM and the absence of garbage collection in the MIPS go-ethereum, the VM could be very easily corrupted.

## Recommended Mitigation Steps
Set the split_depth to an odd number.


## Assessed type

DoS
In some cases, proper CLOCK_EXTENTSION time cannot be ensured to generate the initial instruciton trace
Audit: 2024-07-optimism-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-optimism/blob/70556044e5e080930f686c4e5acde420104bb2c4/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L377-L380


# Vulnerability details

## Impact
In some cases, a team will not be granted enough CLOCK_EXTENSION time to generate the initial instruction trace and perform a counter claim.

## Proof of Concept
In FaultDisputeGame::move, game clocks will grant extensions to ensure a team has enough time to counter a potential claim and/or generate an execution trace bisection root claim. A special case is when the potential grandchild is executiontrace bisection root claim ( SPLIT_DEPTH + 1), twice the extension time (`2 * CLOCK_EXTENSION.raw()`) should be ensured.

The problem is that the current implementation might still cause a team to have less than `2 * CLOCK_EXTENSION.raw()` time to generate an execution trace bisection root claim and perform a counterclaim. Specifically, `if (nextDuration.raw() > MAX_CLOCK_DURATION.raw() - CLOCK_EXTENSION.raw())` condition check is vulnerable.
```solidity
//packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol
  function move(
    Claim _disputed,
    uint256 _challengeIndex,
    Claim _claim,
    bool _isAttack
  ) public payable virtual {
...
|>      if (nextDuration.raw() > MAX_CLOCK_DURATION.raw() - CLOCK_EXTENSION.raw()) {
            // If the potential grandchild is an execution trace bisection root, double the clock extension.
            uint64 extensionPeriod =
                nextPositionDepth == SPLIT_DEPTH - 1 ? CLOCK_EXTENSION.raw() * 2 : CLOCK_EXTENSION.raw();
            nextDuration = Duration.wrap(MAX_CLOCK_DURATION.raw() - extensionPeriod);
        }
...
```
(https://github.com/code-423n4/2024-07-optimism/blob/70556044e5e080930f686c4e5acde420104bb2c4/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L377-L380)

Suppose CLOCK_EXTENSION.raw() is 3 hours, MAX_CLOCK_DURATION.raw() is 84 hours (3.5 days).

**Case A: At time of move(), nextDuration.raw() is 82 hours. nextPosition is at SPLIT_DEPTH -1.**

nextDuration.raw() > MAX_CLOCK_DURATION.raw() - CLOCK_EXTENSION.raw(). The grandchild is at SPLIT_DEPTH + 1 (execution trace root).

extensionPeriod: 6h (CLOCK_EXTENSION.raw() * 2)
nextDuration: 78h (MAX_CLOCK_DURATION.raw() - CLOCK_EXTENSION.raw() * 2)

**Case B: At time of move(), nextDuration.raw() is 81 hours. nextPosition is at SPLIT_DEPTH -1.**

B-1:
nextDuration.raw() == MAX_CLOCK_DURATION.raw() - CLOCK_EXTENSION.raw(). The grandchild is at SPLIT_DEPTH + 1 (execution trace root).

The if condition will be bypassed.

extensionPeriod: 0h
nextDuration: 81h

B-2:
The oppposing team attack. 

B-3: 
The grandchild now has max. 3h to generate execution trace root claim and perform an counter claim. This is half of the intended `CLOCK_EXTENSION.raw() * 2`.

As seen in caseB, in normal conditions, a team that need to create both executiontrace root claim and perform a counter claim may not get the minimum `CLOCK_E
Improper resolution for block number challenge of the root claim
Audit: 2024-07-optimism-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-optimism/blob/70556044e5e080930f686c4e5acde420104bb2c4/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L643


# Vulnerability details

## Impact
A malicious attacker can create dispute games with invalid claims without any loss

## Proof of Concept
When all subgames are resolved and it comes to the root claim resolution, it checks if l2 block number is challenged and if so, set l2 block number challenger as countered address regardless of children resolution.
Abusing this issue, a malicious attacker can create dispute games with incorrect root claim without any loss.

Here's the scenario how the attacker doesn't lose for creating invalid games.

1. A malicious attacker creates a dispute game with incorrect root claim, also with incorrect l2 block number.
2. Since the root claim is not correct, there will be honest challengers who attack the root claim.
3. The attacker calls `challengeRootL2BLock` and set himself as challenger.
4. When subgames are resolved and it comes to root claim, the challenger will be set to attacker, not honest challenger.

## Tools Used
Manual Review

## Recommended Mitigation Steps
When it's root claim, it should first validate if the root claim is invalid, and if it is invalid set attacker as countered address and dispute bond to the attacker. And if the root claim is not countered, it should check l2 block number challenger.

```diff
- if (_claimIndex == 0 && l2BlockNumberChallenged) {
+ if (_claimIndex == 0 && countered == address(0) && l2BlockNumberChallenged) {
    // Special case: If the root claim has been challenged with the `challengeRootL2Block` function,
    // the bond is always paid out to the issuer of that challenge.
    address challenger = l2BlockNumberChallenger;
    _distributeBond(challenger, subgameRootClaim);
    subgameRootClaim.counteredBy = challenger;
} else {
    // If the parent was not successfully countered, pay out the parent's bond to the claimant.
    // If the parent was successfully countered, pay out the parent's bond to the challenger.
    _distributeBond(countered == address(0) ? subgameRootClaim.claimant : countered, subgameRootClaim);

    // Once a subgame is resolved, we percolate the result up the DAG so subsequent calls to
    // resolveClaim will not need to traverse this subgame.
    subgameRootClaim.counteredBy = countered;
}
```



## Assessed type

Context
`MIPS` - Incorrect implementation of SRAV instruction
Audit: 2024-07-optimism-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-optimism/blob/70556044e5e080930f686c4e5acde420104bb2c4/packages/contracts-bedrock/src/cannon/MIPS.sol#L856-L858


# Vulnerability details

## Impact
Incorrect dispute game resolution - it will make True root claim to be False

## Proof of Concept
[MIPS-ISA](https://www.cs.cmu.edu/afs/cs/academic/class/15740-f97/public/doc/mips-isa.pdf): Page 153(A-141)

`SRAV` instruction does arithmetic shift right, the bit shift count is specified by the low-order five bits of `rs` register.

```solidity
else if (func == 0x07) {
    return SE(rt >> rs, 32 - rs);
}
```
However, in implementation it does not validate low-level five bits of `rs` register, but use it as a whole for shift.

As a result, if any of bits after 5th bit is one, the shift results will always be zero.

## Tools Used
Manual Review

## Recommended Mitigation Steps
As it did for other shifts, it should apply `& 0x1F` before shifting.
```solidity
else if (func == 0x07) {
    return SE(rt >> (rs & 0x1F), 32 - (rs - 0x1F));
}
```



## Assessed type

Context