xanoutas/solidity-security-findings huggingface dataset:
Winning Challenges Can Be Stolen 
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#L419


# Vulnerability details

## Impact

Background:

To summarize how bonds are payed in the dispute game, in a sub game, If the parent was not successfully countered, the game pays out the parent's bond to the claimant

And If the parent was successfully countered, pay out the parent's bond to the challenger.

This leads to a system where bonds are moved along a chain of subgames, and only the final subgame bonder receives their bonded stake and the bond of the previous challenger 

You can see this system by looking at the lines of code where `_distributeBond` is called

1)

 ```
 // 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);
```

2)
 ```
   if (challengeIndicesLen == 0 && _claimIndex != 0) {
            // In the event that the parent claim is at the max depth, there will always be 0 subgames. If the
            // `counteredBy` field is set and there are no subgames, this implies that the parent claim was successfully
            // stepped against. In this case, we pay out the bond to the party that stepped against the parent claim.
            // Otherwise, the parent claim is uncontested, and the bond is returned to the claimant.
            address counteredBy = subgameRootClaim.counteredBy;
            address recipient = counteredBy == address(0) ? subgameRootClaim.claimant : counteredBy;
            _distributeBond(recipient, subgameRootClaim);
            resolvedSubgames[_claimIndex] = true;
            return;
        }
```

In the FaultDisputeGame, when a winning challenge is posted and the winner is set to receive the bond payment, an attacker can take this bond payment by challenging twice.

```
    /// @inheritdoc IFaultDisputeGame
    function attack(Claim _disputed, uint256 _parentIndex, Claim _claim) external payable {
        move(_disputed, _parentIndex, _claim, true);
    }

    /// @inheritdoc IFaultDisputeGame
    function defend(Claim _disputed, uint256 _parentIndex, Claim _claim) external payable {
        move(_disputed, _parentIndex, _claim, false);
    }
```


For example, the defender is the winning party. An attacker can challenge the defender, then defend against their own challenge, winning the bond from the original defender.

The chain of bond movements is like

`winningClaim` -> `attack` -> `defend` -> Win

This will lead to theft of bond, also, if another attacker wants to attack the original attacker, due to game theory, the dispute game will always reach maximum depth.


## Proof of Concept


## Tools Used

Manual

## Recommended Mitigation Steps


Need to log which way the
Losing Defender Can Save Bond By Challenging Themselves
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#L652
https://github.com/code-423n4/2024-07-optimism/blob/70556044e5e080930f686c4e5acde420104bb2c4/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L587


# Vulnerability details

## Impact

Not the same as the known issue 

```
If a defender is going to lose the game, they can call challengeRootL2Block() on themselves, and they will get priority and take their own top level bond payout, instead of paying them to the real challenger.

```

Background:

To summarize how bonds are payed in the dispute game, in a sub game, If the parent was not successfully countered, the game pays out the parent's bond to the claimant

And If the parent was successfully countered, pay out the parent's bond to the challenger.

This leads to a system where bonds are moved along a chain of subgames, and only the final subgame bonder receives their bonded stake and the bond of the previous challenger 

You can see this system by looking at the lines of code where `_distributeBond` is called

1)

 ```
 // 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);
```

2)
 ```
   if (challengeIndicesLen == 0 && _claimIndex != 0) {
            // In the event that the parent claim is at the max depth, there will always be 0 subgames. If the
            // `counteredBy` field is set and there are no subgames, this implies that the parent claim was successfully
            // stepped against. In this case, we pay out the bond to the party that stepped against the parent claim.
            // Otherwise, the parent claim is uncontested, and the bond is returned to the claimant.
            address counteredBy = subgameRootClaim.counteredBy;
            address recipient = counteredBy == address(0) ? subgameRootClaim.claimant : counteredBy;
            _distributeBond(recipient, subgameRootClaim);
            resolvedSubgames[_claimIndex] = true;
            return;
        }
```


In this method of attack, if a defender knows they are going to lose, they can simply call attack on their false claim themselves and save their bond.

```
    function attack(Claim _disputed, uint256 _parentIndex, Claim _claim) external payable {
        move(_disputed, _parentIndex, _claim, true);
    }
```
This is because for sub-gemes, when it comes time to distribute, if there is a counter, the bond is sent to the counter in `resolveClaim`

So a defender when know they are going to lose, there can just counter themselves


## Proof of Concept


## Tools Used

Manual

## Recommended Mitigation Steps

Perhaps have a system where a log which way the game went. depending on
Fraudulent Proofs and Incorrect Resolution in `challengeRootL2Block()`
Audit: 2024-07-optimism-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

### Impact

**What is the loss?**
- An attacker can manipulate the [challengeRootL2Block()](https://github.com/code-423n4/2024-07-optimism/blob/main/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L492) function to submit fraudulent proofs, causing the dispute game to resolve incorrectly. This can lead to incorrect resolution, potentially allowing an attacker to claim bonds or other funds that should have been awarded to honest participants.

**How does this break the logic?**
- The game logic depends on integrity of the proofs provided in challenges. So played with the idea of crafting malicious proofs. By crafting a malicious proof, an attacker can create false validation or invalidation of a state transition, disrupting the dispute resolution mechanism.

**Does this lead to loss of funds?**
- Yes, as an attacker can claim bonds or other funds that should be awarded to the honest participants, leading to monetary loss and loss of trust in the dispute resolution process.

### Proof of Concept

In the `challengeRootL2Block` function:

```solidity
function challengeRootL2Block(
    Types.OutputRootProof calldata _outputRootProof,
    bytes calldata _headerRLP
) external {
    if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress();
    if (l2BlockNumberChallenged) revert L2BlockNumberChallenged();

    if (Hashing.hashOutputRootProof(_outputRootProof) != rootClaim().raw()) revert InvalidOutputRootProof();
    if (keccak256(_headerRLP) != _outputRootProof.latestBlockhash) revert InvalidHeaderRLP();

    RLPReader.RLPItem[] memory headerContents = RLPReader.readList(RLPReader.toRLPItem(_headerRLP));
    bytes memory rawBlockNumber = RLPReader.readBytes(headerContents[HEADER_BLOCK_NUMBER_INDEX]);

    if (rawBlockNumber.length > 32) revert InvalidHeaderRLP();

    uint256 blockNumber;
    assembly {
        blockNumber := shr(shl(0x03, sub(0x20, mload(rawBlockNumber))), mload(add(rawBlockNumber, 0x20)))
    }

    if (blockNumber == l2BlockNumber()) revert BlockNumberMatches();

    l2BlockNumberChallenger = msg.sender;
    l2BlockNumberChallenged = true;
}
```

#### Attack Vector 

**1. Initial Steps**:
- Alice initiates a dispute by providing a valid claim.
- Bob, the attacker, prepares to craft a false challenge.

**2. Malicious Proof**:
- Bob creates an `OutputRootProof` and an RLP-encoded block header with manipulated data that produce the expected hash.

**3. Malicious Data**:
- The `OutputRootProof` contains the expected `stateRoot`, `messagePasserStorageRoot`, and `latestBlockhash`. However, Bob crafts it in such a way that the `latestBlockhash` is valid but does not correspond to the actual state of the chain.

**4. Attack Mode**:
- Bob submits the crafted `OutputRootProof` and `headerRLP` to `challengeRootL2Block`.
- The function verifies the hashes but doe
Uncontrolled memory growth in `handleSyscall()` 
Audit: 2024-07-optimism-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The [handleSyscall()](https://github.com/code-423n4/2024-07-optimism/blob/main/packages/contracts-bedrock/src/cannon/MIPS.sol#L151) function contains a vulnerability in the current implementation of the `mmap` syscall `(syscall number 4090)`. This function allows for uncontrolled memory allocation, with an attack vector leading to potential DOS or excessive gas consumption.
 
Steps to Exploit:

- Initiate multiple ``mmap`` syscalls with large a1 values.
- Each call increases the `state.heap` without limit.

It can lead to significant resource waste, causing transactions to fail and thereby potentially disrupting the system's overall functionality.

## Proof of Concept
Vulnerable code here, from [handleSyscall()](https://github.com/code-423n4/2024-07-optimism/blob/main/packages/contracts-bedrock/src/cannon/MIPS.sol#L151): 

```solidity
if (syscall_no == 4090) {
    uint32 sz = a1;
    if (sz & 4095 != 0) {
        // adjust size to align with page size
        sz += 4096 - (sz & 4095);
    }
    if (a0 == 0) {
        v0 = state.heap;
        state.heap += sz;
    } else {
        v0 = a0;
    }
}
```

Above code allows for arbitrary memory allocation without upper bound checks. This enables an attacker being able to exploit this, by repeatedly calling the `mmap` syscall with large sizes, causing the `state.heap` to grow indefinitely and way too much.

Foundry PoC that should pass on your end, proving this vulnerability:

```solidity
    function testMemoryGrowth() public {
        // Create a state with syscall number 4090 (mmap)
        bytes memory stateData = abi.encodePacked(
            bytes32(0), // memRoot
            bytes32(0), // preimageKey
            uint32(0),  // preimageOffset
            uint32(0),  // pc
            uint32(4),  // nextPC
            uint32(0),  // lo
            uint32(0),  // hi
            uint32(0),  // heap
            uint8(0),   // exitCode
            false,      // exited
            uint64(0),  // step
            uint32(4090), // syscall number in $v0 (register 2)
            uint32(0),    // $a0 (register 4)
            uint32(1 << 30), // $a1 (register 5) - request 1GB of memory
            [uint32(0), uint32(0), uint32(0), uint32(0), // registers 0-3
             uint32(0), uint32(0), uint32(0), uint32(0), // registers 6-9
             uint32(0), uint32(0), uint32(0), uint32(0),
             uint32(0), uint32(0), uint32(0), uint32(0),
             uint32(0), uint32(0), uint32(0), uint32(0),
             uint32(0), uint32(0), uint32(0), uint32(0),
             uint32(0), uint32(0), uint32(0), uint32(0)]
        );

        bytes memory proof = new bytes(28 * 32); // Empty proof

        // Call step function multiple times
        for (uint i = 0; i < 5; i++) {
            bytes32 result = mips.step(stateData, proof, bytes32(0));
Game creator can create games whose L2 block cannot be challenged even when it contains absurd values
Audit: 2024-07-optimism-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Malicious Game creator can cause `challengeRootL2Block` to revert, which would prevent challenging the L2 block of games with obviously invalid values.

## Proof of Concept
Have a look at [FaultDisputeGame#challengeRootL2Block](https://github.com/code-423n4/2024-07-optimism/blob/main/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L516) function:
```solidity
function challengeRootL2Block(
    Types.OutputRootProof calldata _outputRootProof,
    bytes calldata _headerRLP
)
    external
{
    // INVARIANT: Moves cannot be made unless the game is currently in progress.
    if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress();

    // The root L2 block claim can only be challenged once.
    if (l2BlockNumberChallenged) revert L2BlockNumberChallenged();

    // Verify the output root preimage.
    if (Hashing.hashOutputRootProof(_outputRootProof) != rootClaim().raw()) revert InvalidOutputRootProof();

    // Verify the block hash preimage.
    if (keccak256(_headerRLP) != _outputRootProof.latestBlockhash) revert InvalidHeaderRLP();

    // Decode the header RLP to find the number of the block. In the consensus encoding, the timestamp
    // is the 9th element in the list that represents the block header.
    RLPReader.RLPItem[] memory headerContents = RLPReader.readList(RLPReader.toRLPItem(_headerRLP));
    bytes memory rawBlockNumber = RLPReader.readBytes(headerContents[HEADER_BLOCK_NUMBER_INDEX]);

    // Sanity check the block number string length.
+++ if (rawBlockNumber.length > 32) revert InvalidHeaderRLP();
    ...
}
```

The function reverts if `rawBlockNumber.length>32`. This allows the game creator to create games, inputting rootClaims whose block number cannot be challenged.

rootClaim is an hash of _outputRootProof
_outputRootProof.latestBlockHash is an hash of _headerRLP
_headerRLP is parsed into an array, then read to get the rawBlockNumber(which is at the 8th position in the array)

When constructing the root claim, malicious user can 
- set the rawBlockNumber to a >32 bytes value, store it in the _headerRLP
- hash the _headerRLP to give the _outputRootProof.latestBlockHash
- `hashOutputRootProof` the _outputRootProof to get the rootClaim.
- create a FDG with the rootClaim
- Now, noone will be able to challengeRootL2Block, even though the rawBlockNumber contained in it is an absurd value

## Tools Used
Manual Review

## Recommended Mitigation Steps
The sanity check for the block number string length should be done during game creation, not in the challengeRootL2Block function.


## Assessed type

Invalid Validation
Unconditional Success Flag Set for Store Conditional (SC) Instruction
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#L794-L796


# Vulnerability details

## Impact
* Incorrect emulation of atomic operations in MIPS programs.
* Potential race conditions or data inconsistencies in emulated MIPS programs that rely on proper LL/SC behavior.
* Inability to accurately implement certain synchronization primitives in the emulated MIPS environment.

## Proof of Concept
In the MIPS architecture, the Store Conditional (SC) instruction is part of the Load-Linked/Store-Conditional (LL/SC) pair used for atomic operations. The SC instruction should only succeed if no other store has occurred to the relevant memory location since the corresponding Load-Linked (LL) instruction.
The current implementation always sets the success flag to 1:

```//*@audit-info ------>>> set 1 only when success 
if (opcode == 0x38 && rtReg != 0) {
    state.registers[rtReg] = 1;
}
```

## Tools Used
manual view
## Recommended Mitigation Steps

To address this issue, consider the following recommendations:

1.Implement proper LL/SC semantics:

* Track the last LL address for each thread or process.
* Check if any store has occurred to that address since the LL instruction.
* Only set the success flag to 1 if no intervening store has occurred.


2.Modify the code to conditionally set the success flag:

```
if (opcode == 0x38 && rtReg != 0) {
    bool success = checkStoreConditionalSuccess(state);
    state.registers[rtReg] = success ? 1 : 0;
}
```

3.Implement the checkStoreConditionalSuccess function to properly verify the success condition of the SC instruction based on the LL/SC semantics.

4.Consider adding a mechanism to track and invalidate LL reservations when necessary (e.g., on certain system events or context switches).


## Assessed type

Context
An incorrect Claim can be treated as a correct one in a specific edge case.
Audit: 2024-07-optimism-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
When the Challenge reaches the `SPLIT_DEPTH` point, this means this block is the one we are disputing, if one of the challengers sees that the hash of that `claimData` for this block is invalid, he will attack it, or defend according to the situation and the suitable decession in the case.

The idea here is that if the block Claim Root is invalid, it should be treated as invalid when reading it from the `PreimageOracle`, where this claim (the claim at the disputed block) is written into the `PreimageOracle` and is read by the `VM` to verify if the block claim is correct or not.

[FaultDisputeGame.sol#L441-L447](https://github.com/code-423n4/2024-07-optimism/blob/main/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L441-L447)
```solidity
    function addLocalData(uint256 _ident, uint256 _execLeafIdx, uint256 _partOffset) external {
@>      (Claim starting, Position startingPos, Claim disputed, Position disputedPos) =
            _findStartingAndDisputedOutputs(_execLeafIdx);
        ...
        } 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 ( ... ) {
        ...
    }
```

We first get the Starting and the Disputed Blocks position, and the Claim for each, add them to the `PreimageOracle` where these values will get read by the `VM` to determine who is correct.

The problem is that the Claim Written to the Game should be the same state as the Claim written to the oracle (if it is invalid in FDG it should be invalid in VM and vice versa), But this is not true.

Since the Claim is `32` bytes size, and we have `8` bytes for the size variable in `PreimageOracle` we should start with offset `8` to read, but we can put offset value as `9`!

What this will do is that the Claim will get written as follows:
- if the claim in the `FDG` is `0x01FF...AABB`
- it will be `0xFF...AABB00`

In most cases, this will only affect the challenger as it will make his claim be invalid in most cases.

The Claim itself is the hashing of the State struct, and `_partOffset` is part of it. and each user calculates what will be the claim of that block state according to the inputdata.

> State Data type
```solidity
    struct State {
        bytes32 memRoot;
        bytes32 preimageKey;
@>      uint32 preimageOffset;
        uint32 pc;
        uint32 nextPC;
        uint32 lo;
        uint32 hi;
        uint32 heap;
        uint8 exitCode;
        bool exited;
        uint64 step;
        uint32[32] regist
Wrong disputed L2BlockNumber can get written into `PreimageOracle` using `_partOffset`
Audit: 2024-07-optimism-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
To achieve the result of the challenge, after we reached `MAX_GAME_DEPTH`, users add their data using `addLocalData`, and after adding they verify the execution using `FDG::step()`, where the user provides state and proof and this state contains the information about the key and offset that the VM will read it from `PreimageOracle` to validate the process.

[MIPS.sol#L27-L40](https://github.com/code-423n4/2024-07-optimism/blob/main/packages/contracts-bedrock/src/cannon/MIPS.sol#L27-L40)
```solidity
    struct State {
        bytes32 memRoot;
@>      bytes32 preimageKey;
@>      uint32 preimageOffset;
        uint32 pc;
        uint32 nextPC;
        uint32 lo;
        uint32 hi;
        uint32 heap;
        uint8 exitCode;
        bool exited;
        uint64 step;
        uint32[32] registers;
    }
```

[FaultDisputeGame.sol#L237](https://github.com/code-423n4/2024-07-optimism/blob/main/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L237)
```solidity
    function step(
        uint256 _claimIndex,
        bool _isAttack,
@>      bytes calldata _stateData,
        bytes calldata _proof
    )
```

`preimageKey` and `preimageOffset` will be used to read the data from `PreimageOracle` that was added by that user, and from what the user can add is `DISPUTED_L2_BLOCK_NUMBER`

[FaultDisputeGame.sol#L447-L455](https://github.com/code-423n4/2024-07-optimism/blob/main/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol#L447-L455)
```solidity
    function addLocalData(uint256 _ident, uint256 _execLeafIdx, uint256 _partOffset) external {
        ...
        } 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 ( ... ) {
    }
```

After this, the data is stored in the `PreimageOracle` using key and offset it can be read.

The problem here is that `_partOffset` is passed by the user itself, it is intended to be `8` as the beginning `8` Bytes are for storing the size of the data.

If we check `PreimageOracle::readPreimage()` which reads the values in the oracle, we will find that it determines the length and the data according to the offset.

[PreimageOracle.sol#L111-L113](https://github.com/code-423n4/2024-07-optimism/blob/main/packages/contracts-bedrock/src/cannon/PreimageOracle.sol#L111-L113)
```solidity
    function readPreimage(bytes32 _key, uint256 _offset) external