xanoutas/solidity-security-findings huggingface dataset:
Upgrades to EdgeChallengeManager could `DoS the assertion chain` temporarily until admin unstuck it by forcing assertion
Audit: 2024-05-arbitrum-foundation-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupUserLogic.sol#L115-L128


# Vulnerability details

## Description
EdgeChallengeManager (a.k.a. ECM) can be upgraded during the life cycle of a rollup and this is officialized by an Admin calling `RollupAdminLogic::setChallengeManager`.


Let say we have the following `assertion chain setup`, so 3 assertions successfully confirmed, but then the 4th one, we have a divergence, and only the assertion winning the challenge will be able to be confirmed. 
```
                  /--- D(ECM1) - bad
     A --- B --- C    
     |     |     |\--- E(ECM2) - winner (edge confirmed)
    ECM1  ECM1  ECM1 
```

The problem arise with the following code from [RollupUserLogic::confirmAssertion](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupUserLogic.sol#L115-L128), the implementation assume that the ECM instance to retrieve the winningEdge information is the `same as the previous assertion`, but as you can see in my setup `that is not guaranteed`. So while indeed the assertion E is the winner (edge confirmed, but in ECM2 instance, not ECM1), the code here will not find the edge information and `will always revert`, so the `assertion chain will be stuck`. The team will be able to resolve the problem by `pausing the rollup` and calling `RollupAdminLogic::forceConfirmAssertion`. This seems unexpected and seem to warrant `Medium` severity.

```solidity
        if (prevAssertion.secondChildBlock > 0) {
            // if the prev has more than 1 child, check if this assertion is the challenge winner
            ChallengeEdge memory winningEdge = IEdgeChallengeManager(prevConfig.challengeManager).getEdge(winningEdgeId);     //<----- THIS: reading the ECM from prevConfig
            require(winningEdge.claimId == assertionHash, "NOT_WINNER");
            require(winningEdge.status == EdgeStatus.Confirmed, "EDGE_NOT_CONFIRMED");
            require(winningEdge.confirmedAtBlock != 0, "ZERO_CONFIRMED_AT_BLOCK");
            // an additional number of blocks is added to ensure that the result of the challenge is widely
            // observable before it causes an assertion to be confirmed. After a winning edge is found, it will
            // always be challengeGracePeriodBlocks before an assertion can be confirmed
            require(
                block.number >= winningEdge.confirmedAtBlock + challengeGracePeriodBlocks,
                "CHALLENGE_GRACE_PERIOD_NOT_PASSED"
            );
        }
```


## Impacts
Assertion chain `temporarily stuck` if an assertion divergence arise while an ECM upgrade had just happened and the winner is against the new ECM instance (while the previous assertion is against the old ECM instance)

## Proof of Concept
I will demostrate the setup described initially. We have 3 actors in play: Admin, Honest and Evil validator.
1) Admin deploy the Rollup and ECM contracts. Everything run smootly for days.
2)
The staker that lost the challenge dispute game can still withdraw his stake 
Audit: 2024-05-arbitrum-foundation-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

In the current implementation of the `RollupUserLogic`, a new assertion can only be created if a user staked the `requiredStake` amount and therefore became a validator. If there are 2 children of the previous assertion, challenge dispute game can be started and the second child has to send his amount to the `loserStakeEscrow`. The problem is that it's not somehow reflected in the `RollupCore` mapping structure and therefore the user can possibly request a withdraw after the finish of the dispute.


## Proof of Concept

Let's say there are 2 assertions: A and B. The first assertion stakes the required amount and suggest a new assertion:

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupUserLogic.sol#L175
```
 require(isStaked(msg.sender), "NOT_STAKED");
```

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupUserLogic.sol#L182
```
require(amountStaked(msg.sender) >= assertion.beforeStateData.configData.requiredStake, "INSUFFICIENT_STAKE");
```

And the mapping in `RollupCore` is also updated:

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupCore.sol#L277
```
_stakerMap[stakerAddress] = Staker(depositAmount, _latestConfirmed, stakerIndex, true, withdrawalAddress);
```

When the second assertion (B) is created, the process is the same and but the stake is transferred to the `loserStakeEscrow` as it's the second child:

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupUserLogic.sol#L210-216
```
 if (!getAssertionStorage(newAssertionHash).isFirstChild) {
            // We assume assertion.beforeStateData is valid here as it will be validated in createNewAssertion
            // only 1 of the children can be confirmed and get their stake refunded
            // so we send the other children's stake to the loserStakeEscrow
            // NOTE: if the losing staker have staked more than requiredStake, the excess stake will be stuck
            IERC20(stakeToken).safeTransfer(loserStakeEscrow, assertion.beforeStateData.configData.requiredStake);
        }

```

The problem is that once assertion is confirmed, the second user can still withdraw his stake even if he submitted incorrect state:

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupUserLogic.sol#L358-364
```
function withdrawStakerFunds() external override whenNotPaused returns (uint256) {
        uint256 amount = withdrawFunds(msg.sender);
        require(amount > 0, "NO_FUNDS_TO_WITHDRAW");
        // This is safe because it occurs after all checks and effects
        IERC20(stakeToken).safeTransfer(msg.sender, amount);
        return amount;
    }
```

By doing this, he bypasses the protocol requirement of the malicious party's stake to be taken in the case of s
confirmAssertin() can be DOSed.
Audit: 2024-05-arbitrum-foundation-findings
Severity: MEDIUM

# Lines of code

https://github.com/OffchainLabs/bold/blob/0420b9ddb88f71f5e86ca1b3bc256c09346b8315/contracts/src/rollup/RollupUserLogic.sol#L115


# Vulnerability details

## Impact
Confirmation of honest assertion can be DOSed after delay period when a malicious validator front-runs  ```confirmAsseertion()``` to post a malicious assertion.

By design, a honest validator should be able confirm assertion and withdraw bonds if there is no rival sibling after delay(1 week) period.

However, the check for rival sibling is incomplete and allows posting rival after delay period has passed.

The implication of this is that honest validator will have to get funds to create a challenge edge(which can be confirmed with time). 

Due to the large amount of bond required to create assertion, honest validator might have created an assertion pool to raise funds to create assertion, with this development, honest participant is required to get funds to create an edge so that assertion will be confirmed.

## Proof of Concept
The check for rivalry sibling does not include time.
1. Alice creates a valid assertion 
2. After delay period is passed, no counter assertion.
3. Alice tries to confirm assertion, but Bob frontons it with a rival assertion, Alice transaction revert, since no edge was provided.
```solidity
@> if (prevAssertion.secondChildBlock > 0 ) { //@audit-issue  
            // if the prev has more than 1 child, check if this assertion is the challenge winner
            ChallengeEdge memory winningEdge = IEdgeChallengeManager(prevConfig.challengeManager).getEdge(winningEdgeId);
            require(winningEdge.claimId == assertionHash, "NOT_WINNER");
            require(winningEdge.status == EdgeStatus.Confirmed, "EDGE_NOT_CONFIRMED");
            require(winningEdge.confirmedAtBlock != 0, "ZERO_CONFIRMED_AT_BLOCK");
            // an additional number of blocks is added to ensure that the result of the challenge is widely
            // observable before it causes an assertion to be confirmed. After a winning edge is found, it will
            // always be challengeGracePeriodBlocks before an assertion can be confirmed
            require(
                block.number >= winningEdge.confirmedAtBlock + challengeGracePeriodBlocks,
                "CHALLENGE_GRACE_PERIOD_NOT_PASSED"
            );
        }
```

Paste ```testSuccessConfirmUnchallengedAssertions()``` in ```Rollup.t.sol```
```solidity
    function testSuccessConfirmUnchallengedAssertions() public returns (bytes32, AssertionState memory, uint64) {
        (bytes32 assertionHash, AssertionState memory state, uint64 inboxcount) = testSuccessCreateAssertion();
        vm.roll(userRollup.getAssertion(genesisHash).firstChildBlock + CONFIRM_PERIOD_BLOCKS + 1);
        bytes32 inboxAccs = userRollup.bridge().sequencerInboxAccs(0);

        //Frontrun Confirmation with invalid assertion that has same parent with assertion to be confirmed
        //uint64 inboxcount = uint64(_createNewBatch());
`sequencerBatchAcc` has an incorrect value in `RollupCore`
Audit: 2024-05-arbitrum-foundation-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

In the current implementation of the `RollupCore` contract, `createNewAssertion()` function is used to create new assertions. The new assertion should have several parameters including the hash of the previous assertion, the state after, and the sequencer inbox accumulator value. The problem is that currently when determining `sequencerInboxAcc` value, the function uses `afterInboxPosition` that shows how much messages should the next assertion process instead of `currentInboxPosition` that shows how much messages this assertion has process (what the value of sequencer accumulator should represent).


## Proof of Concept

Let's say there are 200 messages that were added to the inbox so that `currentInboxPosition` is equal to 200 (at the time assertion was created):

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupCore.sol#L436
```
  uint256 currentInboxPosition = bridge.sequencerMessageCount();
```

Then the function fetches the new messages that may have added to the inbox since the last assertion:

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupCore.sol#L450
```
uint256 afterInboxPosition = afterGS.getInboxPosition();
```

The problem is that the sequencer should represent how many messages this assertion has processed and not the value that the next assertion should represent:

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupCore.sol#L471
```
sequencerBatchAcc = bridge.sequencerInboxAccs(afterInboxPosition - 1);
```


## Tools Used

Manual review.

## Recommended Mitigation Steps

Change `afterInboxPosition` on `currentInboxPosition` when assigning the value of sequence accumulator.


## Assessed type

Other
If an edge or assertion gets slashed, some depositors to the stakingPool will be cheated.
Audit: 2024-05-arbitrum-foundation-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/assertionStakingPool/AbsBoldStakingPool.sol#L41-L54


# Vulnerability details

## Impact
Stakers to a pool are not treated equally when their edge gets slashed. Some will be able to receive the full amount they deposited, while others receive nothing

## Proof of Concept
EdgeStakingPool and AssertionStakingPool inherit AbsBoldStakingPool. They allow users to pool resources together to make assertions and edges.
Looking at AbsBoldStakingPool#withdrawFromPool, we can see that depositors are allowed to withdraw the full amount they deposited, even though the edge they staked on got slashed:
```solidity
    function withdrawFromPool(uint256 amount) public {
        if (amount == 0) {
            revert ZeroAmount();
        }
        uint256 balance = depositBalance[msg.sender];
        if (amount > balance) {
            revert AmountExceedsBalance(msg.sender, amount, balance);
        }

        depositBalance[msg.sender] = balance - amount;
        IERC20(stakeToken).safeTransfer(msg.sender, amount);//@audit-info some depositors will be cheated if edge or assertion(?) gets slashed

        emit StakeWithdrawn(msg.sender, amount);
    }
```  
So let's say 10 people contribute $10 each to an EdgeStakingPool, and the stake required to make an edge is $50.
If the edge they mad gets invalidated, 5 users will be able to withdraw their full $10, while the other 5 get nothing.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Losses from losing an assertion or edge should be socialized among all depositors to a pool.
StakingPool should employ shares mechanism, where each depositor will be able to withdraw their share of the total value of the pool.


## Assessed type

Error
Staked amounts on layer zero edges could be stuck or even lost for honest challengers
Audit: 2024-05-arbitrum-foundation-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Honest parties who created edge challanges can not get a refund if the the stake token or amount zero'd out. As a result, the funds are stuck or even lost even if the stake token or amount are set back again by deploying a new challange manager. (check PoC below for how this can be done by a malicious party).


## Proof of Concept

When a zero layer edge is created it must include stake amount. 

```solidity
	st.safeTransferFrom(msg.sender, receiver, sa);
```
[EdgeChallengeManager.sol:L434](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/challengeV2/EdgeChallengeManager.sol#L434)

The amounts are stored in stakeAmounts[] which are set upon challenge manager deployment. Note that, staking can be disabled by setting zeros for the token or stakeAmounts. To do this, a new challenge manager needs to be deployed and its address updated in the assertion chain.

However, when this is done (e.g. stakeAmounts are zeroed), all honest participants won't be able to refund their stake. This is because of this check:
```solidity
	if (address(st) != address(0) && sa != 0) {
		st.safeTransfer(edge.staker, sa);
	}
```
[EdgeChallengeManager.sol:L580-L582](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/challengeV2/EdgeChallengeManager.sol#L580-L582)

As a result, any unclaimed stakes by honest participants will be lost. 

Furthermore, to create an attack from this, a malicious party can wait till the token or stakeAmounts are zero, and call `refundStake` for all honest participants (with confirmed edges). This is possible since anyone can call `refundStake` for any edge. Thus, resulting in setting their edges as refunded although they didn't receive any amounts.
```solidity
 edge.setRefunded();
```
[EdgeChallengeManager.sol:L575-L576](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/challengeV2/EdgeChallengeManager.sol#L575-L576)

## Tools Used
Manual analysis

## Recommended Mitigation Steps

`refundStake` shouldn't be permission-less, only the staker should be allowed to call it. 





## Assessed type

Other
An error in the accounting of stake refunds could result in insolvency
Audit: 2024-05-arbitrum-foundation-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/challengeV2/EdgeChallengeManager.sol#L433-L434


# Vulnerability details



## Impact
Malicious parties who created edge challanges can not get refund if the the edge was deemed to be a loser. The protocol sends all rivals of a layer zero edge to `excessStakeReceiver`. However, if a new challange manager was deployed during the dispute game, and a new edge was created after, it could cause an error in the accounting of stake refunds resulting in insolvency. Therefore, one or more honest parties won't get their original stake back.

*Note: stakes here refer to staking on layer zero edges upon creation.*

## Proof of Concept

Here is a scenario as an example to illustrate the issue:

Assume we have Bob, Alice, Mario as participants. 
Assume in the edge challenge manager contract, we have 5 WETH balance that's allocated for honest participants for refunding their stakes on edge challanges.

- **Contract balance => 5 WETH**
- Stake amount at the moment is **1 WETH**.
- Bob creates a edge challange for an incorrect assertion.
- Since Bob is the first one, **1 WETH** will be transferred to the edge challenge manager contract
```solidity
	address receiver = edgeAdded.hasRival ? excessStakeReceiver : address(this);
	st.safeTransferFrom(msg.sender, receiver, sa);
```
[EdgeChallengeManager.sol#L433-L434](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/challengeV2/EdgeChallengeManager.sol#L433-L434)

- **Contract balance => 6 WETH**
- Alice creates a rival edge (for another incorrect assertion).
- **1 WETH** will be transferred to `excessStakeReceiver`
- a new edge challenge manager is deployed changing the stake amount to **2 WETH**
- Note that, the dispute game is still on-going (we have a window of a challange period)
- Mario creates a rival edge (for the correct assertion) utilizing BoLD to protect the chain.
- **2 WETH** from Mario transferred to the edge.
- The game finished, Mario won.
- Mario calls `refundStake` to retrieve their stake back (**2 WETH**) 
- Mario receives **2 WETH**
- **Contract balance => 4 WETH**

As noticed, the contract doesn't have enough balance to cover the refunds for all honest participants. Thus, one or more honest participants won't get their refund back.






## Tools Used
Manual analysis

## Recommended Mitigation Steps

Instead of sending all stakes provided after the first one to an excess stake receiver, Accumulate the stakes for all rival edges in the contract. Then on `refundStake` , refund the winner and send the remaining to excess stake receiver.





## Assessed type

Other
Participants who didn't deposit into edge challenge manager can still get a refund
Audit: 2024-05-arbitrum-foundation-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details




## Impact
When the staking is disabled after it was enabled for some time, All participants who have confirmed edges later, will be able to refund themselves although they didn't stake any amount in the first place.  


## Proof of Concept

An example to demonstrate the issue:

- a new edge challenge manager is deployed disabling staking
- Bob creates a edge challange for an correct assertion.
- Since staking is disabled, Bob won't transfer any stake amount.
```solidity
	if (address(st) != address(0) && sa != 0) {
```
[EdgeChallengeManager.sol:L429](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/challengeV2/EdgeChallengeManager.sol#L429)

- Bob has their edge confrimed.
- Bob doesn't call `refundStake` Immediately but decides to wait
- After some time, the protocol enabled the staking again.
- Bob now calls `refundStake`
- Bob receives stake amount although he didn't stake any in the beginning.


## Tools Used
Manual analysis

## Recommended Mitigation Steps

A flag for each participant should be used to know wether they staked or not.




## Assessed type

Other
Griefing Attack Possible Where Validator Will Lose Their Stake
Audit: 2024-05-arbitrum-foundation-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/assertionStakingPool/AssertionStakingPool.sol#L40
https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/assertionStakingPool/EdgeStakingPool.sol#L44


# Vulnerability details

## Proof of Concept

#### Flow

1. `Create a Pool`: Any validator can use the `createPool` function to create a new `AssertionStakingPool`.

```solidity
File: AssertionStakingPoolCreator.sol

    function createPool(
        address _rollup,
        bytes32 _assertionHash
    ) external returns (IAssertionStakingPool) {
        AssertionStakingPool assertionPool = new AssertionStakingPool{salt: 0}(_rollup, _assertionHash);
        emit NewAssertionPoolCreated(_rollup, _assertionHash, address(assertionPool));
        return assertionPool;
    }

```

2. `Deposit Tokens`: Validators deposit tokens into the staking pool using the `depositIntoPool` function.

```solidity
File: AbsBoldStakingPool.sol

    function depositIntoPool(uint256 amount) external {
        if (amount == 0) {
            revert ZeroAmount();
        }

        depositBalance[msg.sender] += amount;
        IERC20(stakeToken).safeTransferFrom(msg.sender, address(this), amount);

        emit StakeDeposited(msg.sender, amount);
    }

```

3. `Create Assertions`: Using the deposited tokens, validators can create new assertions with the `createAssertion` function.

```solidity
File: AssertionStakingPool.sol

    function createAssertion(AssertionInputs calldata assertionInputs) external {
        uint256 requiredStake = assertionInputs.beforeStateData.configData.requiredStake;
        // approve spending from rollup for newStakeOnNewAssertion call
        IERC20(stakeToken).safeIncreaseAllowance(rollup, requiredStake);
        // reverts if pool doesn't have enough stake and if assertion has already been asserted
        IRollupUser(rollup).newStakeOnNewAssertion(requiredStake, assertionInputs, assertionHash, address(this));
    }

```

#### Important things to note about this function

* No access control in `createAssertion`.

* The required stake tokens are directly approved and transferred without verifying the caller's authorization.

#### Griefing Attack Scenario

1. Alice creates a new `AssertionStakingPool` and deposits `X` tokens.

2. Attacker frontruns Alice by calling `createAssertion` with incorrect `assertionInputs`.

3. Alice unknowingly becomes a malicious validator due to the incorrect assertion and loses her stake.

Similar situation will happen with `EdgeStakingPool` contract as well while creating new edge through `createEdge` function.

1. Alice creates a new `EdgeStakingPool` and deposits `X` tokens.

2. Attacker frontruns Alice by calling `createEdge` with incorrect `CreateEdgeArgs`.

3. Alice unknowingly becomes a malicious due to the incorrect edge and loses her stake.

## Impact

Validators can lose their stake due to incorrect assertions being made on their behalf.

## Tools Use