An unbound loop at the resolveClaim() function allows anyone to permanently brick the claim
Summary
An unbound loop at the resolveClaim() function allows anyone to permanently brick the claim.
Vulnerability Detail
When a game is unwound, claims must be resolved. When a claim is resolved, it loops over all it's challenges to make sure they are not contested.
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 issue is that the amount of challenges is uncapped - an attacker could fill up the array so that any resolveClaim() call will deterministically revert due to Out-of-Gas error.
There are 5 SLOADs at minimum in the loop, each one is cold (2100 GAS). We can ignore additional opcode costs, so each loop is about 10k gas. To fill the 30M block gas limit in L1, 3000 challenges will be required (the accurate number is lower as there are a lot of unaccounted gas costs). A well resourced attacker may easily submit enough challenges at a particular level (especially at the lowest level, where the base costs is 0.08 ETH). Once that is done, note that the bond submitted by the challenged claim is stuck, because the claim cannot be resolved (even if all the challenges are defeated).
Impact
A particular claim would be stuck, as well as the whole game being unresolvable.
Code Snippet
Tool used
Manual Review
Recommendation
Refactor the code so that after a game ends, it's possible to do partial evaluation of challenges (i.e. between index X and Y). This will make it resistant to unbound gas consumption attacks.
Trust
high
An unbound loop at the resolveClaim() function allows anyone to permanently brick the claim
Summary
An unbound loop at the resolveClaim() function allows anyone to permanently brick the claim.
Vulnerability Detail
When a game is unwound, claims must be resolved. When a claim is resolved, it loops over all it's challenges to make sure they are not contested.
The issue is that the amount of challenges is uncapped - an attacker could fill up the array so that any
resolveClaim()
call will deterministically revert due to Out-of-Gas error.There are 5 SLOADs at minimum in the loop, each one is cold (2100 GAS). We can ignore additional opcode costs, so each loop is about 10k gas. To fill the 30M block gas limit in L1, 3000 challenges will be required (the accurate number is lower as there are a lot of unaccounted gas costs). A well resourced attacker may easily submit enough challenges at a particular level (especially at the lowest level, where the base costs is 0.08 ETH). Once that is done, note that the bond submitted by the challenged claim is stuck, because the claim cannot be resolved (even if all the challenges are defeated).
Impact
A particular claim would be stuck, as well as the whole game being unresolvable.
Code Snippet
Tool used
Manual Review
Recommendation
Refactor the code so that after a game ends, it's possible to do partial evaluation of challenges (i.e. between index X and Y). This will make it resistant to unbound gas consumption attacks.
Duplicate of #7