xanoutas/solidity-security-findings huggingface dataset:
Creating invalid assertion using honest parties' staked funds
Audit: 2024-05-arbitrum-foundation-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
An attacker can create invalid assertion without risking his own funds, instead locking the honest parties' staked funds.

## Proof of Concept

### Attack explanation in general
 1. A valid assertion is already confirmed, called `A`. So we have the following assertion tree:
```
 A ---------------------> 
 valid                                 
 confirmed                           
 assertion              
 ```
 2. An `AssertionStakingPool` contract is already created with immutable `assertionHash` equal to zero. When the staked amount in this contract reaches to the `requiredStake` amount, it creates a valid assertion. I am assuming all the participants in this staking pool are honest. So we have:
 ```
 A ---------------------> B ----------------->
 valid                  valid               
 confirmed              pending             
 assertion              assertion
                        staker:
                        AssertionStakingPool
 ```
 3. After some time (note that the assertion `B` is not confirmed yet), the attacker gets a flashloan and creates an invalid assertion, called `C` as a child of assertion `B`. So, we will have the following assertion tree.
 ```
 A ---------------------> B -------------------------> C ------------------------->
 valid                  valid                       invalid                     
 confirmed              pending                     pending                     
 assertion              assertion                   assertion                   
                        staker:                     staker:                     
                        AssertionStakingPool        Attacker                           
 ```
 4. Since the assertion `B` now has a child, its staker (which is the staking pool) is considered as an inactive staker. So, the attacker, in the same transaction, calls the function `makeStakeWithdrawableAndWithdrawBackIntoPool` in `AssertionStakingPool` contract. By doing so all the staked amount will return to the pool again.
 5. In the same transaction, the attacker, by using the available amount in `AssertionStakingPool` contract, creates another invalid assertion, called `D`, as a child of assertion `C`. In other words, the attacker calls `createAssertion` in the `AssertionStakingPool` contract to create a new invalid assertion. So we will have:
 ```
 A ---------------------> B -------------------------> C -------------------------> D ----------------->
 valid                  valid                       invalid                     invalid
 confirmed              pending                     pending                     pending
 assertion              assertion                   assertion                   assertion
                        staker:                     staker:                     staker:
Two correct assertion chains could possibly happen which breaks a core invariant in BoLD 
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#L236


# Vulnerability details

## Impact

anyTrustFastConfirmer is supposed to be set only on an AnyTrust chain, it can force confirm any pending assertion, this is a feature which allows a committee members (multi-sig) to confirm assertions quicker.

For this reason, deadline, prev, and challenge validations are skipped, unlike other kind of validations (e.g. parentAssertionHash, confirmState,  assertionHash is currently pending) are still applied. Those are done in `confirmAssertionInternal`. 

However, it doesn't check the status of its sibling (rival assertion) which allows confirming the intended assertion even if it has a confirmed sibling. This should never happen as it creates two correct assertion chains. In other words, we would get two confirmed assertions which have the same parent. This breaks a core invariant in BoLD that is, at any point of time, only one assertion chain can be correct.

## Proof of Concept

Assume we have the following assertion chain

```sh
A -- B -- C
```

C is confirmed.

anyTrustFastConfirmer want to confirm a new assertion under B, let's call it D


So we would have 
```sh
 A -- B -- C
       \-- D
```

anyTrustFastConfirmer will call `fastConfirmNewAssertion`  which does the following:

- It requires `expectedAssertionHash` to be supplied.
```solidity
        // Must supply expectedAssertionHash to fastConfirmNewAssertion
        require(expectedAssertionHash != bytes32(0), "EXPECTED_ASSERTION_HASH");

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

- It checks for `prevAssertion` existence 
```solidity
getAssertionStorage(prevAssertion).requireExists();
```
[RollupUserLogic.sol:L286](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/rollup/RollupUserLogic.sol#L286)

- If the assertion to be created does not exist, it create a new one, otherwise, it skips this part.
```solidity
        if (status == AssertionStatus.NoAssertion) {
            // If not exists, we create the new assertion
            (bytes32 newAssertionHash,) = createNewAssertion(assertion, prevAssertion, expectedAssertionHash);
```
[RollupUserLogic.sol:L288](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/rollup/RollupUserLogic.sol#L288)

- Last step, call `fastConfirmAssertion` which checks if the caller is the anyTrustFastConfirmer, then calls `confirmAssertionInternal` skipping the validations that are normally done.

- If we examine `confirmAssertionInternal`, we find that it has two checks only;
	- it checks if the assertion exists and is pending.
	```solidity
			// Check that assertion is pending, this also checks that assertion exists
			require(asse
incorrect assertion can be confirmed by abusing confirmEdgeByOneStepProof function
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#L817-L818


# Vulnerability details

incorrect assertion can be confirmed by abusing confirmEdgeByOneStepProof function

## Impact

An attacker can create in one transaction an enough deep level of Edges for his own assertion to have the lower part of the commit history be confirmed by calling `confirmEdgeByOneStepProof` and in doing so have the entire non correct assertion, that is an assertion where the upper history commitment is manipulated by the attacker, approved which is a disaster for the protocol.

## Proof of Concept

When Edges are created to contest an assertion and when we reach the level of  `NUM_BIGSTEP_LEVEL + 1` we are in small step mode where we can provide a single proof of inclusion to have our entire chain of edges be confirmed as the correct one.

On every edge creation the edges are split or bisected with `bisectEdge` method :
 
we get a lower and an upper edge child. In the begining we have a chain of `(last approved assertion) -> (our assertion)` by the first bisection we **roughly** get two child edge 
 
the lower one `Lower_step_1(last approved assertion + middle point)` 

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/challengeV2/libraries/EdgeChallengeManagerLib.sol#L633-L635

and the upper one `Upper_step_1(middle point to our assertion)`

https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/challengeV2/libraries/EdgeChallengeManagerLib.sol#L646-L648

The attacker can drill till `NUM_BIGSTEP_LEVEL + 1` by stepping on the lower part of the commitment history and finally create a **valid lower child** that represents only the lower part of commitment history which will pass the validation trough [oneStepProofEntry.proveOneStep](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/challengeV2/libraries/EdgeChallengeManagerLib.sol#L817-L818C13). 

But the history commitment for the upper side can be a changed/manipulated one by the attacker. Having only the lower part correct we practically have our entire assertion confirmed as the correct one.

By providing a valid proof for a history commitment inclusion to this lower part we have our edge set as confirmed and also the `totalTimeUnrivaledCache` set to maximal uint64 value which signals that we have a correct history.

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

All other rival edges (even the valid one) are now reverted with 'RivalEdgeConfirmed'.

By calling enough time `updateTimerCacheByChildren` and `updateTimerCacheByClaim` like is done in the test function [updateTimers](https://github.com/code-423n4
Withdrawals can be delayed in some conditions
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#L565-L574


# Vulnerability details

## Impact
Extending the delay of withdrawals
## Proof of Concept
The maximum delay for withdrawals from L2 to L1 can be up to approximately 1 week (specifically 6.4 days), primarily due to the challenge period inherent in the optimistic rollup design used by Arbitrum. This challenge period is a security mechanism that allows validators to dispute state assertions before they are finalized on the L1 Ethereum blockchain.

The reason for this delay is rooted in the dispute resolution process. When a state assertion is made on the L2 chain, there is a fixed period during which it can be challenged. This is to ensure that only valid state transitions are confirmed on the L1 chain. If a dispute arises, it must be resolved within this period, and only after the period expires without disputes, or after a dispute is resolved in favor of the assertion, can the withdrawal be finalized on L1.

But this could be somehow bricked with an aditional period of time for specific conditions.


Validators can call `newStakeOnNewAssertion` and their funds will be taken on stake while the new assertion created in `RollUpCore`;

The function is as below:
```solidity
Contract: RollupUserLogic.sol

363:     function newStakeOnNewAssertion( 
364:         uint256 tokenAmount,
365:         AssertionInputs calldata assertion,
366:         bytes32 expectedAssertionHash,
367:         address withdrawalAddress
368:     ) public {
369:         require(withdrawalAddress != address(0), "EMPTY_WITHDRAWAL_ADDRESS");
370:         _newStake(tokenAmount, withdrawalAddress);
371:         stakeOnNewAssertion(assertion, expectedAssertionHash);
372:         /// @dev This is an external call, safe because it's at the end of the function
373:         receiveTokens(tokenAmount); 
374:     }
```
L:371 calls `stakeOnNewAssertion` in the same contract.
```solidity
Contract: RollupUserLogic.sol

173:     function stakeOnNewAssertion(AssertionInputs calldata assertion, bytes32 expectedAssertionHash)
```

And when the stakers want to withdraw their stakes, the first function to call is as follows:
```solidity
Contract: RollupUserLogic.sol

240:     /**
241:      * @notice Refund a staker that is currently staked on an assertion that either has a chlid assertion or is the latest confirmed assertion.
242:      */
243:     function returnOldDeposit() external override onlyValidator whenNotPaused {
244:         requireInactiveStaker(msg.sender);
245:         withdrawStaker(msg.sender);
246:     }
```
So the staker should be inactive - validated by `requireInactiveStaker`
And the funds should be made withdrawable by `withdrawStaker`

The logic of the `requireInactiveStaker` is as below;

```solidity
Contract: RollupCore.sol

614:     function requireInactiveStaker(address stakerAddress) internal view { 
615:         requir
`checkClaimIdLink` does not check `ClaimId`
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#L683-L710


# Vulnerability details

## Impact
`checkClaimIdLink` does not check `ClaimId`, and a terminal node can inherit timers which it does not deserve.

## Proof of Concept
According to BoLD paper section 5.4, suppose A, B are terminal nodes which rivals each other (aka shares the same mutualId), and A has children a1, a2, B has children b1, b2, b3. The five children share the same origin id (which is A,B's mutualId).

We should have
β(A,t) := λ(A,t) + max{β(a1,t), β(a2,t), β(a3,t)} 
β(B,t) := λ(B,t) + max{β(b1,t), β(b2,t)} 


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

However, in our implementation, we only check the originId of the children matches the mutualId of the parent.
We can actually set β(A,t) := λ(A,t) + β(b1,t), which means an edge can inherit timer from its rival's children!
Even worse, all of a1, a2, a3, b1, b2's descendants at the same level will share the same originId. We can start from a (proved) proof node at level N, by using it as claimingEdgeId and using its level N-1 length 1 ancestor (or the ancestor's rival) as edgeId, we can set edgeId's timer to type(uint64).max. By repeating so, we can almost instantly confirm any level 0 length 1 edge.

```
            // when bisecting originId is preserved
            ChallengeEdge memory lowerChild = ChallengeEdgeLib.newChildEdge(
                ce.originId, ce.startHistoryRoot, ce.startHeight, bisectionHistoryRoot, middleHeight, ce.level
            );
```
## Tools Used
Manual

## Recommended Mitigation Steps
require(store.edges[claimingEdgeId].claimId == edgeId);


## Assessed type

Context
Adversary can make honest parties unable to retrieve their assertion stakes if the required amount is decreased
Audit: 2024-05-arbitrum-foundation-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
When the required stake (to create a new assertions) is updated to a lower amount, adversary can make the honest party unable to retrieve their assertion stakes.

## Proof of Concept

     A -- B -- C -- D(latest confirmed) -- E
Suppose the initial stake amount is 1000 ETH, and till now no invalid assertions have been made. (A, B, C, D, E are all valid and made by the same validator). The rollup contract should hold 1000 ETH now.

     A -- B -- C -- D(latest confirmed) -- E
                                        \
                                         \ F(invalid)
Then, the admin update the required stake to 700 ETH, Alice made an invalid assertion F. Since its parent D was created before the update, Alice will still need to stake 1000 ETH, and the 1000 ETH will be sent to loserStakeEscrow.
```
        if (!getAssertionStorage(newAssertionHash).isFirstChild) {

            // only 1 of the children can be confirmed and get their stake refunded
            // so we send the other children's stake to the loserStakeEscrow
            IERC20(stakeToken).safeTransfer(loserStakeEscrow, assertion.beforeStateData.configData.requiredStake);
        }
```

     A -- B -- C -- D(latest confirmed) -- E
                                        \

                                         \ F -- G
(a) Alice creates F's children, G. Now, only 700 ETH of stake is needed. However, as the comment suggests, no refund will be made since G's ancestor could need more stake.
```
        // requiredStake is user supplied, will be verified against configHash later
        // the prev's requiredStake is used to make sure all children have the same stake
        // the staker may have more than enough stake, and the entire stake will be locked
        // we cannot do a refund here because the staker may be staker on an unconfirmed ancestor that requires more stake
        // excess stake can be removed by calling reduceDeposit when the staker is inactive
        require(amountStaked(msg.sender) >= assertion.beforeStateData.configData.requiredStake, "INSUFFICIENT_STAKE");
```
(b) To bypass the limit in (a), Alice calls her friend Bob to make the assertion G instead , Bob will only need to stake 700 ETH now. The rollup contract currently holds 1700 ETH. Then, Alice can withdraw her stake since she is no longer active. (her last staked assertion have a child)
```
    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 == lates
A flaw in the time confirmation mechanism enables the confirmation of erroneous edges.
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#L520-L531
https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/challengeV2/libraries/EdgeChallengeManagerLib.sol#L689-L710


# Vulnerability details

## Impact
An attacker can exploit a vulnerability in the confirmation by time algorithm to confirm incorrect edges, leading to the confirmation of erroneous assertions.

## Proof of Concept
There are two methods available for confirming an edge. Stakers can either bisect the edge until a one-step proof can be obtained and executed, or they can wait until the time threshold has been reached and confirm an unrivaled edge.

The confirmation by time algorithm involves two methods: `updateTimerCacheByChildren` and `updateTimerCacheByClaim`. These methods are responsible for updating the unrivaled time cache of edges in a chain. 

```solidity
    /// @inheritdoc IEdgeChallengeManager
    function multiUpdateTimeCacheByChildren(bytes32[] calldata edgeIds) public {
        for (uint256 i = 0; i < edgeIds.length; i++) {
            (bool updated, uint256 newValue) = store.updateTimerCacheByChildren(edgeIds[i]);
            if (updated) emit TimerCacheUpdated(edgeIds[i], newValue);
        }
    }

    /// @inheritdoc IEdgeChallengeManager
    function updateTimerCacheByChildren(bytes32 edgeId) public {
        (bool updated, uint256 newValue) = store.updateTimerCacheByChildren(edgeId);
        if (updated) emit TimerCacheUpdated(edgeId, newValue);
    }
```

If the assertion has edges in multiple levels each of them must update their unrivaled time cache via `updateTimerCacheByClaim`.

```solidity
    /// @inheritdoc IEdgeChallengeManager
    function updateTimerCacheByClaim(bytes32 edgeId, bytes32 claimingEdgeId) public {
        (bool updated, uint256 newValue) = store.updateTimerCacheByClaim(edgeId, claimingEdgeId, NUM_BIGSTEP_LEVEL);
        if (updated) emit TimerCacheUpdated(edgeId, newValue);
    }
```
The algorithm relies on accumulating enough unrivaled time to pass the confirmation threshold.

```solidity
    function confirmEdgeByTime(
        EdgeStore storage store,
        bytes32 edgeId,
        uint64 claimedAssertionUnrivaledBlocks,
        uint64 confirmationThresholdBlock
    ) internal returns (uint256) {
        if (!store.edges[edgeId].exists()) {
            revert EdgeNotExists(edgeId);
        }

        uint256 totalTimeUnrivaled = timeUnrivaledTotal(store, edgeId);
        totalTimeUnrivaled += claimedAssertionUnrivaledBlocks;

>>      if (totalTimeUnrivaled < confirmationThresholdBlock) {
            revert InsufficientConfirmationBlocks(totalTimeUnrivaled, confirmationThresholdBlock);
        }

        // we also check the edge is pending in setConfirmed()
        store.edges[edgeId].setConfirmed();

        // also checks that no other rival has been confirmed
        setConfirmedRival(store, edgeId);

        return total