code-423n4 / 2024-05-arbitrum-foundation-findings

1 stars 2 forks source link

adversary can win the dispute game in the re-org event #40

Closed howlbot-integration[bot] closed 1 month ago

howlbot-integration[bot] commented 1 month ago

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: abi.encode(
                    ProofUtils.expansionFromLeaves(states2, 0, 1),
                    ProofUtils.generatePrefixProof(1, ArrayUtilsLib.slice(states2, 1, states2.length))
                ),
                proof: abi.encode(
                    ProofUtils.generateInclusionProof(ProofUtils.rehashed(states2), states2.length - 1),
                    genesisStateData,
                    ei.a2Data
                )
            })
        );

        _safeVmRoll(block.number + NUM_BLOCK_WAIT);
        assertEq(ei.challengeManager.timeUnrivaled(edge1Id), NUM_BLOCK_UNRIVALED, "Edge1 timer");
        assertEq(ei.challengeManager.timeUnrivaled(edge2Id), 0, "Edge2 timer");

    BisectionChildren memory children = bisect(ei.challengeManager, edge1Id, states1, 16, states1.length - 1);

    _safeVmRoll(block.number + 3);

    BisectionChildren memory children2 = bisect(ei.challengeManager, edge2Id, states2, 16, states2.length - 1);

    assertEq(ei.challengeManager.timeUnrivaled(edge1Id), 2, "Edge1 timer");

    _safeVmRoll(block.number + 3);

    //*@audit-info ------->>> the last bisection is done by edge 2 
    BisectionChildren memory children3 = bisect(ei.challengeManager, children2.lowerChildId, states2, 8, 16);

    assertEq(ei.challengeManager.timeUnrivaled(edge1Id), 2, "Edge1 timer");

    ChallengeEdge memory edge = ei.challengeManager.getEdge(edge1Id);

    bytes32 mutualId = edge.mutualIdMem();

    //*@audit-info ------>>> re-org happen here 
    ei.challengeManager.ResetFirstRival(mutualId);

    assertEq(ei.challengeManager.timeUnrivaled(edge1Id), 11, "Edge1 timer");

    _safeVmRoll(block.number + challengePeriodBlock );

    ei.challengeManager.updateTimerCacheByChildren(children3.lowerChildId);
    ei.challengeManager.updateTimerCacheByChildren(children3.upperChildId);
    ei.challengeManager.updateTimerCacheByChildren(children2.lowerChildId);
    ei.challengeManager.updateTimerCacheByChildren(children2.upperChildId);
    ei.challengeManager.updateTimerCacheByChildren(children.lowerChildId);
    ei.challengeManager.updateTimerCacheByChildren(children.upperChildId);
    ei.challengeManager.confirmEdgeByTime(edge1Id, ei.a1Data);
    vm.expectRevert(abi.encodeWithSelector(RivalEdgeConfirmed.selector, edge2Id, edge1Id));
    ei.challengeManager.confirmEdgeByTime(edge2Id, ei.a2Data);

    assertTrue(ei.challengeManager.getEdge(edge1Id).status == EdgeStatus.Confirmed, "Edge confirmed");

    return (ei, edge1Id);
}

Tools Used

manual view

Recommended Mitigation Steps

when rivals are born , there is no reason back to UNRIVALED again . So when rivals are born ,update unrivaled time and doesn't count for that UNRIVALED time anymore .

Assessed type

Context

gzeoneth commented 1 month ago

Invalid. The honest validator is expected to observe reorg and reissue the move. Reorg are considered as part of the adversary censoring budget.

c4-judge commented 1 month ago

Picodes marked the issue as unsatisfactory: Invalid

irving4444 commented 1 month ago

@gzeoneth @Picodes thanks for commenting in my report I want to point out another scenario that honest party lose funds or dispute game is won by malicious party .

1.malicious alice create edge 1 2.malicious bob create edge 2 3.Honest party come and create edge 3 4.first rival of those three mutual id become bob's edge 2 4.block re-org happen at block where malicious bob create edge 2 5.first rival will be UNRIVALED again

Here I do think honest party is only expected to observe their transaction is reverted or NOT at block -reorg. So honest party is unlikely to make new assertion cause their transaction is not reverted. Even honest party observe that scenario, he need to make new assertion and create edge in order to get rid of first rival from UNRIVALED , otherwise honest party gonna lose the dispute game . Only one true assertion can be made and the rest will be lose . honest party see that block-reorg which make first rival of their mutual id to UNRIVALED , he need to create new assertion which is gonna be lost .

scenario 1 Honest party observe the re-org and their transaction is not at block-reorg . So he might be chill and wait for rivals from malicious party until they can call confirm edge by time .Eventually he gonna lose the dispute game cause malicious party 's timer is bigger than his.

scenario 2 Honest party observe the re-org and see that he gonna lose the dispute game . He might create new assertion and create edge . However there 's only one true assertion and the rest assertion will be lost . So bonds from honest party 's second assertion will be lost .

It is assumed that off-chain computation costs are negligible, and honest validators can react to malicious action broadcasted on-chain immediately in the same block unless the malicious party spend their censorship budget. According to public known issues and As far as I understand it, re-org are not out scope .

gzeoneth commented 1 month ago

Honest validator is expected to be able to observe all reorg on the parent chain, for example, by watching the blockhash of the chain head. It can then recalculate all the required moves.

irving4444 commented 1 month ago

Even honest party observe the re-org , he needs to make new assertion and create new edge to remove the UNRIVALED. I think they can’t just reissue the move because block re-org happen at where second malicious actor’s created edge block , Not at honest party ‘s edge.

gzeoneth commented 1 month ago

When a reorg is observed, the validator should resync all its internal state like any proper reorg handling and (re)issue any necessary moves.

irving4444 commented 1 month ago

Does honest validator need to make new second assertion in order to make RIVAL again or am i missing something?

irving4444 commented 1 month ago

I agree that honest validators are expected to observe the re-org and can make necessary move . Firstly I pointed out that 1.Malicious Alice make layer zero edge 1 at Block-1 and first rival will set to UNRIVALED 2.Honest party make layer zero edge 2 at Block-2 and first rival will set to edge id 2 3.block re-org happen at block creating edge 2 4.first rival will set to UNRIVALED again

yes ofc , honest party observe the re-org and they can simply make edge 2 again and everything will be fine.

But I want to point out another scenario 1.Malicious alice create layer zero edge 1 at Block-1 and first rival will set to UNRIVALED 2.malicious Bob create layer zero edge 2 later at Block-2 and first rival will set to edge id 2 3.Honest party create layer zero edge 3 at Block-3 4.block re-org happen at block creating edge 2 and creating edge 2 transactions is reverted 5.first rival will set to UNRIVALED again

This time , honest party cannot simply reissue the move because malicious edge 2 is reverted and not honest party's edge 3.Honest party need to make first rival set to some edge id , otherwise malicious party can easily win the dispute game .

In order to make first rival to RIVAL again , new layer zero edge has to be created ( new assertion has to be created too) . But new layer zero edge cannot be created with previous same valid assertions. Protocol make sure that there’s only unique id exists. So honest validator need to create new assertion with different claim from valid one .

gzeoneth commented 1 month ago

Nothing prevents the honest validator to make new edges. There can be some interruption e.g. attacker being able to gain timer during the reorg period, but reorg are considered as part of the adversary censoring budget because it essentially is.

irving4444 commented 1 month ago

In order to make new layer zero edges , honest validator has to make new assertion and has to stake for creating edge . I think there is only one true assertion and edges , the rest edges will be lost .The latter created layer zero edge stake fund will be lost i guess.

gzeoneth commented 1 month ago

That is incorrect, no reorg will make a previously valid assertion/edge become invalid. Both assertions and edges are committed to an hash which not only includes its content, but also its history. If a reorg altered the history, the hash would change and the tx would revert. No stake would be lost.

irving4444 commented 1 month ago

Sir , I wanted to point it out that re-org will not make a previously valid assertion/edge become invalid. Re-org will make first rival mapping to UNRIVALED again . Honest validator need to create new edge to make it first rival mapping to RIVAL again . To make new edge , stake funds are needed and only one edge will be winner . Honest validator make edge 3 at block 3 and he need to stake some funds. Honest validator need to create edge 4 to make rival again , he also need to stake some funds . Only one of his edge will win and the other will be lost. Honest validator cannot create valid edge twice . There’s only one unique id exist.

gzeoneth commented 1 month ago

I am reading the original submission again and I think I can see where you get it wrong now:

  1. Unfornately , re-org event happen at block where BOB create layer zero edge id 2
  2. first rival of mutual ID for both layer zero edge id 1 & 2 will be set UNRIVALED again

This is incorrect. firstRival is set once the 2nd rival is created, the order of creation does not matter. The ResetFirstRival function in your POC is impossible, no re-org will reset the mapping to UNRIVALED while still have 2 edge with the same mutual id created.

irving4444 commented 1 month ago

ResetFirstRival function in your POC is impossible, no re-org will reset the mapping to UNRIVALED while still have 2 edge with the same mutual id created.

I just want to show that first rival is set to UNRIVALED again , but yea no re-org will reset the mapping to UNRIVALED while still have 2 edge with the same mutual id created. I accept the fact my POC is kinda messed up . and my orginal report can be fully mitigatable by re -issue the move .

But I want to point out the another scenario :) that cannot be simply mitigated by re-issue the move. ( pls don't skip reading below scenario , this is different from my original report scenario , I think we are having miscommunication )

honest party cannot simply reissue the move from observing the re-org in the following scenario.

//malicious alice create edge 1 
store.firstrival[mutualId] = UNRIVALED

//malicious BoB create edge 2
store.firstrival[mutualId] = edge id 2 

//honest party create edge 3 
store.firstrival[mutualId] = edge id 2 (actually we didn't set anything here  because first rival is already set , I made it for visualization  )

- protocol implement that when first rival is set to some edge id , we don't have to set anything again when next edge is created [look at here](https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/6f861c85b281a29f04daacfe17a2099d7dad5f8f/src/challengeV2/libraries/EdgeChallengeManagerLib.sol#L177-L189)

//block re-org happen at block where Malicious Bob create edge 2
( store.firstrival[mutualId] = edge id 2 ) This TX will revert 

- all state changes made within creating edge 2 TX will roll back .This include first rival setting to  edge 2 .

- Since first rival state setting to edge 2 is roll backed , first rival state will set to previous first rival state which is first rival setting to UNRIVALED when malicious alice created edge 1.

- There will be edge 1 & edge 3 existing  and first rival is set to empty (UNRIVALED) 

- Now malicious Alice and Honest party are rivaling each other while first rival is  empty . 

Honest party observe the re-org and see that first rival set to UNRIVALED .Honest party need to create edge to make first rival set to RIVAL again .

In my original report , I described that there 's one malicious party and one honest party . And I fully accept fact that honest party can simply reissue the move that reverted in block re-org .

In scenario I described later , there's two malicious party and one honest party .Second malicious party 's creating edge 2 is reverted , so honest party cannot simply re-issue the move . Honest party need to create new layer zero edge in order to first rival set to some edge ID again .When creating new layer zero edge , stake funds are needed and only one true assertion/ edge can be won .So honest party 's second edge (which is needed to make first rival set to some edge id again ) will be lost because protocol doesn't allow creating same edge id twice and there's only one winner .

Pls reconsider fact about malicious party's edge is reverted in re-org at my later exploit scenario , block re-org didn't happen at honest party's edge this time .I think we have miscommunication along the discussion :)

key - note : transaction reverted after re-org is mining again . So IF it's mined again , everything will be fine and nothing bad happen . So malicious Bob has to cancel the reverted transaction to exploit the system .To cancel a transaction that has been reverted due to a block reorganization (re-org) and ensure it is not mined again, you need to create a new transaction with the same nonce as the original transaction. This new transaction should be designed to replace the original one, effectively canceling it.

  1. Find the Nonce of the Original Transaction:

    2.Create a New Transaction with the Same Nonce:

    This new transaction can be a simple transaction sending a small amount of Ether to yourself or another address you control. The key is to use the same nonce and a higher gas price than the original transaction to ensure it is mined first

Picodes commented 1 month ago

@irving4444 thanks for your comment. Regarding your original report which is what's being judged here, as it is handled by reissuing the move, I'll keep the original verdict

irving4444 commented 1 month ago

@Picodes thanks for judging , my later scenario is slightly different from my original one , Could pls u take a look at it and make decision based on it ?

Picodes commented 1 month ago

@irving4444 unfortunately we aren't supposed to take into account different scenarios than the one of the original report

irving4444 commented 1 month ago

@Picodes Sir I want to point out another attack idea which cannot be simply mitigated .The based exploit scenarios are still same sir .

Picodes commented 1 month ago

Yes, my point is that PJQA is made to add clarification on the original report, and what's being judged is the original report and the impact it described. It's common that a report could have led to a higher severity if a better scenario was found