Arbitrum BoLD - Emmanuel's results

A new dispute protocol that unlocks permissionless validation for Arbitrum chains.

General Information

Platform: Code4rena

Start Date: 10/05/2024

Pot Size: $300,500 USDC

Total HM: 4

Participants: 27

Period: 17 days

Judge: Picodes

Total Solo HM: 1

Id: 375

League: ETH

Arbitrum Foundation

Findings Distribution

Researcher Performance

Rank: 7/27

Findings: 1

Award: $12,737.19

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: Kow

Also found by: Emmanuel, SpicyMeatball, xuwinnie

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
:robot:_110_group
duplicate-2

Awards

12737.1941 USDC - $12,737.19

External Links

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:

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:

    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

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 during edge bisection, the latter will not change.

Now, within checkClaimIdLink function, also check that :

    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
            );
        }
@>      if (store.edges[edgeId].uniqueId != store .edges[claimingEdgeId].uniqueId){
            revert("UniqueId must match");
        }
        // 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
            );
        }
    }

Assessed type

Context

#0 - c4-judge

2024-06-03T22:30:20Z

Picodes marked the issue as satisfactory

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter