xanoutas/solidity-security-findings huggingface dataset:
Users can create assertion with stale `baseStake` amount
Audit: 2024-05-arbitrum-foundation-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/assertionStakingPool/AssertionStakingPool.sol#L40-L46
https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/rollup/RollupUserLogic.sol#L177-L182
https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/rollup/RollupCore.sol#L492
https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/rollup/RollupCore.sol#L69-L70
https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/rollup/RollupAdminLogic.sol#L223-L226


# Vulnerability details

## Impact
This breaks core protocol functionality because it lead to a situation where assertions are created by a user whose stake is below the current `baseStake` amount.

## Proof of Concept
Users can create assertion once required stake amount is reached by calling `AssertionStakingPool::createAssertion(...)` . The call trace is as shown below
```solidity
  AssertionStakingPool::createAssertion(...)
   ->RollupUserLogic::newStakeOnNewAssertion(...)
@> -->RollupUserLogic::stakeOnNewAssertion(...)
   ---> RollupCore::createNewAssertion(...)
```

New assertions [build on the `configData` of their parent assertion](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/rollup/RollupCore.sol#L69-L70) and as such the `RollupUser(rollup)::stakeOnNewAssertion(...)` implements a check to ensure that the user creating a new assertion is sufficiently staked with respect to the `baseStake` value at the time the parent assertion was created as seen on L182 below

```solidity
File: RollupUserLogic.sol
163:     function stakeOnNewAssertion(AssertionInputs calldata assertion, bytes32 expectedAssertionHash)
164:         public
165:         onlyValidator
166:         whenNotPaused
167:     {
...
175:         require(isStaked(msg.sender), "NOT_STAKED");
176: 
177:         // requiredStake is user supplied, will be verified against configHash later
178:         // the prev's requiredStake is used to make sure all children have the same stake
...
182:@>       require(amountStaked(msg.sender) >= assertion.beforeStateData.configData.requiredStake, "INSUFFICIENT_STAKE");
183: 

```

However the problem lies in the validation on L182 in the `RollupUser(rollup)::stakeOnNewAssertion(...)` function, because the [value of `baseStake` can be updated by the rollup owner,](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/rollup/RollupCore.sol#L69-L70) if the `baseStake` value is increased before this new assertion is created and the current `amountStaked(msg.sender)` is less than this new `baseStake` amount, a user can still successfully create an assertion. This breaks the core protocol functionality because
Lack of input validation in edge pool allows malicious user to create a clearly invalid insertion to make staker lose fund.
Audit: 2024-05-arbitrum-foundation-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

Lack of input validation in edge pool allows malicious user to create a clearly invalid insertion to make staker lose fund.

## Proof of Concept

anyone can create an edge pool with edge id
```solidity
  constructor(
        address _challengeManager,
        bytes32 _edgeId
    ) AbsBoldStakingPool(address(EdgeChallengeManager(_challengeManager).stakeToken())) {
        challengeManager = _challengeManager;
        edgeId = _edgeId;
    }
```

then after there are enough user stake fund, [an edge can be created.](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/assertionStakingPool/EdgeStakingPool.sol#L44)
```solidity
 function createEdge(CreateEdgeArgs calldata args) external {
        uint256 requiredStake = EdgeChallengeManager(challengeManager).stakeAmounts(args.level);
        IERC20(stakeToken).safeIncreaseAllowance(address(challengeManager), requiredStake);
        bytes32 newEdgeId = EdgeChallengeManager(challengeManager).createLayerZeroEdge(args);
        if (newEdgeId != edgeId) {
            revert IncorrectEdgeId(newEdgeId, edgeId);
        }
    }
```

the function takes arbitrary CreateEdgeArgs input:

```solidity
/// @notice Data for creating a layer zero edge
struct CreateEdgeArgs {
    /// @notice The level of edge to be created. Challenges are decomposed into multiple levels.
    ///         The first (level 0) being of type Block, followed by n (set by NUM_BIGSTEP_LEVEL) levels of type BigStep, and finally
    ///         followed by a single level of type SmallStep. Each level is bisected until an edge
    ///         of length one is reached before proceeding to the next level. The first edge in each level (the layer zero edge)
    ///         makes a claim about an assertion or assertion in the lower level.
    ///         Finally in the last level, a SmallStep edge is added that claims a lower level length one BigStep edge, and these
    ///         SmallStep edges are bisected until they reach length one. A length one small step edge
    ///         can then be directly executed using a one-step proof.
    uint8 level;
    /// @notice The end history root of the edge to be created
    bytes32 endHistoryRoot;
    /// @notice The end height of the edge to be created.
    /// @dev    End height is deterministic for different levels but supplying it here gives the
    ///         caller a bit of extra security that they are supplying data for the correct level of edge
    uint256 endHeight;
    /// @notice The edge, or assertion, that is being claimed correct by the newly created edge.
    bytes32 claimId;
    /// @notice Proof that the start history root commits to a prefix of the states that
    ///         end history root commits to
    bytes prefixProof;
adversary can win the dispute game in the re-org event 
Audit: 2024-05-arbitrum-foundation-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/challengeV2/libraries/EdgeChallengeManagerLib.sol#L160-L201
https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/challengeV2/libraries/EdgeChallengeManagerLib.sol#L411-L439


# Vulnerability details

## Impact
Malicious parties can the dispute game in the re-org event .

## Proof of Concept
    function add(EdgeStore storage store, ChallengeEdge memory edge) internal returns (EdgeAddedData memory) {
     ..../
       if (firstRival == 0) {
            store.firstRivals[mutualId] = UNRIVALED;
        } else if (firstRival == UNRIVALED) {
            store.firstRivals[mutualId] = eId;
        } else {
 
        }
       ....../ 
       }



When creating layer zero edge or bisection child edges , all of those edges will be added to edge store and their first rival will be set .If the edge is added for first time and there is no rival for that mutual ID , first rival will set to UNRIVALED .Otherwise first rival will set to that edge ID .

POC 
1.Malicious party ALICE create layer zero edge id 1 

2.first rival of that mutual ID will be set UNRIVALED

3.honest party BOB create layer zero edge id 2 with same mutual id to edge 1 

4.first rival of that mutual ID will be set edge id 2

5.honest party will bisect the edge and will create two child edges and waiting for alice 's turn to create rivals for his children edges.

6.Unfornately , re-org event happen at block where BOB create layer zero
 edge id 2

7.first rival of mutual ID for both layer zero edge id 1 & 2 will be set UNRIVALED again 

8.BOB didn't know that re-org and waiting for children edges rivals from malicious party .

9.Malicious party alice 's UNRIVALED time will be bigger than BOB because she's been UNRIVALED since she created layer zero edge 1 .

10.Malicious ALICE can front run call confirm edge by time ahead of BOB .


ADD ResetFirstRival to EdgeChallengeManager.sol

        function ResetFirstRival(bytes32 mutualId) public {
        store.firstRivals[mutualId] = keccak256(abi.encodePacked("UNRIVALED"));
    }

ADD test to EdgeChallengeManager.t.sol 

        function testReorg() public returns (EdgeInitData memory, bytes32) {
        (EdgeInitData memory ei, bytes32[] memory states1,, bytes32 edge1Id) = testCanCreateEdgeWithStake();

        _safeVmRoll(block.number + NUM_BLOCK_UNRIVALED);

        assertEq(ei.challengeManager.timeUnrivaled(edge1Id), NUM_BLOCK_UNRIVALED, "Edge1 timer");
        
            bytes32[] memory states2 = a2RandomStates;
            bytes32[] memory exp2 = a2RandomStatesExp;
            bytes32 edge2Id = ei.challengeManager.createLayerZeroEdge(
                CreateEdgeArgs({
                    level: 0,
                    endHistoryRoot: MerkleTreeLib.root(exp2),
                    endHeight: height1,
                    claimId: ei.a2,
                    prefixProof:
A malicious validator can avoid loss his money doing bad assertions
Audit: 2024-05-arbitrum-foundation-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

Validator can unstake if their assertions is the last confirmed (see the the first arrow below) or his assertion has already a first child(see arrow below):

```solidity

function requireInactiveStaker(address stakerAddress) internal view {
        require(isStaked(stakerAddress), "NOT_STAKED");
        // A staker is inactive if
        // a) their last staked assertion is the latest confirmed assertion
        // b) their last staked assertion have a child
        bytes32 lastestAssertion = latestStakedAssertion(stakerAddress);
        bool isLatestConfirmed = lastestAssertion == latestConfirmed(); <----
        bool haveChild = getAssertionStorage(lastestAssertion).firstChildBlock > 0; <----
        require(isLatestConfirmed || haveChild, "STAKE_ACTIVE");
    }

```
[[Link]](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/rollup/RollupCore.sol#L565C5-L574C6)

Other point to understand the vulnerability is that validators can create a new assertion with non confirmed parents assertions(a parent is the past assertion). with that been said An attacker can control 2 validators and do the next steps:

1. stake in a bad assertion with his validator number 1 (he has to call `newStakeOnNewAssertion`).
2. stake on new assertion with a parent create in the step 1 with validator 2.
3. with validator 1 call `returnOldDeposit` since the assertion of the validator number 1 has already a child he can withdraw.
4. with validator 1 create a assertion with a parent assertion of the step 2.
5. at this point validator 2 can call `returnOldDeposit` and withdraw his money.

an attacker can keep doing this indefinitely leading to some problems:


```solidity
  function stakeOnNewAssertion(AssertionInputs calldata assertion, bytes32 expectedAssertionHash)
        public
        onlyValidator
        whenNotPaused
    {
        ...

        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); <------
        }
    }

```
[[Link]](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/rollup/RollupUserLogic.sol#L163C3-L217C6)

Each time that a assertion is created a amount equivalent to the `requiredStake` is been sent it to the `loserStakeEscrow`, successfully letting without funds the ro
The adversary validators steal the staked funds in `RollupUserLogic.sol`
Audit: 2024-05-arbitrum-foundation-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Description
All the validators that have lost a challenge in the past can steal funds from The honest validators in `RollupUserLogic.sol`
Here are quick 7 steps POC (check below for coded POC): 
Note: Alice and Bob are playing under the same entity.

1- The adversary validators **Bob** lose a challenge.
Bob loses 10 Wei here.

2- The honest validator creates the next assertion.

3- The admin call [setBaseStake()](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupAdminLogic.sol#L223-L226) to decrease the `baseStake` state variable.
e.g: from 10 Wei to 8 Wei.

4- **Alice** invoke [newStakeOnNewAssertion()](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupUserLogic.sol#L316-L322) to create the first child of **Bob** assertion 
Alice stake (technically it's a loss at this point) 10 Wei for this.
However, the `requiredStake` in **Alice**'s assertion `configHash` is only 8 Wei ([Check it here](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupCore.sol#L492)).

5- **Bob** trigger [reduceDeposit()](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupUserLogic.sol#L241-L245) to reduce his amount staked to only 8 Wei.

6- Now, **Bob** invoke [stakeOnNewAssertion()](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupUserLogic.sol#L163-L217) to create the first child of  **Alice** assertion.

7- **Alice** invoke [returnOldDeposit()](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupUserLogic.sol#L222-L225) to withdraw her 10 Wei and **Bob**  withdraw his 2 Wei by calling [withdrawStakerFunds()](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupUserLogic.sol#L358-L364).

### Deep dive
**Key points:**
- In the `RollupUserLogic.sol`, When an assertion has two children or more, The protocol only keeps 1 stake per challenge, so the loser stakes are already sent to the `loserStakeEscrow`([check here](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupUserLogic.sol#L210-L216)). So, the only funds will be available in `RollupUserLogic` is from the honest validators and they can withdraw the funds when are not active staker.

- In the RollupCore system, it is possible to create multiple assertions with only one stack. It uses a structure where assertions are linked, and each new assertion is created based on the state of the previous one. This is why when an adversary validator loses a challenge his values in [_stakerMap[stakerAddress]](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupCore.sol#L102) are not updated. 

- The `setBaseStake()` function in the `RollupAdminLogic.sol` contract is used to update
An invalid assertion can get confirmed, even when there are honest participants
Audit: 2024-05-arbitrum-foundation-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/challengeV2/libraries/EdgeChallengeManagerLib.sol#L826-L830
https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/challengeV2/libraries/EdgeChallengeManagerLib.sol#L529


# Vulnerability details

## Impact
Even though an assertion is invalid, adversary can make it get confirmed just because some states within it were valid.
Note that even though an honest party later proves the state that was invalid, it would be irrelevant cos the adversay block edge level has been confirmed, and timer set to max uint64.

## Proof of Concept
Normally, if there are two rival assertions, it is expected that the honest participant would bisect his edge to the exact point where he disagrees with the adversary, and then oneStepProve that executing a step on the prevState(which everyone agrees on), would yield the state that he claimed.

Once honest participant does that, he can then:
- confirmEdgeByOneStepProof, which would update the timerCache of that proven singleStepEdge to uint64.max
- updateTimerCacheByClaim, which allows him to recursively update timerCache of parent levels(till he reaches block level) to timerCache of the singleStepEdge(i.e. uint64.max)
```solidity
function updateTimerCacheByClaim(
    EdgeStore storage store,
    bytes32 edgeId,
    bytes32 claimingEdgeId,
    uint8 numBigStepLevel
) internal returns (bool, uint256) {
    // calculate the time unrivaled without inheritance
    uint256 totalTimeUnrivaled = timeUnrivaled(store, edgeId);
    checkClaimIdLink(store, edgeId, claimingEdgeId, numBigStepLevel);
@>  totalTimeUnrivaled += store.edges[claimingEdgeId].totalTimeUnrivaledCache;
    return updateTimerCache(store, edgeId, totalTimeUnrivaled);
}
```
- confirmEdgeByTime for the block level edge, which checks that the edge's timerCache(in this case, uint64.max) is greater than confirmationThresholdBlocks.
- Now, the block level edge is confirmed, so he can confirmAssertion in the assertion chain after the confirmPeriodBlocks.

But the issue is, Adversary can create an invalid assertion and bisect it to a point where it was valid, and perform the steps outlined above.

For example,
- Honest party assertion claim state = ABCDEFGH
- Adversary claim state=ABCDEFGZ
- Adversary bisects his edge to the point where he is able to prove that SmallStepEdge B transitions to C
- Adversary calls confirmEdgeByOneStepProof to confirm his SmallStepEdge 
- Adversary updateTimeerCacheByClaim till he reaches his Block Edge
- Adversary confirmEdgeByTime of his block edge, and would be able to confirmAssertion in the assertionChain after confirmPeriodBlocks


## Tools Used
Manual Review

## Recommended Mitigation Steps
onestepProving an edge should not immediately allow the parent block edge to be confirmable, as the bigger picture may be invalid.
Only those that were able to oneStepProve all the singleStepEdges as requested by other rivals after the confirmat
Adversary can update his timerCache with his rival's(i.e. honest party's) timer, and maliciously win assertions
Audit: 2024-05-arbitrum-foundation-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/challengeV2/libraries/EdgeChallengeManagerLib.sol#L528
https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/challengeV2/libraries/EdgeChallengeManagerLib.sol#L698-L700


# Vulnerability details

## Impact
Adversary will have his edge confirmed, even though he wasn't able to prove his single step edge.


## Proof of Concept
updateTimerCacheByClaim only checks that the `mutualId of edgeId`==`originId of claimingEdge`.
Since mutualId of two rivals are equal, adversary can updateTimerCacheByClaim, entering the claiming (or one step proved) edge of his rival:

updateTimerCacheByClaim function:
```solidity
function updateTimerCacheByClaim(
    EdgeStore storage store,
    bytes32 edgeId,
    bytes32 claimingEdgeId,
    uint8 numBigStepLevel
) internal returns (bool, uint256) {
    // calculate the time unrivaled without inheritance
    uint256 totalTimeUnrivaled = timeUnrivaled(store, edgeId);
@>  checkClaimIdLink(store, edgeId, claimingEdgeId, numBigStepLevel); //@audit-issue I can update my timerCache with my rival's claim timer
    totalTimeUnrivaled += store.edges[claimingEdgeId].totalTimeUnrivaledCache; 
    return updateTimerCache(store, edgeId, totalTimeUnrivaled);
}
```
checkClaimIdLink function:
```solidity
    function checkClaimIdLink(
        EdgeStore storage store,
        bytes32 edgeId,
        bytes32 claimingEdgeId,
        uint8 numBigStepLevel
    ) private view {
@>      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
            );
        }
    }
```

If an adversary cannot prove his edge, but an honest party has, he would update his timerCache with the honest party's. This will allow him to confirm his edge.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Within the ChallengeEdge struct, there should be a parameter that does not change across levels and bisections of an edge.
In the current implementation, claimId is set to 0 when bisecting an edge. This is why we can't use it as the "parameter" we are looking for.
I suggest having a parameter ,let's say uniqueID(I'm short of names) within the challenge Edge struct.
This "uniqueID" should be set to `claimId` of the block edge being created.
The difference between ChallengeEdge.claimId and ChallengeEdge.uniqueId is that while the former is set to 0