sherlock-audit / 2024-02-optimism-2024-judging

6 stars 4 forks source link

Trust - Loss of bonds and the game due to wrong assumption in resolveClaim() #198

Closed sherlock-admin3 closed 6 months ago

sherlock-admin3 commented 7 months ago

Trust

high

Loss of bonds and the game due to wrong assumption in resolveClaim()

Summary

Incorrect resolveClaim() logic can make the wrong team win and scoop up the bonds of the other team.

Vulnerability Detail

The game runs a chess clock for each time, at every moment any unanswered claim counts against the pending response team, until it's timer runs out and it can no longer perform moves. To resolve a claim, the code logic needs to make sure that it can no longer be answered by a counter-move, i.e. the opponent's timer has ran to zero.

When the timer runs out, a claim can be resolved. At this point it loops over all challenges to the claim and if any have their own challenges, it reverts. Otherwise, it treats the challenge as successful and marks the claim as countered.

for (uint256 i = 0; i < challengeIndicesLen; ++i) {
    uint256 challengeIndex = challengeIndices[i];
    // INVARIANT: Cannot resolve a subgame containing an unresolved claim
    if (subgames[challengeIndex].length != 0) revert OutOfOrderResolution();
    ClaimData storage claim = claimData[challengeIndex];
    // If the child subgame is uncountered and further left than the current left-most counter,
    // update the parent subgame's `countered` address and the current `leftmostCounter`.
    // The left-most correct counter is preferred in bond payouts in order to discourage attackers
    // from countering invalid subgame roots via an invalid defense position. As such positions
    // cannot be correctly countered.
    // Note that correctly positioned defense, but invalid claimes can still be successfully countered.
    if (claim.counteredBy == address(0) && leftmostCounter.raw() > claim.position.raw()) {
        countered = claim.claimant;
        leftmostCounter = claim.position;
    }
}

The critical issue is that it is incorrect to assume any unanswered challenges are definitely correct challenges and treat them as final. It could well be that these are freshly made challenges and the opposing team still has time to respond to them. The function only takes the target claim's timer into account, not its challenge's timer. Logically we don't give the team of the claim's side sufficient time to answer the expired team's challenge. This can be abused to force incorrect resolution of the game.

GAME_DURATION = 100

Impact

Incorrect resolution of a claim, directly leading to loss of bonds and malicious roots being approved.

Code Snippet

Tool used

Manual Review

Recommendation

When looping over the challenges for the claim being resolved, make sure those have expired as well, otherwise the honest team may need to respond to them.

Duplicate of #8

sherlock-admin2 commented 5 months ago

The protocol team fixed this issue in the following PRs/commits: https://github.com/ethereum-optimism/optimism/pull/10148