xanoutas/solidity-security-findings huggingface dataset:
Edge from dishonest challenge edge tree can inherit timer from honest tree allowing confirmation of incorrect assertion
Audit: 2024-05-arbitrum-foundation-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/challengeV2/EdgeChallengeManager.sol#L505-L508
https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/challengeV2/libraries/EdgeChallengeManagerLib.sol#L520-L531
https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/challengeV2/libraries/EdgeChallengeManagerLib.sol#L689-L710


# Vulnerability details

## Impact
Timers can be inherited across different challenge trees and consequently incorrect assertions can be confirmed.

## Proof of Concept
The function `RollupUserLogic::updateTimerCacheByClaim` allows inheritance of timers between different levels of a challenge. It performs some validation on edge being inherited from in `checkClaimIdLink` (the claiming edge).
https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/challengeV2/libraries/EdgeChallengeManagerLib.sol#L689-L710
```solidity
    function checkClaimIdLink(EdgeStore storage store, bytes32 edgeId, bytes32 claimingEdgeId, uint8 numBigStepLevel)
        private
        view
    {
        // the origin id of an edge should be the mutual id of the edge in the level below
        if (store.edges[edgeId].mutualId() != store.edges[claimingEdgeId].originId) {
            revert OriginIdMutualIdMismatch(store.edges[edgeId].mutualId(), store.edges[claimingEdgeId].originId);
        }
        // the claiming edge must be exactly one level below
        if (nextEdgeLevel(store.edges[edgeId].level, numBigStepLevel) != store.edges[claimingEdgeId].level) {
            revert EdgeLevelInvalid(
                edgeId,
                claimingEdgeId,
                nextEdgeLevel(store.edges[edgeId].level, numBigStepLevel),
                store.edges[claimingEdgeId].level
            );
        }
    }
```
As per the comments, the claiming edge must be exactly one level below (ie. in the subchallenge directly after the inheriting edge) and its `originId` must match the `mutualId` of the inheriting edge. For clarification, we note that the inheriting edge must be a leaf edge in a challenge/subchallenge tree since the root edges of subchallenges (the layer zero edges) have `originId` derived from the `mutualId` of one of these leaf edges, and this `originId` is inherited by all its children which result from bisection.

Note that rival edges share the same `mutualId` by definition and since there isn't any extra validation, if a specific edge is a valid inheriting edge, all rivals will also be valid inheriting edges. This means rivals belonging to dishonest challenge edge trees will also be able to inherit from the timer of edges in the honest tree. Consequently, if an honest edge accumulates sufficient unrivalled time for confirmation, a malicious actor can frontrun the confirmation of the honest challenge tree to confirm t
Inconsistent sequencer unexpected delay in DelayBuffer may harm users calling forceInclusion()
Audit: 2024-05-arbitrum-foundation-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/bridge/DelayBuffer.sol#L43
https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/bridge/DelayBuffer.sol#L90-L98
https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/bridge/SequencerInbox.sol#L287


# Vulnerability details

## Impact

Buffer unepected delay due to sequencer outage is inconsistent.

## Proof of Concept

When the sequencer is down, users may call `SequencerInbox::forceInclusion()` to get their message added to the inbox `sequencerInboxAccs`. However, there is an incosistency when the sequencer has been down and there is more than 1 message, or even if just 1 message.

The `DelayBuffer` is used to dynamically adjust how much delay a user has to wait to call `SequencerInbox::forceInclusion()`. The buffer increase mechanism is not relevant for this issue. 

The buffer decrease consists of subtracting the last time the buffer was updated, `self.prevSequencedBlockNumber`, by the previous block number of the last delay buffer update, `self.prevBlockNumber`. This is done this way to ensure that the sequencer delay can not be double counted, as the delayed inbox may have more than 1 delayed message.

However, the approach taken as a way of protecting the sequencer and not depleting the buffer incorrectly as the drawback that it also means that the buffer will not always be decreased.

For example, if the sequencer is working at a block number A, a message is submited at block number A + 10 and another one at block number A + 110. If the sequencer is down, the user has to wait, for example, delay blocks of 100 (or the delay buffer, depending on the [smallest](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/bridge/SequencerInbox.sol#L843-L845)). If 200 blocks have passed since A + 10 (the first message), both delayed messages may be force included, at block number A  + 210.

The discrepancy is that, depending on how the delayed messages are included, the buffer delay will be reduced differently. If both messages are removed at once, by calling `SequencerInbox::forceInclusion()` with the id of the newest message, the buffer delay will not be decreased, as `self.prevBlockNumber` is A, the same as `self.prevSequencedBlockNumber`. This would harm users that want to force included messages as the buffer would be bigger and they would have to wait more time for their message to be force included.

If the oldest message is first force included, the buffer delay will not decreased, as above, but `self.prevSequencedBlockNumber` will be updated to A + 210 and `self.prevBlockNumber` to A + 10. Then, if the second message is force included, `self.prevSequencedBlockNumber - self.prevBlockNumber == A + 210 - (A + 10) == 200`, which means that the buffer would correctly decrease as the sequencer was offline (ignoring the fact that there is a threshold, but the issue is the same as long as the delay is bigger t
BOLDUpgradeAction::cleanupOldRollup will revert whenever there is more than a single staker to refund
Audit: 2024-05-arbitrum-foundation-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/BOLDUpgradeAction.sol#L350-L359


# Vulnerability details

## Description
`BOLDUpgradeAction.sol` has a problem inside `cleanupOldRollup` function which will always make this function revert whenever there is more than a single staker to refund (`stakerCount() > 1`) which seems to warrant `Medium` severity.

## Impacts
When the upgrade occurs and `BOLDUpgradeAction::cleanupOldRollup` is called, if there are `more then a single staker to be refunded` (which is highly probable), the function `will revert`. The only solution will be to deploy another instance of BOLDUpgradeAction (which include a fix) and call `perform` again. Since this is the [first step into the upgrade](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/BOLDUpgradeAction.sol#L466), we can assume redeploying a new fixed contract will resolve the issue with no side effects (the upgrade is not in the middle and some stuff needs to be reverted for example etc) and no other action required. I still think this warrant `Medium` severity for the time being saved from the Arbitrum team investigating this random revert, creating doubts/fears, etc.

## Proof of Concept
Create the following file test file `BOLDUpgradeAction.t.sol` and run the test as follow. The code from `RollupMinimal` is taken from the current `nitro-contracts`  repo, and `BOLDUpgradeActionMinimal` is having only the `cleanupOldRollup` pretty much as is.

`forge test --match-test=testcleanupOldRollup -vvv`

That will fail with the following output. 
```
Failing tests:
Encountered 1 failing test in test/challengeV2/BOLDUpgradeAction.t.sol:BOLDUpgradeActionTest
[FAIL. Reason: panic: array out-of-bounds access (0x32)] testcleanupOldRollup() (gas: 1459605)
```

To resolve the problem, apply the following simple changes, and run the test gain.

```diff
        for (uint64 i = 0; i < stakerCount; i++) {
-           address stakerAddr = ROLLUP_READER.getStakerAddress(i);
+           address stakerAddr = ROLLUP_READER.getStakerAddress(0);
            Staker memory staker = ROLLUP_READER.getStaker(stakerAddr);
            if (staker.isStaked && staker.currentChallenge == 0) {
                address[] memory stakersToRefund = new address[](1);
                stakersToRefund[0] = stakerAddr;

                ROLLUP_READER.forceRefundStaker(stakersToRefund);
            }
        }
```

```
Ran 1 test for test/challengeV2/BOLDUpgradeAction.t.sol:BOLDUpgradeActionTest
[PASS] testcleanupOldRollup() (gas: 1191121)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 565.87µs (393.97µs CPU time)
```


```solidity
pragma solidity ^0.8.17;

import "forge-std/Test.sol";

struct Staker {
    uint256 amountStaked;
    uint64 index;
    uint64 latestStakedNode;
    // currentChallenge is 0 if staker is not in a challenge
    uint64 currentChallenge;
    bool isStaked;
}

struct Zombie {
    address stakerAddres
Stakers can create edges with non unique Edge ID's.
Audit: 2024-05-arbitrum-foundation-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/assertionStakingPool/EdgeStakingPool.sol#L47
https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/challengeV2/EdgeChallengeManager.sol#L447


# Vulnerability details

## Impact
Stakers can create edges with non unique Edge ID's which breaks the protocol's main invariant.

## Proof of Concept
One of the main invariants of the protocol is that Edge ID's and Assertion ID’s must be unique. This isn't entirely true, as duplicate edges with the same edge id can be created. This breaks one of the main invariants of the protocol. The bug sterns from the fact that the there is no check in place to ensure that the edge id of the new edge that is about to created is unique (i.e it hasn't been created before with those parameters). Duplicate edges will have the same edge Id, which it to be non unique. 
See Additonal context / Main Invariants https://code4rena.com/audits/2024-05-arbitrum-bold

```
Main invariants
...
Edge ID’s and Assertion ID’s must be unique
```

When two assertions are created that have the same predecessor the protocol needs to decide which of the two is correct. Stakers then compete by creating edges that reference the assertion that they believe in. If stakers create two or more edges that are referencing the same assertion, the edge ids of those edges will not be unique.

The following test can be added to EdgeStakingPool.t.sol to demonstrate the described scenario.

```
    function testCreateEdgesWithNonUniqueEdgeIDs(CreateEdgeArgs memory args) public {
        uint256 requiredStake = challengeManager.stakeAmounts(args.level);
        bytes32 realEdgeId = keccak256(abi.encode(args));
        IEdgeStakingPool stakingPool = stakingPoolCreator.createPool(address(challengeManager), realEdgeId);

        token.transfer(address(stakingPool), requiredStake);
        stakingPool.createEdge(args);

        // Creating another edge with the same args in order to make the edge id to be non unique
        token.transfer(address(stakingPool), requiredStake);
        stakingPool.createEdge(args);

        assertEq(token.balanceOf(address(stakingPool)), 0);
        assertEq(token.balanceOf(address(challengeManager)), 2 * requiredStake);
    }
```
The two edges will have the same edge id, making it to be non unique to an edge.

```
Edge ID 1: 0x6e3776c7656560aeae3a4a471cacf76bc8ee0f2d96eb74e2a8d2c3b4ad463f14
Edge ID 2: 0x6e3776c7656560aeae3a4a471cacf76bc8ee0f2d96eb74e2a8d2c3b4ad463f14
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
The protocol should check if the edge id of the new edge to be created is unique and it should revert if it is not unique.























## Assessed type

Invalid Validation
Upgrades to EdgeChallengeManager changing staking requirement can cause `funds loss`
Audit: 2024-05-arbitrum-foundation-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/challengeV2/EdgeChallengeManager.sol#L429
https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/challengeV2/EdgeChallengeManager.sol#L580


# Vulnerability details

The fact that `EdgeChallengeManager.sol` is upgradeble, if an Admin would be to upgrade the contract and update `stakeToken` and/or `stakeAmounts`, that would result in `critical consequences`.

This report is related to `TOB-ARBCH-31` from the TrailOfBits audit which was identified as `Informational`. Reader should read that finding prior to continue reading this report to get proper context. This report will provide additional information how this would have a critical impacts which `increase the severity` of the original finding and how the current contrat `doesn't prevent` an Admin todo such upgrade, coupled with the fact that not all the team member seems to be fully aware of this (see discussion from Discord, refer to last section), which seems to warrant `Medium` severity overall.

## Impacts (stakeAmounts)
An upgrade implying a change in the staking requirement would be problematic (using WETH as stakeToken):

1. `No stake` required --> `100 WETH` stake required: A user which have created a level edge zero before the upgrade might be able to be refunded 100 WETH after the upgrade even if he didn't stake any at the edge creation (see **POC**).
2. `50 WETH` stake required --> `100 WETH` stake required: This is very similar to impact 1. A user which have created a level edge zero before the upgrade might be able to be refunded 100 WETH (instead of his 50 WETH staked), so the difference betweem the new stake requirement `-` the old stake requirement. This `impact is the most realistic` to be happening more often in production.
3. `100 WETH` stake required --> `50 WETH` stake required: A user which have created a level edge zero before the upgrade `will not` be able to get refunded his 100 WETH even if he did stake such amount at the edge creation since now the contract will refund only the new value, which is 50 WETH, so `half` of his original value.
4. `100 WETH` stake required --> `no stake` required: A user which have created a level edge zero before the upgrade `will not` be able to get refunded his 100 WETH even if he did stake such amount at the edge creation since now the contract will **skip** the transfer, so he will get `nothing`.

```solidity
    function refundStake(bytes32 edgeId) public {
        ChallengeEdge storage edge = store.get(edgeId);
        // setting refunded also do checks that the edge cannot be refunded twice
        edge.setRefunded();

        IERC20 st = stakeToken;
        uint256 sa = stakeAmounts[edge.level];
        // no need to refund with the token or amount where zero'd out
        if (address(st) != address(0) && sa != 0) {                                // <------------------------- THIS condition will SKIP the transfer, as identified
the blobGas is been refunded twice in `addSequencerL2BatchFromBlobs` functions
Audit: 2024-05-arbitrum-foundation-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/bridge/SequencerInbox.sol#L508


# Vulnerability details

The sequencer contract is been refunding the BatchPoster for gas used calling the     function `addSequencerL2BatchFromBlobs` see the `refundsGas` modifier:

```solidity
 modifier refundsGas(IGasRefunder gasRefunder, IReader4844 reader4844) {
        uint256 startGasLeft = gasleft(); 
        _;
        if (address(gasRefunder) != address(0)) {
            uint256 calldataSize = msg.data.length;
            uint256 calldataWords = (calldataSize + 31) / 32;
            // account for the CALLDATACOPY cost of the proxy contract, including the memory expansion cost
            startGasLeft += calldataWords * 6 + (calldataWords**2) / 512; 
           
            if (msg.sender != tx.origin) { 
                
            } else {
                
                if (address(reader4844) != address(0)) {
                   
                    try reader4844.getDataHashes() returns (bytes32[] memory dataHashes) {  <-----
                        if (dataHashes.length != 0) {
                            uint256 blobBasefee = reader4844.getBlobBaseFee();
                            startGasLeft +=
                                (dataHashes.length * gasPerBlob * blobBasefee) /
                                block.basefee;
                        }
                    } catch {} 
                }
            }

            gasRefunder.onGasSpent(payable(msg.sender), startGasLeft - gasleft(), calldataSize);
        }
    }

```
if the `reader4844` is set it, the modifier is refunding the blob gas too see the arrow above.

The problem is that the blob gas fee is been refunded in the `addSequencerL2BatchFromBlobsImpl` function:

```solidity

 function addSequencerL2BatchFromBlobs(
        uint256 sequenceNumber,
        uint256 afterDelayedMessagesRead,
        IGasRefunder gasRefunder,
        uint256 prevMessageCount,
        uint256 newMessageCount
    ) external refundsGas(gasRefunder, reader4844) {
        if (!isBatchPoster[msg.sender]) revert NotBatchPoster();
        if (isDelayProofRequired(afterDelayedMessagesRead)) revert DelayProofRequired();

        addSequencerL2BatchFromBlobsImpl(
            sequenceNumber,
            afterDelayedMessagesRead,
            prevMessageCount,
            newMessageCount
        );  <------
    }

```
[[Link]](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/bridge/SequencerInbox.sol#L400)

## Impact
the blobGas is been refunded twice in `addSequencerL2BatchFromBlobs` functions making the protocol loss funds.


## Proof of Concept

The BatchPoster is been refunded first in `addSequencerL2BatchFromBlobsImpl` function (see arrow below):

```solidity

 function addSequencerL2BatchFromBlobsImpl(
        uint256 sequenceNumber,
        uint256 afterDelayedMessagesRead,
Logical flaw in `_setBufferConfig` function that can lead to unexpected behavior and potentially incorrect state updates.
Audit: 2024-05-arbitrum-foundation-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/bridge/SequencerInbox.sol#L851-L856


# Vulnerability details

## Impact
[_setBufferConfig function src\bridge\SequencerInbox.sol:847-865](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/bridge/SequencerInbox.sol#L847-L865)

The following lines: [src\bridge\SequencerInbox.sol:851-855](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/bridge/SequencerInbox.sol#L851-L856)
```solidity
if (buffer.bufferBlocks == 0 || buffer.bufferBlocks > bufferConfig_.max) {
    buffer.bufferBlocks = bufferConfig_.max;
}
if (buffer.bufferBlocks < bufferConfig_.threshold) {
    buffer.bufferBlocks = bufferConfig_.threshold;
}
```
The problem is that these two conditions are not mutually exclusive, and the second condition can override the first condition's assignment.

For example, if `buffer.bufferBlocks` is initially 0, and `bufferConfig_.max` is greater than `bufferConfig_.threshold`, the first condition will set `buffer.bufferBlocks` to `bufferConfig_.max`. However, the second condition will then immediately set `buffer.bufferBlocks` to `bufferConfig_.threshold`, effectively overriding the previous assignment.

This behavior is likely unintended and could lead to unexpected results.

## Proof of Concept
The purpose of the `_setBufferConfig` function is to update the buffer configuration based on the provided `bufferConfig_` parameters. However, the two conditional statements that update the `buffer.bufferBlocks` value have overlapping conditions, which can result in unintended behavior.

**Here's a breakdown of the issue:**

The first condition `buffer.bufferBlocks == 0 || buffer.bufferBlocks > bufferConfig_.max` checks if the current `buffer.bufferBlocks` value is either zero or greater than the new `bufferConfig_.max` value. If this condition is true, it sets `buffer.bufferBlocks` to `bufferConfig_.max`.

The second condition `buffer.bufferBlocks < bufferConfig_.threshold` checks if the current `buffer.bufferBlocks` value is less than the new `bufferConfig_.threshold` value. If this condition is true, it sets `buffer.bufferBlocks` to `bufferConfig_.threshold`.

> The problem arises when `bufferConfig_.threshold` is less than `bufferConfig_.max`. In this case, the second condition can override the assignment made by the first condition, leading to an incorrect state update.

**For example, consider the following scenario:**

Initial state: `buffer.bufferBlocks = 0`
New configuration: `bufferConfig_.max = 100, bufferConfig_.threshold = 50`
In this scenario, the first condition `buffer.bufferBlocks == 0 || buffer.bufferBlocks > bufferConfig_.max` will be true, and `buffer.bufferBlocks` will be set to `bufferConfig_.max = 100`.

However, the second condition `buffer.bufferBlocks < bufferConfig_.threshold` will also be true (si
`cleanupOldRollup()` reverts due to premature out of bounds
Audit: 2024-05-arbitrum-foundation-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/rollup/BOLDUpgradeAction.sol#L341-L363


# Vulnerability details

## Impact

`cleanupOldRollup()` gets the stakerCount from the oldRollup, then loops over that number if they are less than 50 or 50 addresses if they are over 50 stakers.

- `Rollup.stakerCount()` gets the number of staker
- `Rollup.getStakerAddress()` gets the staker address from the `_stakerList` array
- `Rollup.forceRefundStaker()` makes the staker's stake withdrawable and then deletes the staker 

However, by deleting a staker, the array is being reduced from the right while the by incrementing the loop, we are skipping addresses from the left. 


## Proof of Concept
```solidity
        for (uint64 i = 0; i < stakerCount; i++) {
            address stakerAddr = ROLLUP_READER.getStakerAddress(i);

            OldStaker memory staker = ROLLUP_READER.getStaker(stakerAddr);
            if (staker.isStaked && staker.currentChallenge == 0) {
                address[] memory stakersToRefund = new address[](1);
                stakersToRefund[0] = stakerAddr;

                IOldRollupAdmin(address(OLD_ROLLUP)).forceRefundStaker(stakersToRefund);
            }
```
By increaasing the index i, and getting the next staker address, the loop will run out of bounds since the array is being reduced in the `OLD_ROLLUP` contract. 


This is not an issue with the old rollup, but with the loop depicted above.
## Tools Used
MANUAL REVIEW, Josephdara

## Recommended Mitigation Steps

Always get the first address in the loop, only change that if the currentChallenge for the first address is not zero.

```solidity
uint64 j;
       for (uint64 i = 0; i < stakerCount; i++) {
            address stakerAddr = ROLLUP_READER.getStakerAddress(j);

            OldStaker memory staker = ROLLUP_READER.getStaker(stakerAddr);
            if (staker.isStaked && staker.currentChallenge == 0) {
                address[] memory stakersToRefund = new address[](1);
                stakersToRefund[0] = stakerAddr;

                IOldRollupAdmin(address(OLD_ROLLUP)).forceRefundStaker(stakersToRefund);
            }else{
                j++;
            }
        }

```




## Assessed type

Error