-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Copy FaultDisputeGame2.sol and FaultDisputGame2.t.sol and FaultDisputeActors2.sol #24
Conversation
merge optimism
* contracts-bedrock: L1Block interop Ports the `L1Block` contract from the `feat/interop` branch to develop using a pattern where we can extend the contracts. This sort of pattern may not always work but is an experiment in reducing feature branches. * contracts-bedrock: reorder params for func in L1BlockInterop * contracts-bedrock: add missing test for L1Block * contracts-bedrock: refactor tests for L1BlockInterop * contracts-bedrock: add L1BlockInterop to differential-testing.go * contracts-bedrock: add test for testDiff_encodeSetL1BlockValuesInterop_succeeds in Encoding tests * op-node: add L1BlockInterop to derive l1_block_info * op-node: add FuzzL1InfoInteropRoundTrip * op-node: add tests for L1Block to l1_block_info * contracts-bedrock: update snapshots for L1Block, L1BlockInterop * contracts-bedrock: update semver-lock for L1Block, L1BlockInterop * Revert "op-node: add tests for L1Block to l1_block_info" This reverts commit d2e599e. * Revert "op-node: add FuzzL1InfoInteropRoundTrip" This reverts commit e14007c. * Revert "op-node: add L1BlockInterop to derive l1_block_info" This reverts commit 4bb279b. * Revert "contracts-bedrock: add test for testDiff_encodeSetL1BlockValuesInterop_succeeds in Encoding tests" This reverts commit 12f9a07. * Revert "contracts-bedrock: add L1BlockInterop to differential-testing.go" This reverts commit 209669d. * contracts-bedrock: drop redundant test in tests for L1BlockInterop * contracts-bedrock: fix order of function args in L1Block * contracts-bedrock: update semver-lock for L1Block --------- Co-authored-by: Diego <[email protected]>
…0364) * ctb: Enable setting custom names in deploySafe() * Fix formatting * ctb: fix spacing on deploySafe log
* feat: Squash libraries in dispute contracts Reorganizes the libraries in the dispute contracts to remove circular dependencies. The existing dependency structure resolves within the monorepo's environment, but not when the monorepo's contracts are imported as a forge project dependency. This unblocks our ability to use these contracts for post-checks in `superchain-ops`. * Remove `IDisputeGame` dep in `LibUDT` * semver lock
merge from optimism
function resolveClaim(uint256 _claimIndex, uint256 _numToResolve) external { | ||
// INVARIANT: Resolution cannot occur unless the game is currently in progress. | ||
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); | ||
|
||
ClaimData storage subgameRootClaim = claimData[_claimIndex]; | ||
Duration challengeClockDuration = getChallengerDuration(_claimIndex); | ||
|
||
// INVARIANT: Cannot resolve a subgame unless the clock of its would-be counter has expired | ||
// INVARIANT: Assuming ordered subgame resolution, challengeClockDuration is always >= MAX_CLOCK_DURATION if all | ||
// descendant subgames are resolved | ||
if (challengeClockDuration.raw() < MAX_CLOCK_DURATION.raw()) revert ClockNotExpired(); | ||
|
||
// INVARIANT: Cannot resolve a subgame twice. | ||
if (resolvedSubgames[_claimIndex]) revert ClaimAlreadyResolved(); | ||
|
||
uint256[] storage challengeIndices = subgames[_claimIndex]; | ||
uint256 challengeIndicesLen = challengeIndices.length; | ||
|
||
// Uncontested claims are resolved implicitly unless they are the root claim. Pay out the bond to the claimant | ||
// and return early. | ||
if (challengeIndicesLen == 0 && _claimIndex != 0) { | ||
// In the event that the parent claim is at the max depth, there will always be 0 subgames. If the | ||
// `counteredBy` field is set and there are no subgames, this implies that the parent claim was successfully | ||
// stepped against. In this case, we pay out the bond to the party that stepped against the parent claim. | ||
// Otherwise, the parent claim is uncontested, and the bond is returned to the claimant. | ||
address counteredBy = subgameRootClaim.counteredBy; | ||
address recipient = counteredBy == address(0) ? subgameRootClaim.claimant : counteredBy; | ||
_distributeBond(recipient, subgameRootClaim); | ||
resolvedSubgames[_claimIndex] = true; | ||
return; | ||
} | ||
|
||
// Fetch the resolution checkpoint from storage. | ||
ResolutionCheckpoint memory checkpoint = resolutionCheckpoints[_claimIndex]; | ||
|
||
// If the checkpoint does not currently exist, initialize the current left most position as max u128. | ||
if (!checkpoint.initialCheckpointComplete) { | ||
checkpoint.leftmostPosition = Position.wrap(type(uint128).max); | ||
checkpoint.initialCheckpointComplete = true; | ||
|
||
// If `_numToResolve == 0`, assume that we can check all child subgames in this one callframe. | ||
if (_numToResolve == 0) _numToResolve = challengeIndicesLen; | ||
} | ||
|
||
// Assume parent is honest until proven otherwise | ||
uint256 lastToResolve = checkpoint.subgameIndex + _numToResolve; | ||
uint256 finalCursor = lastToResolve > challengeIndicesLen ? challengeIndicesLen : lastToResolve; | ||
for (uint256 i = checkpoint.subgameIndex; i < finalCursor; i++) { | ||
uint256 challengeIndex = challengeIndices[i]; | ||
|
||
// INVARIANT: Cannot resolve a subgame containing an unresolved claim | ||
if (!resolvedSubgames[challengeIndex]) 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) && checkpoint.leftmostPosition.raw() > claim.position.raw()) { | ||
checkpoint.counteredBy = claim.claimant; | ||
checkpoint.leftmostPosition = claim.position; | ||
} | ||
} | ||
|
||
// Increase the checkpoint's cursor position by the number of children that were checked. | ||
checkpoint.subgameIndex = uint32(finalCursor); | ||
|
||
// Persist the checkpoint and allow for continuing in a separate transaction, if resolution is not already | ||
// complete. | ||
resolutionCheckpoints[_claimIndex] = checkpoint; | ||
|
||
// If all children have been traversed in the above loop, the subgame may be resolved. Otherwise, persist the | ||
// checkpoint and allow for continuation in a separate transaction. | ||
if (checkpoint.subgameIndex == challengeIndicesLen) { | ||
address countered = checkpoint.counteredBy; | ||
|
||
// Once a subgame is resolved, we percolate the result up the DAG so subsequent calls to | ||
// resolveClaim will not need to traverse this subgame. | ||
subgameRootClaim.counteredBy = countered; | ||
|
||
// Mark the subgame as resolved. | ||
resolvedSubgames[_claimIndex] = true; | ||
|
||
// If the parent was not successfully countered, pay out the parent's bond to the claimant. | ||
// If the parent was successfully countered, pay out the parent's bond to the challenger. | ||
_distributeBond(countered == address(0) ? subgameRootClaim.claimant : countered, subgameRootClaim); | ||
} | ||
} |
Check warning
Code scanning / Slither
Reentrancy vulnerabilities Medium
External calls:
- _distributeBond(recipient,subgameRootClaim)
- WETH.unlock(_recipient,bond)
State variables written after the call(s):
- resolvedSubgames[_claimIndex] = true
FaultDisputeGame.resolvedSubgames can be used in cross function reentrancies:
- FaultDisputeGame.resolve()
- FaultDisputeGame.resolveClaim(uint256,uint256)
- FaultDisputeGame.resolvedSubgames
function step( | ||
uint256 _claimIndex, | ||
bool _isAttack, | ||
bytes calldata _stateData, | ||
bytes calldata _proof | ||
) | ||
public | ||
virtual | ||
{ | ||
// INVARIANT: Steps cannot be made unless the game is currently in progress. | ||
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); | ||
|
||
// Get the parent. If it does not exist, the call will revert with OOB. | ||
ClaimData storage parent = claimData[_claimIndex]; | ||
|
||
// Pull the parent position out of storage. | ||
Position parentPos = parent.position; | ||
// Determine the position of the step. | ||
Position stepPos = parentPos.move(_isAttack); | ||
|
||
// INVARIANT: A step cannot be made unless the move position is 1 below the `MAX_GAME_DEPTH` | ||
if (stepPos.depth() != MAX_GAME_DEPTH + 1) revert InvalidParent(); | ||
|
||
// Determine the expected pre & post states of the step. | ||
Claim preStateClaim; | ||
ClaimData storage postState; | ||
if (_isAttack) { | ||
// If the step position's index at depth is 0, the prestate is the absolute | ||
// prestate. | ||
// If the step is an attack at a trace index > 0, the prestate exists elsewhere in | ||
// the game state. | ||
// NOTE: We localize the `indexAtDepth` for the current execution trace subgame by finding | ||
// the remainder of the index at depth divided by 2 ** (MAX_GAME_DEPTH - SPLIT_DEPTH), | ||
// which is the number of leaves in each execution trace subgame. This is so that we can | ||
// determine whether or not the step position is represents the `ABSOLUTE_PRESTATE`. | ||
preStateClaim = (stepPos.indexAtDepth() % (1 << (MAX_GAME_DEPTH - SPLIT_DEPTH))) == 0 | ||
? ABSOLUTE_PRESTATE | ||
: _findTraceAncestor(Position.wrap(parentPos.raw() - 1), parent.parentIndex, false).claim; | ||
// For all attacks, the poststate is the parent claim. | ||
postState = parent; | ||
} else { | ||
// If the step is a defense, the poststate exists elsewhere in the game state, | ||
// and the parent claim is the expected pre-state. | ||
preStateClaim = parent.claim; | ||
postState = _findTraceAncestor(Position.wrap(parentPos.raw() + 1), parent.parentIndex, false); | ||
} | ||
|
||
// INVARIANT: The prestate is always invalid if the passed `_stateData` is not the | ||
// preimage of the prestate claim hash. | ||
// We ignore the highest order byte of the digest because it is used to | ||
// indicate the VM Status and is added after the digest is computed. | ||
if (keccak256(_stateData) << 8 != preStateClaim.raw() << 8) revert InvalidPrestate(); | ||
|
||
// Compute the local preimage context for the step. | ||
Hash uuid = _findLocalContext(_claimIndex); | ||
|
||
// INVARIANT: If a step is an attack, the poststate is valid if the step produces | ||
// the same poststate hash as the parent claim's value. | ||
// If a step is a defense: | ||
// 1. If the parent claim and the found post state agree with each other | ||
// (depth diff % 2 == 0), the step is valid if it produces the same | ||
// state hash as the post state's claim. | ||
// 2. If the parent claim and the found post state disagree with each other | ||
// (depth diff % 2 != 0), the parent cannot be countered unless the step | ||
// produces the same state hash as `postState.claim`. | ||
// SAFETY: While the `attack` path does not need an extra check for the post | ||
// state's depth in relation to the parent, we don't need another | ||
// branch because (n - n) % 2 == 0. | ||
bool validStep = VM.step(_stateData, _proof, uuid.raw()) == postState.claim.raw(); | ||
bool parentPostAgree = (parentPos.depth() - postState.position.depth()) % 2 == 0; | ||
if (parentPostAgree == validStep) revert ValidStep(); | ||
|
||
// INVARIANT: A step cannot be made against a claim for a second time. | ||
if (parent.counteredBy != address(0)) revert DuplicateStep(); | ||
|
||
// Set the parent claim as countered. We do not need to append a new claim to the game; | ||
// instead, we can just set the existing parent as countered. | ||
parent.counteredBy = msg.sender; | ||
} |
Check warning
Code scanning / Slither
Reentrancy vulnerabilities Medium
External calls:
- validStep = VM.step(_stateData,_proof,uuid.raw()) == postState.claim.raw()
State variables written after the call(s):
- parent.counteredBy = msg.sender
FaultDisputeGame.claimData can be used in cross function reentrancies:
- FaultDisputeGame._findStartingAndDisputedOutputs(uint256)
- FaultDisputeGame._findTraceAncestor(Position,uint256,bool)
- FaultDisputeGame.claimData
- FaultDisputeGame.claimDataLen()
- FaultDisputeGame.getChallengerDuration(uint256)
- FaultDisputeGame.initialize()
- FaultDisputeGame.move(uint256,Claim,bool)
- FaultDisputeGame.resolve()
- FaultDisputeGame.resolveClaim(uint256,uint256)
- FaultDisputeGame.step(uint256,bool,bytes,bytes)
function addLocalData(uint256 _ident, uint256 _execLeafIdx, uint256 _partOffset) external { | ||
// INVARIANT: Local data can only be added if the game is currently in progress. | ||
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); | ||
|
||
(Claim starting, Position startingPos, Claim disputed, Position disputedPos) = | ||
_findStartingAndDisputedOutputs(_execLeafIdx); | ||
Hash uuid = _computeLocalContext(starting, startingPos, disputed, disputedPos); | ||
|
||
IPreimageOracle oracle = VM.oracle(); | ||
if (_ident == LocalPreimageKey.L1_HEAD_HASH) { | ||
// Load the L1 head hash | ||
oracle.loadLocalData(_ident, uuid.raw(), l1Head().raw(), 32, _partOffset); | ||
} else if (_ident == LocalPreimageKey.STARTING_OUTPUT_ROOT) { | ||
// Load the starting proposal's output root. | ||
oracle.loadLocalData(_ident, uuid.raw(), starting.raw(), 32, _partOffset); | ||
} else if (_ident == LocalPreimageKey.DISPUTED_OUTPUT_ROOT) { | ||
// Load the disputed proposal's output root | ||
oracle.loadLocalData(_ident, uuid.raw(), disputed.raw(), 32, _partOffset); | ||
} else if (_ident == LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER) { | ||
// Load the disputed proposal's L2 block number as a big-endian uint64 in the | ||
// high order 8 bytes of the word. | ||
|
||
// We add the index at depth + 1 to the starting block number to get the disputed L2 | ||
// block number. | ||
uint256 l2Number = startingOutputRoot.l2BlockNumber + disputedPos.traceIndex(SPLIT_DEPTH) + 1; | ||
|
||
oracle.loadLocalData(_ident, uuid.raw(), bytes32(l2Number << 0xC0), 8, _partOffset); | ||
} else if (_ident == LocalPreimageKey.CHAIN_ID) { | ||
// Load the chain ID as a big-endian uint64 in the high order 8 bytes of the word. | ||
oracle.loadLocalData(_ident, uuid.raw(), bytes32(L2_CHAIN_ID << 0xC0), 8, _partOffset); | ||
} else { | ||
revert InvalidLocalIdent(); | ||
} | ||
} |
Check warning
Code scanning / Slither
Unused return Medium
function addLocalData(uint256 _ident, uint256 _execLeafIdx, uint256 _partOffset) external { | ||
// INVARIANT: Local data can only be added if the game is currently in progress. | ||
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); | ||
|
||
(Claim starting, Position startingPos, Claim disputed, Position disputedPos) = | ||
_findStartingAndDisputedOutputs(_execLeafIdx); | ||
Hash uuid = _computeLocalContext(starting, startingPos, disputed, disputedPos); | ||
|
||
IPreimageOracle oracle = VM.oracle(); | ||
if (_ident == LocalPreimageKey.L1_HEAD_HASH) { | ||
// Load the L1 head hash | ||
oracle.loadLocalData(_ident, uuid.raw(), l1Head().raw(), 32, _partOffset); | ||
} else if (_ident == LocalPreimageKey.STARTING_OUTPUT_ROOT) { | ||
// Load the starting proposal's output root. | ||
oracle.loadLocalData(_ident, uuid.raw(), starting.raw(), 32, _partOffset); | ||
} else if (_ident == LocalPreimageKey.DISPUTED_OUTPUT_ROOT) { | ||
// Load the disputed proposal's output root | ||
oracle.loadLocalData(_ident, uuid.raw(), disputed.raw(), 32, _partOffset); | ||
} else if (_ident == LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER) { | ||
// Load the disputed proposal's L2 block number as a big-endian uint64 in the | ||
// high order 8 bytes of the word. | ||
|
||
// We add the index at depth + 1 to the starting block number to get the disputed L2 | ||
// block number. | ||
uint256 l2Number = startingOutputRoot.l2BlockNumber + disputedPos.traceIndex(SPLIT_DEPTH) + 1; | ||
|
||
oracle.loadLocalData(_ident, uuid.raw(), bytes32(l2Number << 0xC0), 8, _partOffset); | ||
} else if (_ident == LocalPreimageKey.CHAIN_ID) { | ||
// Load the chain ID as a big-endian uint64 in the high order 8 bytes of the word. | ||
oracle.loadLocalData(_ident, uuid.raw(), bytes32(L2_CHAIN_ID << 0xC0), 8, _partOffset); | ||
} else { | ||
revert InvalidLocalIdent(); | ||
} | ||
} |
Check warning
Code scanning / Slither
Unused return Medium
function addLocalData(uint256 _ident, uint256 _execLeafIdx, uint256 _partOffset) external { | ||
// INVARIANT: Local data can only be added if the game is currently in progress. | ||
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); | ||
|
||
(Claim starting, Position startingPos, Claim disputed, Position disputedPos) = | ||
_findStartingAndDisputedOutputs(_execLeafIdx); | ||
Hash uuid = _computeLocalContext(starting, startingPos, disputed, disputedPos); | ||
|
||
IPreimageOracle oracle = VM.oracle(); | ||
if (_ident == LocalPreimageKey.L1_HEAD_HASH) { | ||
// Load the L1 head hash | ||
oracle.loadLocalData(_ident, uuid.raw(), l1Head().raw(), 32, _partOffset); | ||
} else if (_ident == LocalPreimageKey.STARTING_OUTPUT_ROOT) { | ||
// Load the starting proposal's output root. | ||
oracle.loadLocalData(_ident, uuid.raw(), starting.raw(), 32, _partOffset); | ||
} else if (_ident == LocalPreimageKey.DISPUTED_OUTPUT_ROOT) { | ||
// Load the disputed proposal's output root | ||
oracle.loadLocalData(_ident, uuid.raw(), disputed.raw(), 32, _partOffset); | ||
} else if (_ident == LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER) { | ||
// Load the disputed proposal's L2 block number as a big-endian uint64 in the | ||
// high order 8 bytes of the word. | ||
|
||
// We add the index at depth + 1 to the starting block number to get the disputed L2 | ||
// block number. | ||
uint256 l2Number = startingOutputRoot.l2BlockNumber + disputedPos.traceIndex(SPLIT_DEPTH) + 1; | ||
|
||
oracle.loadLocalData(_ident, uuid.raw(), bytes32(l2Number << 0xC0), 8, _partOffset); | ||
} else if (_ident == LocalPreimageKey.CHAIN_ID) { | ||
// Load the chain ID as a big-endian uint64 in the high order 8 bytes of the word. | ||
oracle.loadLocalData(_ident, uuid.raw(), bytes32(L2_CHAIN_ID << 0xC0), 8, _partOffset); | ||
} else { | ||
revert InvalidLocalIdent(); | ||
} | ||
} |
Check warning
Code scanning / Slither
Unused return Medium
function addLocalData(uint256 _ident, uint256 _execLeafIdx, uint256 _partOffset) external { | ||
// INVARIANT: Local data can only be added if the game is currently in progress. | ||
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); | ||
|
||
(Claim starting, Position startingPos, Claim disputed, Position disputedPos) = | ||
_findStartingAndDisputedOutputs(_execLeafIdx); | ||
Hash uuid = _computeLocalContext(starting, startingPos, disputed, disputedPos); | ||
|
||
IPreimageOracle oracle = VM.oracle(); | ||
if (_ident == LocalPreimageKey.L1_HEAD_HASH) { | ||
// Load the L1 head hash | ||
oracle.loadLocalData(_ident, uuid.raw(), l1Head().raw(), 32, _partOffset); | ||
} else if (_ident == LocalPreimageKey.STARTING_OUTPUT_ROOT) { | ||
// Load the starting proposal's output root. | ||
oracle.loadLocalData(_ident, uuid.raw(), starting.raw(), 32, _partOffset); | ||
} else if (_ident == LocalPreimageKey.DISPUTED_OUTPUT_ROOT) { | ||
// Load the disputed proposal's output root | ||
oracle.loadLocalData(_ident, uuid.raw(), disputed.raw(), 32, _partOffset); | ||
} else if (_ident == LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER) { | ||
// Load the disputed proposal's L2 block number as a big-endian uint64 in the | ||
// high order 8 bytes of the word. | ||
|
||
// We add the index at depth + 1 to the starting block number to get the disputed L2 | ||
// block number. | ||
uint256 l2Number = startingOutputRoot.l2BlockNumber + disputedPos.traceIndex(SPLIT_DEPTH) + 1; | ||
|
||
oracle.loadLocalData(_ident, uuid.raw(), bytes32(l2Number << 0xC0), 8, _partOffset); | ||
} else if (_ident == LocalPreimageKey.CHAIN_ID) { | ||
// Load the chain ID as a big-endian uint64 in the high order 8 bytes of the word. | ||
oracle.loadLocalData(_ident, uuid.raw(), bytes32(L2_CHAIN_ID << 0xC0), 8, _partOffset); | ||
} else { | ||
revert InvalidLocalIdent(); | ||
} | ||
} |
Check warning
Code scanning / Slither
Unused return Medium
function addLocalData(uint256 _ident, uint256 _execLeafIdx, uint256 _partOffset) external { | ||
// INVARIANT: Local data can only be added if the game is currently in progress. | ||
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); | ||
|
||
(Claim starting, Position startingPos, Claim disputed, Position disputedPos) = | ||
_findStartingAndDisputedOutputs(_execLeafIdx); | ||
Hash uuid = _computeLocalContext(starting, startingPos, disputed, disputedPos); | ||
|
||
IPreimageOracle oracle = VM.oracle(); | ||
if (_ident == LocalPreimageKey.L1_HEAD_HASH) { | ||
// Load the L1 head hash | ||
oracle.loadLocalData(_ident, uuid.raw(), l1Head().raw(), 32, _partOffset); | ||
} else if (_ident == LocalPreimageKey.STARTING_OUTPUT_ROOT) { | ||
// Load the starting proposal's output root. | ||
oracle.loadLocalData(_ident, uuid.raw(), starting.raw(), 32, _partOffset); | ||
} else if (_ident == LocalPreimageKey.DISPUTED_OUTPUT_ROOT) { | ||
// Load the disputed proposal's output root | ||
oracle.loadLocalData(_ident, uuid.raw(), disputed.raw(), 32, _partOffset); | ||
} else if (_ident == LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER) { | ||
// Load the disputed proposal's L2 block number as a big-endian uint64 in the | ||
// high order 8 bytes of the word. | ||
|
||
// We add the index at depth + 1 to the starting block number to get the disputed L2 | ||
// block number. | ||
uint256 l2Number = startingOutputRoot.l2BlockNumber + disputedPos.traceIndex(SPLIT_DEPTH) + 1; | ||
|
||
oracle.loadLocalData(_ident, uuid.raw(), bytes32(l2Number << 0xC0), 8, _partOffset); | ||
} else if (_ident == LocalPreimageKey.CHAIN_ID) { | ||
// Load the chain ID as a big-endian uint64 in the high order 8 bytes of the word. | ||
oracle.loadLocalData(_ident, uuid.raw(), bytes32(L2_CHAIN_ID << 0xC0), 8, _partOffset); | ||
} else { | ||
revert InvalidLocalIdent(); | ||
} | ||
} |
Check warning
Code scanning / Slither
Unused return Medium
unit test:
cd optimism/packages/contracts-bedrock/
forge test --mc FaultDisputeGame2_Test