xanoutas/solidity-security-findings huggingface dataset:
Manipulating preimage length leads to ability to disprove or DoS VM step execution
Audit: 2024-07-optimism-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Ability to make honest party to loose dispute game. DoSing challenger or defender Cannon VM step.

## Proof of Concept
Cannon game implements MIPS VM on-chain, which is executed on the last step of execution trace bisection game. Because of that, some resource intensive operations like hashing whould be too costly to perform, hence in Optimism implementation VM requires data of the step to be put asynchronously via `PreimageOracle.loadLocalData()`. When loading preimages, user passes the size of the preimage, which is saved in storage:
```javascript
    function loadLocalData(
        uint256 _ident,
        bytes32 _localContext,
        bytes32 _word,
        uint256 _size, // @audit user defined value
        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.
        // @audit what if I pass correct data, but with 0 size?
@>      preimageLengths[key_] = _size;
    }
```

So, the same preimage and offset will have different sizes if called multiple times with different sizes. Additionally, from what can be seen in the code snippet above, the size is put as a part of the preimage at `_partOffset`. This is be problematic when reading the preimage:

```javascript
    function readPreimage(bytes32 _key, uint256 _offset) external view returns (bytes32 dat_, uint256 datLen_) {
        require(preimagePartOk[_key][_offset], "pre-image must exist");

        // Calculate the length of the pre-image data
        // Add 8 for the length-prefix part
        datLen_ = 32;
        uint256 length = preimageLengths[_key];
@>      if (_offset + 32 >= length + 8) {
@>          datLen_ = length + 8 - _offset;
        }

        //
Attacker can steal any amount from the L1 bridge by submitting a game for a future L2Block that doesn't exist yet
Audit: 2024-07-optimism-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The current challengeRootL2Block function prevents a situation where an attacker submits a valid game for a valid L2 block but with a fake l2BlockNumber as extraData. However, is doesn't prevent an attacker from submitting a game for a future L2Block number that doesn't exist yet on L2, with an output root hash that's based on a  fake L2 block header that includes the claimed L2Block number (preventing the challengeRootL2Block function from countering the claim). As the scenario below will show, the game logic offers no way for an honest challanger to invalidate such a game, enabling an attacker to successfully submit a fake root (for a far-in-the-future non-existant L2 block) setting the fake root to a value that enables them to proove a withdraw request of any amount.


## Proof of Concept

1. Assume the current L2 head block number is 1000, and the last trusted anchor in AnchorStateRegistry is for L2 block 990.
2. Attacker submits a game claiming to provide an output root for L2 block 100000. The submitted output root is based on the OutputRootProof struct:
```solidity
struct OutputRootProof {
    bytes32 version;
    bytes32 stateRoot;
    bytes32 messagePasserStorageRoot;
    bytes32 latestBlockhash;
    }
``` 
The attacker carefully selects the fields from which they produce the hash to be: StateRoot, messagePasserStorageRoot that can enable them to later prove a withdrawal of a very large amount. For latestBlockhash they set a random RLP encoded data with only their claimed block number (100000) at the HEADER_BLOCK_NUMBER_INDEX. They then produce the claim hash from these values. Note that the fact that latestBlockhash is fake will not prevent the attacker from proving withdrawals based on the game later on, since this value is not used or checked anywhere on the portal.  

3. Since the L1 has no way to verify the latest safe L2 block number, there is no check in DisputeGameFactory::create that the proposed block exists, and the game is created.  

4. Any honest challanger who disputes this game will be forced to use made-up values for counter claims. This is because the challanger program can only produce honest claims for existing L2 blocks. In our case, a challange to the claimed block will have to produce the output root of block 50000, which doesnt exist.

5. Note that the challanger program has an trace extention rule that deals with the fact that because the split height is fixed, the game always starts with very high indexes. The rule states that if a claim index implies a block higher than the proposed block, it's value is assumed to be the same as the proposed block. This rule however will not solve this situation, because the proposed block itself does not exist, which will cause the callanger program to fail in producin
if step is called in a game with multiple branches, the call will almost always revert 
Audit: 2024-07-optimism-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-optimism/blob/70556044e5e080930f686c4e5acde420104bb2c4/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L260-L279
https://github.com/code-423n4/2024-07-optimism/blob/70556044e5e080930f686c4e5acde420104bb2c4/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L904-L922
https://github.com/code-423n4/2024-07-optimism/blob/70556044e5e080930f686c4e5acde420104bb2c4/packages/contracts-bedrock/src/dispute/lib/LibPosition.sol#L156-L182


# Vulnerability details

## Impact
step calls to counter invalid claims would fail 
## Proof of Concept
the preclaim of an attack and post claim of a defence are  decided using trace ancestor of the parent with the parent index of the parent set as the bound set as the bound, it does so by calling traceIndexbounded with either the parent postion - 1 for attack or + 1 for defence 
assuming the entire game tree has a lot of branches which is very likely and would mean that several claims have been inserted into the tree(eg every claim so far has a attack and defence claim) then the upperbound is likely going to be very high seeing as though the upperbound is the index of a position at maxdepth -1 
for example in a game with a maxdepth of 8 and all upper branches being filled then is it possible for the upper bound to be a number like 30 
this would cause a revert when finding the trace index bounded as the depth would always be less than bounded 
## Tools Used
manual analysis








## Assessed type

Other
Unchallengeable false Exit claims in FaultDisputeGame
Audit: 2024-07-optimism-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-optimism/blob/e685f194269e3477c7e8ac25c27e3b5956c05360/packages/contracts-bedrock/src/cannon/MIPS.sol#L692-L701
https://github.com/code-423n4/2024-07-optimism/blob/e685f194269e3477c7e8ac25c27e3b5956c05360/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L302


# Vulnerability details

## Impact


A vulnerability in the FaultDisputeGame and MIPS contracts allows an attacker to inject a false exit claim that cannot be effectively challenged. The root cause is that the MIPS (VM) contract does not verify the Merkle proof of the state data when the state is marked as exited. Instead, it immediately returns the current state without any verification. This oversight allows an attacker to claim any arbitrary state as an "exited" state, bypassing crucial security checks.

*Note: attacker doesn't necesarily mean challenger*


- Steps:

1. An attacker make a claim about the execution reaching an exited state
2. When this claim is challenged, the MIPS contract sees the 'exited' flag and skips all verification, including the Merkle proof check.
```soldiity
  // Don't change state once exited
		if (state.exited) {
			return outputState();
		}
		.
		.
		.
		uint32 insn = readMem(state.pc, 0); => merkle proof check is in readMem
```
3. This makes the false claim unchallengeable, as the verification step that would normally detect the falsehood is bypassed.

This vulnerability can lead to:

1. Acceptance of invalid state transitions in the L2 system.
2. Rejection of valid state transitions.
3. Manipulation of the dispute resolution mechanism.
4. Economic losses for honest participants who cannot successfully challenge false claims.



## Proof of Concept

This test shows that an attacker can inject a false exit claim, which cannot be challenged due to the `step` function's behavior with exited states. The game resolves in favor of the defender (attacker) despite the false claim.

Here is the test:

```solidity

function test_step_exitedStateExploit() public {
    // Give the test contract some ether
    vm.deal(address(this), 1000 ether);

    // Make claims all the way down to the maximum depth
    uint256 maxDepth = gameProxy.maxGameDepth();
    for (uint256 i = 0; i < maxDepth; i++) {
        uint256 bond = _getRequiredBond(i);
        (,,,, Claim disputed,,) = gameProxy.claimData(i);
        if (i == gameProxy.splitDepth()) {
            // At split depth, use a false exit claim
            Claim falseExitClaim = _changeClaimStatus(_dummyClaim(), VMStatuses.INVALID);
            gameProxy.attack{ value: bond }(disputed, i, falseExitClaim);
        } else {
            gameProxy.attack{ value: bond }(disputed, i, _dummyClaim());
        }
    }

    // Prepare state data for the step function
    // This represents an already exited state
    bytes memory exitedStateData = abi.encode(
        bytes32(0),  // memRoot
        bytes32(0),  // preimageKey
        uint32(0),   // preimageOffset
`CLOCK_EXTENSION` logic does not account for LPP `CHALLENGE_PERIOD`
Audit: 2024-07-optimism-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-optimism/blob/70556044e5e080930f686c4e5acde420104bb2c4/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L371-L382
https://github.com/code-423n4/2024-07-optimism/blob/70556044e5e080930f686c4e5acde420104bb2c4/packages/contracts-bedrock/src/cannon/PreimageOracle.sol#L656-L657


# Vulnerability details


## Impact

In the `FaultDisputeGame` contract, the `CLOCK_EXTENSION` variable is the minimum amount of time a player has to respond to any follow-up claims. On OP mainnet, this time is currently set to 3 hours.

While this is helpful for honest users who might run low on time, it is especially important for addressing "freeloader claims." These are malicious claims that don't attempt to change the game's outcome, but aim to reclaim bond payments by remaining unchallenged. As described in [the OP docs](https://specs.optimism.io/fault-proof/stage-one/fault-dispute-game.html#freeloader-claims), "the honest challenger must always counter freeloader claims for incentive compatibility to be preserved." Since countering a freeloader claim means inheriting the adversary's clock, honest users must be capable of relying on just the `CLOCK_EXTENSION` timeline, even if they always act immediately.

In addition to the `CLOCK_EXTENSION`, the `PreimageOracle` contract introduces another important variable, the `CHALLENGE_PERIOD`. This variable is the minimum waiting time for potential challenges before a large preimage proposal (LPP) can be confirmed. On OP mainnet, this time is currently set to 24 hours.

When considered together, these two mechanisms can create a problem. An honest user is expected to always be capable of responding to claims within 3 hours, but may also need to wait 24 hours before a necessary LPP can be confirmed for a `step()` resolution. There are two potential counterarguments to this issue:

1. The `loadKeccak256PreimagePart()` function provides an alternative method of proving keccak256 preimages without requiring the `CHALLENGE_PERIOD` wait. However, it can be shown that the op-program can request preimages large enough that `loadKeccak256PreimagePart()` would exceed the L1 block gas limit. For example, [notice that op-program can request transaction receipt hash preimages](https://github.com/code-423n4/2024-07-optimism/blob/70556044e5e080930f686c4e5acde420104bb2c4/op-program/client/l1/oracle.go#L81-L97), which can have an extremely large size for transactions that log a lot of data.


2. If an honest user anticipates that an LPP may be needed, they can initiate the LPP before the final `step()`. However, this isn't currently implemented in op-challenger, [which only considers `OracleData` if the next move is a `step()`](https://github.com/code-423n4/2024-07-optimism/blob/70556044e5e080930f686c4e5acde420104bb2c4/op-challenger/game/fault/solver/game_solver.go#L57-L115). Moreover, multiple possible LPP preimages could be reachable within 24 hours of the game ending, making it
`MIPS` - `RDHWR`(opcode 0x1F, function code 0x3B) instruction is not handled
Audit: 2024-07-optimism-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Incorrect resolution of dispute game - False claims can be resolved as True

## Proof of Concept
Every Go routine(program) starts with a small stack size and whenever more stack is required, there is an action taken to increase the stack size.
This is handled by all syscall wrapper functions have this check at the beginning of the function.

For example, here's how it looks like for `syscall_init`:
```mips
.text:000C8D0C                             .globl syscall_init
.text:000C8D0C             syscall_init:
.text:000C8D0C 8F C1 00 08                 lw      $at, 8($fp)
.text:000C8D10 00 3D 08 2B                 sltu    $at, $sp
.text:000C8D14 14 20 00 06                 bnez    $at, loc_C8D30
.text:000C8D18 00 00 00 00                 nop
.text:000C8D1C 00 1F 18 25                 move    $v1, $ra
.text:000C8D20 0C 02 D6 90                 jal     runtime_morestack_noctxt
.text:000C8D24 00 00 00 00                 nop
.text:000C8D28 10 00 FF F8                 b       syscall_init
.text:000C8D2C 00 00 00 00                 nop
.text:000C8D30              # ---------------------------------------------------------------------------
.text:000C8D30
.text:000C8D30             loc_C8D30:
.text:000C8D30              # Actual logic goes here
                                            ...
```
As shown in the code snippet, if it requires more stack it calls `runtime_morestack` or `runtime_morestack_noctxt` function to increase size of the stack.

And the `runtime_morestack`/`runtime_morestack_noctxt` functions include logic uses `RDHWR` instruction that stands for `Read Hardware Register`, as shown below:
```mips
.text:000B8ACC                             .globl runtime_save_g
.text:000B8ACC             runtime_save_g:
.text:000B8ACC 3C 17 02 1E+                lb      $s7, runtime_iscgo
.text:000B8AD4 12 E0 00 05                 beqz    $s7, locret_B8AEC
.text:000B8AD8 00 00 00 00                 nop
.text:000B8ADC 00 03 B8 25                 move    $s7, $v1
.text:000B8AE0 7C 03 E8 3B                 rdhwr   $v1, $29         # <------ Here
.text:000B8AE4 AC 7E 90 00                 sw      $fp, -0x7000($v1)
.text:000B8AE8 00 17 18 25                 move    $v1, $s7
.text:000B8AEC
.text:000B8AEC             locret_B8AEC:
.text:000B8AEC 03 E0 00 08                 jr      $ra
.text:000B8AF0 00 00 00 00                 nop
.text:000B8AF0              # End of function runtime_save_g
```
Specifically, it is used with first argument as 29, which means to read local user storage data which is stored in hardware register.

Since this instruction is not handled in `MIPS` VM, the step for this instruction can't succeed because it reverts.
As a result, the invalid leaf couldn't be countered which makes it valid.

## Tools Used
Manual Review

## Recommended Mitigati
Improper proof validation causes incorrect resolution of step
Audit: 2024-07-optimism-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Incorrect resolution of dispute game - True claim can be resolved as False

## Proof of Concept
When stepping, `proof` data that represents VM's memory is provided along with data.
Each slot takes 28 * 32bytes of data, 32bytes for data(leaf) and 27 * 32bytes for merkle proof.

The slot0 of proof indicates the PC(Program Counter) of the VM.
There might exist slot1 based on the opcode. If the opcode is related to memory load, slot0 represents the memory to load.
Or if the opcode is related to memory write, slot1 represents the memory to overwrite.

The validity of data of slot0 and slot1 are guaranteed my `memRoot` field of stateData which is validated.
However, even though slot0 and slot1 satisfy `memRoot`, it does not guarantee if the memory data is correct.

For example, here's an attack scenario, where an attacker can mess up with memory to make invalid step.

1. Assume the root claim is valid.
2. A malicious attacker finds a random block and get one memory read instruction leaf.
3. It's assumed that the instruction leaf is the right child.
4. The attacker makes a defend step at the end.
5. The attacker passes slot0 data into slot1 data(or get any other memory and its proof that satisfies `memRoot`).
6. Since the slot1 data is incorrect, calculated `postState` can't be same as assumed `postState`.
7. As a result, the attacker can count the leaf and make the opposite resolution of the game

## Tools Used
Manual Review

## Recommended Mitigation Steps
When constructing memory slots, address of memory should be put together with memory data to construct hash of the leaves, so that when `readMem` is called, it should validate if `slot1` has correct address data.

```diff
    function readMem(uint32 _addr, uint8 _proofIndex) internal pure returns (uint32 out_) {
        unchecked {
            // Compute the offset of the proof in the calldata.
            uint256 offset = proofOffset(_proofIndex);

            assembly {
                // Validate the address alignement.
                if and(_addr, 3) { revert(0, 0) }

                // Load the leaf value.
                let leaf := calldataload(offset)
                offset := add(offset, 32)

+               // Load the memory address
+               let memAddr = calldataload(oofset)
+               offset := add(offset, 32)
+
+               let addrStart = _addr & 31; // Start of 32bytes memory
+               if iszero(eq(addrStart, memAddr)) { revert(0, 0) }

                // Convenience function to hash two nodes together in scratch space.
                function hashPair(a, b) -> h {
                    mstore(0, a)
                    mstore(32, b)
                    h := keccak256(0, 64)
                }

+               leaf := hashPair(leaf, memAddr)
`MIPS` - No syscall handling for `mmap2`(4210)
Audit: 2024-07-optimism-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Incorrect resolution of dispute game - True claim will be resolved as False

## Proof of Concept
In `MIPS` vm, `mmap` syscall is handled by checking syscall no. 4090.
However, `mmap2` syscall(4210) is usually used allocate/map memories.

Here's code snippet of low-level function `syscall_mmap`.
(The assembly below is generated from ELF file built from `op-program-client`)

```mips
.text:000CBAB4                             .globl syscall_mmap
.text:000CBAB4             syscall_mmap:
.text:000CBAB4 8F C1 00 08                 lw      $at, 8($fp)
.text:000CBAB8 00 3D 08 2B                 sltu    $at, $sp
.text:000CBABC 14 20 00 06                 bnez    $at, loc_CBAD8
.text:000CBAC0 00 00 00 00                 nop
.text:000CBAC4 00 1F 18 25                 move    $v1, $ra
.text:000CBAC8 0C 02 D6 90                 jal     runtime_morestack_noctxt
.text:000CBACC 00 00 00 00                 nop
.text:000CBAD0 10 00 FF F8                 b       syscall_mmap
.text:000CBAD4 00 00 00 00                 nop
.text:000CBAD8              # ---------------------------------------------------------------------------
.text:000CBAD8
.text:000CBAD8             loc_CBAD8:
                            .....
                            .....
.text:000CBB50 00 00 00 00                 nop
.text:000CBB54              # ---------------------------------------------------------------------------
.text:000CBB54
.text:000CBB54             loc_CBB54:
.text:000CBB54 8F A1 00 2C                 lw      $at, 0x2C($sp)
.text:000CBB58 AF A1 00 04                 sw      $at, 4($sp)
.text:000CBB5C 8F A1 00 30                 lw      $at, 0x30($sp)
.text:000CBB60 AF A1 00 08                 sw      $at, 8($sp)
.text:000CBB64 8F A1 00 34                 lw      $at, 0x34($sp)
.text:000CBB68 AF A1 00 0C                 sw      $at, 0xC($sp)
.text:000CBB6C 8F A1 00 38                 lw      $at, 0x38($sp)
.text:000CBB70 AF A1 00 10                 sw      $at, 0x10($sp)
.text:000CBB74 8F A1 00 3C                 lw      $at, 0x3C($sp)
.text:000CBB78 AF A1 00 14                 sw      $at, 0x14($sp)
.text:000CBB7C AF A2 00 18                 sw      $v0, 0x18($sp)
.text:000CBB80 0C 03 43 F8                 jal     syscall_mmap2    # <----------- mmap calls mmap2 syscall
.text:000CBB84 00 00 00 00                 nop
.text:000CBB88 8F A1 00 1C                 lw      $at, 0x1C($sp)
.text:000CBB8C 8F A2 00 20                 lw      $v0, 0x20($sp)
.text:000CBB90 8F A3 00 24                 lw      $v1, 0x24($sp)
.text:000CBB94 AF A1 00 48                 sw      $at, 0x48($sp)
.text:000CBB98 AF A2 00 4C                 sw      $v0, 0x4C($sp)
.text:000CBB9C AF A3 00 50                 sw      $v1, 0x50($sp)
.text:000CBBA0 8F BF 00 00                 lw      $ra, 0($sp)
.text:000CBBA4 27