Organization
- @optimistic-ethereum
Engagement Type
Cantina Reviews
Period
-
Repositories
Findings
Low Risk
6 findings
4 fixed
2 acknowledged
Informational
8 findings
1 fixed
7 acknowledged
Low Risk6 findings
Initial super deployments accept invalid anchors
State
- Acknowledged
Severity
- Severity: Low
Submitted by
r0bert
Description
_assertValidFullConfigaccepts any nonzero initial anchor belowuint64.max. When a permissionless game is enabled, it only excludes the exact0xdeadplaceholder. U20 commit 9714d4dd2c77052fad62505598db3568b1458f7d (#21720) also newly permitsSUPER_CANNON_KONAto be the respected game at initial deployment. The function never proves that the supplied root is the chain's SuperV1 genesis commitment or that its sequence is a timestamp the challenger can retrieve.For example, a chain operator can accidentally supply a legacy V0 output root and L2 block number.
OPContractsManagerV2.deploy, the canonicalDeployOPChainscript, andOPContractsManagerStandardValidatorall accept that tuple. A freshAnchorStateRegistrythen exposes it as the starting proposal for permissionless super games.An arbitrary proposer can create a correctly encoded super game one sequence later. The production challenger rejects the starting block number as a pre-genesis timestamp, so it cannot create a player. The unopposed claim expires into
DEFENDER_WINSand becomes claim-valid. Consequently, its attacker-selected per-chain output root can authorize fabricated withdrawals from all ETH or tokens later held by the portal and lockbox.The official
op-deployer preparepipeline computes the correct genesis proposal, which reduces likelihood. The public OPCM API and supported Forge deployment script still accept the malformed tuple. The repository's own permissionless deployment fixture uses an arbitrary non-placeholder root and sequence.Recommendation
Do not allow a fresh chain to respect a permissionless super game based only on a caller-supplied
Proposal. Bootstrap withSUPER_PERMISSIONED, then permitSUPER_CANNON_KONAonly after a trusted, canonical super game has installed an authenticated genesis or later anchor. If direct permissionless bootstrap remains supported, require a proposal attested by the prepared genesis process and make bothDeployOPChainandOPContractsManagerStandardValidatorreject values that do not match it.OP Labs: Acknowledged. The starting anchor is a trusted deployment input whose correctness is checked offchain by the operator and the supported
op-deployerflow calculates it.StandardValidator accepts arbitrary same-version super-permissioned game implementations
State
Severity
- Severity: Low
Submitted by
r0bert
Description
The U20 StandardValidator stores the release's approved
superPermissionedDisputeGameImpladdress and passes it into the new specialized SPDG validation path. However,assertValidSuperPermissionedDisputeGamedoes not compare the factory's registered implementation with that address. It callsversion()on both contracts and accepts equality between the returned strings.The registered candidate controls its own
version()result. A different implementation can therefore report1.0.0, retain the canonical packed ASR and proposer arguments and make the validator return an empty success string. The other SPDG checks authenticate only the ASR and the proposer bytes stored by the factory; they do not prove that the registered implementation enforces those arguments or any game semantics.This defeats the validator's stated purpose of ensuring that L1 contracts are configured correctly before and after an upgrade. The production SPDG enforces proposer authorization, well-formed calldata, root-claim binding to a decoded super-root preimage and a sequence newer than the anchor. A same-version substitute can omit all of these checks. Once cloned by the factory, it can allow an untrusted caller to create an immediately defender-winning game whose
rootClaimByChainIdreturns an attacker-selected output root committing to fabricated MessagePasser state. After the normal ASR air gap, that game is claim-valid and can authorize fabricated withdrawal proofs throughOptimismPortal2.During an upgrade or manual recovery, the DGF owner can register an incorrect, compromised or lookalike implementation while retaining the canonical packed ASR and proposer arguments. The substitute reports the official
1.0.0version, soOPContractsManagerStandardValidator.validate(..., false)returns an empty success string even though the implementation is different. An arbitrary account can then create aDEFENDER_WINSgame with an attacker-selected output root. The factory registration, respected-game flag, resolution timestamp and normal ASR air gap make the game claim-valid, allowing the attacker to prove and finalize a withdrawal against the fabricated root and release portal or shared-lockbox assets. The condition requires a privileged implementation change, giving it low-to-moderate likelihood; exploitation after that change is permissionless and requires only the ordinary finality and proof delays.Recommendation
Require
game.gameAddress == _impls.expectedGameImplinassertValidSuperPermissionedDisputeGame; do not use a value returned by the candidate itself as identity evidence. If operational requirements permit multiple approved builds, validate an immutable allowlist of exact runtime code hashes or implementation addresses. Apply the same identity rule to every standard and migration validator path and add a negative test where a different implementation returns the expected version and canonical getters.OP Labs: Fixed in commit 283bb45.
Cantina: Fix verified. StandardValidator now requires the factory's registered
SUPER_PERMISSIONEDimplementation to equal the release-approved implementation address. A same-version lookalike is rejected withSPDG-150.U20 StandardValidator does not bind the SystemConfig chain ID to the validated super-game domain
State
Severity
- Severity: Low
Submitted by
r0bert
Description
The U20 StandardValidator accepts the expected L2 chain ID as caller-supplied
ValidationInputDev.l2ChainID, but the new super-permissioned-game branch returns throughassertValidSuperPermissionedDisputeGamewithout comparing that value withSystemConfig.l2ChainId(). The general SystemConfig validation at lines 285-303 also does not perform this comparison. Super-game implementations deliberately store zero rather than a member chain ID, so the remaining game-argument checks cannot establish the missing binding.This matters because
OptimismPortal2.proveWithdrawalTransactionselects a member output root by callingrootClaimByChainId(systemConfig.l2ChainId()). A validator invocation for chain X can therefore return an empty success string while the Portal will use chain Y's root. If Y is absent from the super root, withdrawals for the affected Portal revert. If Y is present, the Portal checks withdrawals against Y's MessagePasser root rather than X's, creating a wrong-domain settlement configuration and potentially allowing the same Y withdrawal evidence to be settled through more than one Portal.For example, an imported deployment or reinitialization can store chain ID Y in
SystemConfigwhile the validator input and intended domain use chain ID X. The DGF, ASR, implementations, proposer and all other checked fields can remain canonical. U20 validates the super-game configuration without comparing X and Y, so it returns an empty success string. The Portal later selects Y from every super root. It either cannot find Y and blocks withdrawals or verifies an X Portal withdrawal against Y's MessagePasser state. Under a malformed shared topology, the same Y withdrawal evidence could therefore be settled through more than one Portal using pooled liquidity. This requires a malformed deployment, reinitialization or operator-supplied validation input rather than an unprivileged setter. Canonical tooling normally derives the values from one intent, so the mismatch has low likelihood. The validator is expressly intended to catch this configuration error, but validation is advisory and trusted governance can repair it.Recommendation
In
assertValidSystemConfig, require_sysCfg.l2ChainId() == _input.l2ChainIDby passing the expected chain ID into the helper. Keep the existing legacy-game argument check as an additional predicate.OP Labs: Fixed in commit cba37e8.
Cantina: Fix verified. StandardValidator now requires
SystemConfig.l2ChainId()to equal the chain ID supplied for validation and reportsSYSCON-140on a mismatch.Super-root migration can retain a legacy anchor and leave permissionless invalid claims unchallengeable
State
- Acknowledged
Severity
- Severity: Low
Submitted by
r0bert
Description
U20 changes
SUPER_ROOT_GAMES_MIGRATIONfrom opt-in to unconditionally enabled and extends the release allowance foroverrides.cfg.startingAnchorRoot, but never requires the instruction when an upgrade disables the legacy output-root games and enables a super-root game._loadFullConfigfalls back toAnchorStateRegistry.getStartingAnchorRoot()when that instruction is absent. This is not the current anchor whenanchorGameis set: the current anchor is the legacy game's output root and L2 block number._assertValidFullConfigvalidates the anchor only for initial deployments, so the upgrade and the post-upgradeStandardValidatorboth accept this mixed state.The subsequent
AnchorStateRegistry.initializecall receives the unchanged original starting root. ItsrootChangedcheck is therefore false and it does not clear the existing legacyanchorGame. NewSuperFaultDisputeGameinstances consequently copy the legacy output-root hash and L2 block number intostartingProposal, although the super trace interprets that number as a timestamp and the hash as a super-root commitment.This breaks the honest challenger before any onchain move is made. The challenger constructs its top prestate with
SuperRootAtTimestamp(legacyL2BlockNumber). For a normal OP chain, the legacy block number is far below the L2 genesis Unix timestamp, soop-noderejects it inTargetBlockNumberas pre-genesis. That provider error is notErrInvalidPrestate; the scheduler therefore refuses to create the game player even whenallow-invalid-prestateis enabled.Because of this issue, after an accepted but incomplete U20 migration, any account can create a permissionless
SUPER_CANNON_KONAgame with an arbitrary, correctly encoded super root at sequence numberN + 1. Because honest challengers cannot instantiate a trace for the retained legacy prestate, no counter is posted. Once the game clock expires it resolvesDEFENDER_WINS; after the finality delay, the AnchorStateRegistry treats it as a valid anchor. The attacker's per-chain output root can then be used to prove fabricated withdrawals throughOptimismPortal2, putting all ETH and tokens secured by that portal at risk.The precondition for this issue to be triggered is an otherwise authorized U20 migration that installs the super game family but omits one optional-looking extra instruction, followed by normal guardian activation of the permissionless super game (or a migration that selects it immediately). For example, suppose a permissionless chain has a finalized legacy output-root game as its anchor at L2 block
N. If the chain's ProxyAdmin owner disables legacy games, enables the super game family and omitsoverrides.cfg.startingAnchorRoot,OPContractsManagerV2.upgradesucceeds, retains the legacyanchorGameand theStandardValidatorreturns no errors. Once the guardian selectsSUPER_CANNON_KONAas respected, an unprivileged attacker can create a super game containing an attacker-selected output root at timestampN + 1and pay the configured initial bond. Honest challengers querysuperroot_atTimestamp(N), butop-noderejectsNas pre-genesis, so the scheduler never creates a player. The attacker waits for the game clock and finality delay, resolves the uncontested root asDEFENDER_WINS, closes the game and uses the now-valid anchor to prove and finalize withdrawals against the attacker-selected per-chain output root. This is a realistic configuration failure rather than a compromised-admin attack: the code comment says the migration requires the override, the public upgrade API does not enforce that requirement and the StandardValidator reports the resulting system as valid. The repository's ownSuperGameTestInitperforms this exact omission. The official fork migration helper includes the instruction, which reduces likelihood and the guardian has the dispute/finality window to detect and blacklist a malicious game.Recommendation
When an upgrade transitions from a legacy respected game type to any super-root game type, require a
startingAnchorRootinstruction instead of merely permitting it. Validate that the supplied root is nonzero, that its sequence number is a valid timestamp strictly greater than the currentgetAnchorRoot()sequence and ensure the legacyanchorGameis cleared atomically. The StandardValidator should also reject a super-root configuration whose active anchor is still a legacy game type. If feasible, have migration tooling supply the encoded super-root preimage and verify its hash, rather than accepting an unbound hash alone.OP Labs: Acknowledged. OPCM cannot fully validate a new anchor onchain. OP Labs will instead verify that an anchor root is set and matches a canonical, finalized block through the
superchain-opsupgrade task or a manual verification process.SuperPermissioned game initialization can trap a configured bond
State
- Fixed
Severity
- Severity: Low
Submitted by
slowfi
Description
The standalone
SuperPermissionedDisputeGame.initializefunction is payable, but the game has no bond accounting, distribution, or withdrawal path for an initialization value. A nonzero initialization bond sent to a SuperPermissioned clone therefore remains trapped in that clone.The normal U20 configuration validator checks that
SUPER_PERMISSIONEDhas a zero initialization bond, but the game implementation itself does not enforce that invariant. Any configuration path that supplies a nonzero bond can consequently create publicly usable game clones that hold unrecoverable ETH. Malformed privileged game configuration can permanently lock ETH in each created SuperPermissioned clone. The amount is bounded per clone, but the condition is repeated for every game created under the affected configuration.Recommendation
Make the SuperPermissioned initializer nonpayable if it must never receive a bond, or explicitly reject nonzero
msg.valueand enforce the zero bond invariant at every configuration mutation path. Add a balance conservation test for clone creation.OP Labs: Fixed in commit 44eb954.
Cantina: Fix verified.
SuperPermissionedDisputeGame.initializenow rejects any nonzeromsg.valuewithIncorrectBondAmountbefore completing initialization.The validator does not bind dispute-game registries to the Portal's registry
State
- Fixed
Severity
- Severity: Low
Submitted by
slowfi
Description
The standard validator selects super mode from the Portal's
AnchorStateRegistry, but decodes anAnchorStateRegistryindependently from each dispute game factory's encoded arguments. It validates each decoded registry's implementation, ProxyAdmin, SystemConfig, DGF, retirement timestamp, and anchor, but never requires the decoded registry to equal the Portal's registry.An authorized configuration can therefore use registry A for the Portal while the SuperPermissioned game arguments point to a second ordinary registry B. If both registries pass their local checks but respect different game types, standard validation can return no errors for the split. A proposer can then create a game that snapshots B's respected type while the Portal consults A, leaving the game and Portal registry identity unbound. A trusted configuration mistake can therefore make StandardValidator report success for a registry split that undermines the intended dispute game and withdrawal trust boundary.
Recommendation
Read
portal.anchorStateRegistry()once and require every enabled game's encoded registry to equal it. Validate the Portal registry through the same deep checks, and add a negative test where two locally valid registries have different respected game types.OP Labs: Fixed in commit ef695a3.
Cantina: Fix verified. AnchorStateRegistry validation now requires the registry encoded by each game to equal
OptimismPortal2.anchorStateRegistry()and reportsANCHORP-70on a mismatch. The release covers legacy, super, and ZK game paths with negative regression tests.
Informational8 findings
Initial permissionless deployments accept unavailable VM prestates
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
r0bert
Description
U20 allows a fresh chain to enable and immediately respect
SUPER_CANNON_KONA, but_assertValidFullConfigaccepts its fault-VM absolute prestate whenever the commitment is nonzero. It does not require an approved release commitment or prove that a matching Cannon/Kona state artifact is available. The supportedDeployOPChainwrapper applies the same nonzero check.OPContractsManagerStandardValidatorthen compares the installed value with another caller-supplied copy, so it also reports the configuration as valid.The production challenger must resolve the exact onchain commitment before it constructs a type-9 game player. It checks local files and the configured prestate URL for
.bin.gz,.json.gzand.jsonartifacts, then recomputes the commitment. An unavailable or mismatched artifact returnsErrPrestateUnavailablebefore player construction. The scheduler's optional invalid-prestate exception is ineffective because it is applied later and only acceptsErrInvalidPrestate.For example, an operator can make a one-word typo in the selected prestate or supply a stale unpublished commitment. All deployment and validation calls succeed. Once the chain is funded, an untrusted proposer can create a game containing a fabricated per-chain output root. No standard challenger can construct a player. The root claim therefore expires to
DEFENDER_WINS, becomes an ASR anchor after finality and authorizes a withdrawal that never occurred on L2.The impact is an arbitrary loss of ETH or tokens held by the portal or shared lockbox. The precondition is an honest deployment configuration error, not malicious governance. U20's
op-deployer prestatecommand explicitly accepts arbitrary 32-byte CLI, environment and per-chain override values other than one reserved placeholder, so this error is realistic. Exploitation after deployment is permissionless.Recommendation
Do not let a fresh chain respect a permissionless game unless the selected absolute prestate is authenticated and available. Bind each permitted game type to an immutable allowlist of release-approved prestate commitments in OPCM and StandardValidator. Before broadcasting deployment, make
op-deployerdownload or load the selected artifact, recompute its commitment with the production state converter and reject any mismatch or unavailable file.If arbitrary prestate overrides must remain supported, initially respect
SUPER_PERMISSIONED. EnableSUPER_CANNON_KONAonly after an end-to-end challenger dry run has resolved the artifact and constructed the game player.OP Labs: Acknowledged. The contracts intentionally validate the prestate commitment, while artifact availability and correctness remain part of the trusted release-approval process.
Implementations tuple removal can silently misdecode legacy container responses
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
r0bert
Description
U20 removes
protocolVersionsImplfrom the second position of the all-staticImplementationsreturn tuple. Return types do not contribute to a function selector, so theimplementations()selector remains unchanged across the release boundary.A caller compiled against the U20 interface can therefore call a pre-U20 container and successfully decode its 19-word response as the new 18-field struct. The decoder consumes the first 18 words and ignores the final word:
l1ERC721BridgeImplreceives the oldprotocolVersionsImpl, every following field is shifted,storageSetterImplreceives the oldzkDisputeGameImpland the actual oldstorageSetterImplis discarded.No target production path was found that queries a legacy container through the new interface, so this is not a demonstrated protocol exploit. It is an informational compatibility hazard for release-spanning deployment, inventory, validation or monitoring tooling that selects an ABI independently of the container version.
Recommendation
Use a versioned getter with a distinct selector for the new tuple or query and validate the container version before selecting the return schema. Low-level compatibility tooling should require the exact expected return length before decoding. Regenerate dependent ABIs during the U20 migration and add negative tests for both old-caller/new-container and new-caller/old-container combinations.
OP Labs: Acknowledged. Supported deployment tooling upgrades the ABI and contracts together and the opposite stale-ABI direction reverts safely.
U20 upgrades canonicalize divergent legacy fee-scalar state
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
r0bert
Description
U20's upgrade configuration preserves
basefeeScalar()andblobbasefeeScalar()separately.SystemConfig.initializethen passes those values to_setGasConfigEcotone, which reconstructs the packedscalar()value rather than preserving its prior raw value.Before U20, the deprecated
setGasConfig(uint256,uint256)function could updatescalar()without updating the two mirror fields. A chain that used that setter can therefore enter the upgrade with divergent values, after which U20 replacesscalar()with the version-1 encoding derived from the mirrors. A target-revision upgrade PoC reproduces this state transition.The U20 release notice explicitly describes this possible divergence and the code intentionally treats the mirror fields as canonical, so the behavior is not a security vulnerability. It remains operationally relevant because the upgrade changes an observable configuration value on affected legacy deployments and may surprise tooling that expected raw storage preservation.
Recommendation
Have upgrade tooling compare the current
scalar()value with the value reconstructed frombasefeeScalar()andblobbasefeeScalar()before execution. Surface any mismatch and the exact post-upgrade value for explicit operator acknowledgement.OP Labs: Acknowledged. No chain included in this upgrade used the deprecated setter and the component scalar fields are intentionally treated as canonical.
tx.origin enforces an EOA-only proposer model and propagates authority through intermediaries
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The rewritten standalone
SuperPermissionedDisputeGame.initializeauthorizes creation withtx.origin == proposer. If the configured proposer is a Safe, module, account abstraction wallet, relayer, or another contract, the check can never pass becausetx.originis always an EOA. This creates a proposal liveness failure for supported contract based proposer flows.When the proposer is an EOA, the authority is implicitly propagated through every intermediary contract it calls. An intermediary can create a defender winning game while the EOA remains the transaction origin. The game resolves immediately to
DEFENDER_WINS, subject to the normal ASR respect, blacklist, and finality controls. The configured proposer account model may therefore be unusable, or an intermediary in the proposer call graph may gain unintended authority to create a resolved proposal.Recommendation
Replace
tx.originauthorization with an explicit supported account model. Usemsg.senderfor direct proposer calls, or verify an EIP 1271, signature, or module authorization binding the proposer, payload, chain, nonce, and expiry. Test EOA, Safe, module, account abstraction, and relayer call graphs.OP Labs: Acknowledged.
msg.senderis always theDisputeGameFactory, so the contract intentionally usestx.originfor proposer authorization. CheckinggameCreator()instead would add more code to this critical permission check, whileop-proposersupports only EOAs and the existingPermissionedDisputeGameuses the same model.A standard member upgrade can restore legacy games to a shared interop set
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The ordinary
OPContractsManagerV2.upgradepath accepts caller supplied dispute game configurations. Its legacy versus super mode restriction applies only to initial deployments, and the respected game override accepts any enabled listed type. On a shared interop set, the upgrade reinitializes the shared AnchorStateRegistry and rewrites the shared dispute game factory. U20 per proxy admin routing makes those shared writes succeed even when a non first member initiates the upgrade.An authorized but malformed upgrade can therefore install and respect a legacy per chain game across every portal sharing the ASR and DGF. For legacy games,
OptimismPortal2uses the raw root claim and does not exercise the legacy game's chain ID check. Withdrawal and output root hashes omit the chain ID, while replay state is local to each portal. The same chain A withdrawal proof can consequently be accepted through multiple portals and paid from shared custody. A privileged malformed upgrade can create cross portal withdrawal replay and duplicate value paid from a shared ETH lockbox. After the malformed state is installed, an ordinary proof submitter can trigger the duplicate finalization.Recommendation
Make mode compatibility an invariant of every state mutating deployment and upgrade path. When the ASR and DGF are shared, reject all legacy respected types and legacy implementations, and preserve shared state only from a canonical full set configuration. Add a two member negative test that attempts legacy types from both member upgrade entry points and verifies that a withdrawal can be accepted by at most the source chain portal.
OP Labs: Acknowledged. The
migratefunction will not be called as part of U20, so chains will not share theDisputeGameFactoryandAnchorStateRegistrycomponents required for this scenario. It would also take a privileged operation to install the invalid configuration and that same authority could more directly set an anchor against which invalid withdrawals could be proven.A zero SuperPermissionedDisputeGame proposer cannot create games
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
r0bert
Description
_assertValidFullConfigallows an enabledSUPER_PERMISSIONEDgame withproposer == address(0). The factory stores this value and the StandardValidator also accepts it when the expected proposer is zero.SuperPermissionedDisputeGame.initializerequirestx.origin == proposer(). Since no usable transaction can originate from the zero address, every game creation reverts withBadAuth. If this is the respected game type, no permissioned super-root games can be created until governance repairs the configuration.This requires a trusted ProxyAdmin owner to supply the bad value. The standard deployment script also rejects a zero proposer, so the issue is limited to configuration hardening.
Recommendation
Reject a zero proposer in
_assertValidFullConfigand the StandardValidator.OP Labs: Acknowledged.
U20 resets the retained overhead value during upgrade
State
Severity
- Severity: Informational
Submitted by
r0bert
Description
U20 keeps the deprecated
overhead()getter for integration compatibility and the release notice states that its stored value does not change during the upgrade. However, the reinitializer unconditionally setsoverheadto zero. Any legacy nonzero value is therefore lost during an ordinary U20 upgrade.This may surprise integrations that still read the getter. It does not affect fees or finality because op-node no longer uses this value after Ecotone.
Recommendation
Remove the
overhead = 0write so legacy values remain unchanged. If the reset is intentional, update the release notice and integration guidance instead.OP Labs: Fixed in commit 630e797.
Cantina: Fix verified. The Upgrade 20 notice now states that the upgrade clears
overheadto zero, that it remains zero, and that integrations reading the getter must expect this change. This documentation-only fix merged after theop-contracts/v8.0.0-rc.3tag, as intended and does not require a new contract release.Legacy validator input rejects healthy super-mode deployments
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
r0bert
Description
The legacy
ValidationInputprovides only one prestate._toValidationInputDevassigns it to Cannon and sets the expected Kona prestate to zero. U20 enables super-root mode by default, so validation compares this zero value against the nonzero prestate ofSUPER_CANNON_KONA.Consequently, the legacy overload reports a healthy deployment as invalid with
SCKDG-40or reverts when failures are not allowed. The completeValidationInputDevoverload validates the same deployment correctly. This is a read-only compatibility issue with no direct custody impact.Recommendation
Deprecate the legacy overload or return a clear unsupported-input error in super mode. Alternatively, obtain the expected Kona prestate from an authenticated configuration.
OP Labs: Acknowledged.