Skip to content
Quantix Security Labs

Risk summary

State at remediation commit 8a3d5c7e19f0

Findings
9
Critical and high
2
Open or partial
1
Remediation
94%

Findings by severity

  • Critical1
  • High1
  • Medium4
  • Low2
  • Informational1

Remediation statusAnalysis

  • Resolved7
  • Partially resolved1
  • Acknowledged1
  • Open0

Obsidian Rollup

Layer 2

Document
QX-TSA-2026-008 · v1.2
Type
Protocol Architecture Review
Review
1 Dec 2025 – 9 Jan 2026
Published
5 Feb 2026
Ecosystems
Ethereum
Languages
Solidity, Go
Risk Index
A83.0
Assessment contents · 12 sections

01

Assessment overview

Obsidian Rollup is an Ethereum Layer 2 that posts transaction batches to L1, proposes L2 output roots, and settles withdrawals through a fault-proof dispute game after a challenge window. A single operator runs the sequencer; users can force-include deposits through the L1 Portal. Quantix reviewed the L1 contracts, the pending v2 upgrade, and the Go batcher at the commits listed in this report.

The most serious issue was in the v2 upgrade rather than the deployed system: the new `OutputOracle` implementation inserted a storage variable ahead of existing ones. Executing the upgrade would have shifted every later slot, making the contract read the stored output count as the finalisation period and allowing withdrawals to be finalised without a challenge window. This critical finding was fixed before the upgrade was proposed.

A high-severity issue allowed withdrawals to be finalised against a dispute game that had not yet resolved in the proposer's favour, provided the proof-maturity delay had passed. Four medium-severity findings covered upgrade governance, gas griefing of withdrawals, batch resubmission after L1 reorganisations, and unpriced deposit spam. All findings were resolved except the upgrade-delay recommendation, which was partially adopted.

The system remains in a permissioned stage: a single sequencer orders transactions and a security council can upgrade contracts without delay in an emergency. These properties are documented and are reflected in the governance component of the protocol's risk profile.

02

Technical analysis: protocol architecture

The batcher compresses L2 transactions into frames and posts them to BatchInbox on L1. A proposer periodically submits output roots to OutputOracle; each output can be challenged by opening a DisputeGame, a bisection game that narrows a disagreement to a single instruction executed by an on-chain fault-proof VM.

Withdrawals are initiated on L2, proven on L1 against an output root through Portal.proveWithdrawalTransaction, and finalised after the proof has matured and the corresponding game has resolved. Deposits enter through Portal.depositTransaction and must be included by the sequencer within a sequencing window, or they are force-included by derivation.

All L1 contracts sit behind EIP-1967 proxies administered by a ProxyAdmin owned by a 2-of-3 operations multisig, with a 4-of-7 security council able to act in emergencies.

Trusted· 4

Assumed to behave as specified; compromise is out of the threat model.

  • BatchInboxData availability for L2 batches
  • OutputOracleProposed L2 state roots
  • DisputeGamePermissionless fault proofs
  • PortalDeposits and withdrawals; holds bridged ETH
trust boundary

Semi-trusted· 2

Trusted for liveness or within bounds; misbehaviour is partly modelled.

  • Sequencer and batcher (Go)Transaction ordering and batch posting
  • ProposerSubmits output roots
trust boundary

Untrusted· 1

May act adversarially; every input is validated.

  • Challengers, depositors, withdrawersAny external account
Components grouped by trust level. Calls that cross a trust boundary are where the review concentrated input validation and authorisation checks.

Trust assumptions

  • At least one honest challenger monitors outputs and responds within the game clock.
  • Ethereum L1 provides data availability and censorship resistance for forced inclusion.
  • The security council does not collude to push a malicious upgrade.
  • The fault-proof VM executes L2 state transitions faithfully (out of scope).

03

Scope

L1 rollup contracts (batch inbox, output oracle, dispute game, portal), the v2 upgrade and the Go sequencer and batcher. 4,310 nSLOC.

Repository
github.com/example-org/obsidian-rollup
Review effort
Four reviewers over twenty-five working days, including the pending v2 upgrade and the Go sequencer
Reviewed commit
2f9b6e01c4a87d35e9f0b12c6a4d8e73f1a05b96
Remediation commit
8a3d5c7e19f02b64d8e1a9c0f37b25e64c0d1a73
Files and components in scope
PathDescriptionnSLOC
contracts/L1/BatchInbox.solReceives compressed transaction batches from the batcher142
contracts/L1/OutputOracle.solStores proposed L2 output roots; v1 and pending v2318
contracts/dispute/DisputeGame.solBisection fault-proof game over disputed outputs1,046
contracts/L1/Portal.solDeposits, withdrawal proving and finalisation684
contracts/L1/SystemConfig.solBatcher, gas and resource parameters221
contracts/L1/ResourceMetering.solDeposit gas pricing (added during remediation)167
op-sequencer/batcher/*.goBatch construction, submission and retry logic1,238
script/UpgradeV2.s.solProxy upgrade script for the v2 release94
Total8 items3,910

Out of scope

  • The L2 execution client and derivation pipeline beyond batch submission
  • Fault-proof VM and off-chain challenger agent
  • L2 predeploys other than the withdrawal message passer interface
  • Operational security of the security council signers

04

Threat model

Each threat lists the actor, the vector and the mitigation, and links to the findings that realised it in the reviewed code.

Assets at risk

  • ETH and tokens held by the Portal
  • Correctness of finalised withdrawals
  • Liveness of L2 block production
  • Integrity of the upgrade path

Actors

  • Malicious proposerSemi-trustedSubmits invalid output roots
  • Withdrawal grieferUntrustedFinalises other users' withdrawals with chosen gas
  • Deposit spammerUntrustedSubmits many deposits with large gas limits
  • Upgrade administratorTrustedUpgrades L1 proxies
Threats, vectors, mitigations and related findings
ThreatActorVectorMitigationFindings
Corrupt the challenge period during an upgradeUpgrade administratorStorage layout shift in the v2 implementationAppend-only layout, namespaced storage, layout diff in CIOR-01
Finalise a withdrawal backed by an invalid outputMalicious proposerFinalise before the dispute game resolvesRequire DEFENDER_WINS plus a finality delayOR-03
Upgrade the Portal without noticeUpgrade administratorInstant ProxyAdmin upgradeTimelock for non-emergency upgradesOR-02
Make withdrawals fail permanentlyWithdrawal grieferFinalise with too little gas for the target callMinimum-gas check before the external callOR-04
Stall the safe headEthereum reorganisationBatcher loses track of a reorged batchTrack inclusion by block hash and resubmitOR-05
Fill L2 blocks with forced depositsDeposit spammerUnpriced deposit gasEIP-1559-style resource meteringOR-06

05

Findings

Severity

9 findings

OR-01CriticalResolvedUnsafe proxy upgrade

OutputOracle v2 inserts a storage variable before existing slots, corrupting the finalisation period on upgrade

contracts/L1/OutputOracle.sol (v2):L28–L41, script/UpgradeV2.s.sol:L52

Likelihood
High
Impact
High
Weakness
CWE-664
Description

A proxy keeps its state in its own storage and executes the implementation's code against it. Every implementation must therefore preserve the slot assignment of existing variables; new variables may only be appended or placed in dedicated namespaces.

The v1 OutputOracle declares, in order, startingBlockNumber, startingTimestamp, l2Outputs (a dynamic array) and finalizationPeriodSeconds set during initialisation. The v2 implementation adds uint256 public proposalBond as the first state variable, before startingBlockNumber. After the upgrade every existing variable is read one slot later: startingTimestamp reads the old startingBlockNumber, the array length of l2Outputs is read from the old startingTimestamp slot, and finalizationPeriodSeconds reads the slot that previously held the length of l2Outputs.

Because finalizationPeriodSeconds would equal the number of outputs proposed so far (a few thousand), the seven-day challenge window would shrink to roughly an hour. The upgrade script calls upgradeToAndCall with no layout validation, so the shift would have taken effect on execution.

Preconditions
  • The v2 upgrade is executed as scripted.
Exploit scenario
  1. 1.The operations multisig executes UpgradeV2.s.sol.
  2. 2.The OutputOracle now reports a finalisation period equal to the old output count, about 3,400 seconds.
  3. 3.A malicious proposer submits an invalid output root, proves a withdrawal of the Portal's ETH against it, and finalises after an hour, before honest challengers can complete a dispute game.
Impact

Loss of bridged funds held by the Portal and invalid L2 state accepted on L1. The finding was caught before the upgrade was proposed; no deployed contract was affected.

Recommendation

Append new variables after existing ones or move state into an EIP-7201 namespaced struct. Add an automated storage-layout comparison between the deployed and new implementations to the upgrade pipeline and refuse to produce upgrade calldata when the check fails.

Fix
contracts/L1/OutputOracle.sol (v2, remediation commit)diff
 contract OutputOracle is Initializable, ISemver {-    uint256 public proposalBond;     uint256 public startingBlockNumber;     uint256 public startingTimestamp;     Types.OutputProposal[] internal l2Outputs;     uint256 public finalizationPeriodSeconds;+    /// @custom:storage-location erc7201:obsidian.storage.OutputOracleV2+    struct V2Storage { uint256 proposalBond; }+    bytes32 private constant V2_SLOT = 0x6c1b2a4f63e0d2d4b5b2a5c6a0e9b8d3e7f1a2c4b6d8e0f2a4c6e8b0d2f4a600;
Resolution

Fixed. New state lives in a namespaced struct, and the upgrade pipeline now diffs forge inspect storageLayout output against the deployed implementation and fails on any change to existing slots.

OR-02MediumPartially resolvedGovernance manipulation

L1 contracts can be upgraded without delay by the operations multisig

contracts/universal/ProxyAdmin.sol (deployment configuration), contracts/L1/Portal.sol

Likelihood
Low
Impact
High
Weakness
CWE-284
Description

The ProxyAdmin that controls every L1 proxy is owned directly by a 2-of-3 operations multisig. An upgrade takes effect in the block it is executed. Users therefore have no window to withdraw if they disagree with an upgrade, and a compromise of two operations signers would give immediate control of all bridged funds.

Rollups at this stage commonly keep an emergency upgrade path, but separate it from routine upgrades, which pass through a public delay longer than the withdrawal period so that users can exit first.

Preconditions
  • Two of three operations signers act maliciously or are compromised.
Exploit scenario
  1. 1.The compromised signers upgrade the Portal to an implementation that transfers its ETH balance to an attacker-controlled address.
Impact

Total loss of bridged assets in a key-compromise scenario. No defect in contract logic.

Recommendation

Route routine upgrades through a timelock longer than the withdrawal period (at least eight days) and reserve instant upgrades for the security council, with that power documented and monitored.

Fix
Deployment configuration (remediation)diff
- proxyAdmin.owner = OPERATIONS_MULTISIG  // 2-of-3, instant+ proxyAdmin.owner = UpgradeTimelock        // minDelay = 48 hours, proposer = OPERATIONS_MULTISIG+ UpgradeTimelock.EMERGENCY_ROLE = SECURITY_COUNCIL  // 4-of-7, instant, publicly logged
Resolution

Partially resolved. Routine upgrades now pass through a 48-hour UpgradeTimelock, shorter than the eight days recommended, so users still cannot always exit before an upgrade takes effect; the 4-of-7 security council retains an instant path for emergencies, which Obsidian documents in its public risk disclosures.

OR-03HighResolvedBusiness logic

Withdrawals can be finalised against a dispute game that has not resolved for the defender

contracts/L1/Portal.sol:L301–L337 (finalizeWithdrawalTransaction, _checkWithdrawal)

Likelihood
Medium
Impact
High
Weakness
CWE-841
Description

When a withdrawal is proven, the Portal records the dispute game that backs the output root and the proof timestamp. _checkWithdrawal later verifies that PROOF_MATURITY_DELAY has passed since proving and that game.status() != GameStatus.CHALLENGER_WINS.

The check is phrased as "not lost" rather than "won". A game that is still IN_PROGRESS passes it. Dispute games can legitimately run longer than the proof-maturity delay when bisection is deep and both parties use their full clocks, so a withdrawal proven against an output that is under active dispute can be finalised before the game concludes. If the challenger later wins, the withdrawal has already been paid.

The OP Stack design that Obsidian follows requires the game to have resolved in favour of the defender and an additional "air-gap" delay after resolution, during which a guardian can blacklist a game resolved incorrectly.

Preconditions
  • An invalid output root has been proposed and disputed.
  • The dispute game runs longer than the proof-maturity delay.
Exploit scenario
  1. 1.A malicious proposer submits an output root that includes a fabricated withdrawal of 5,000 ETH.
  2. 2.An honest challenger opens a dispute game; the proposer plays every move at the last moment so the game stays in progress.
  3. 3.After the proof-maturity delay, the proposer finalises the withdrawal. _checkWithdrawal passes because the game is not CHALLENGER_WINS yet.
  4. 4.The game later resolves for the challenger, but the ETH has already left the Portal.
Impact

Theft of Portal funds by a malicious proposer despite an honest challenger, defeating the purpose of fault proofs.

Recommendation

Require game.status() == GameStatus.DEFENDER_WINS and block.timestamp − game.resolvedAt() > DISPUTE_GAME_FINALITY_DELAY, and let the guardian blacklist games during that delay.

Fix
contracts/L1/Portal.sol (remediation commit)diff
   function _checkWithdrawal(bytes32 withdrawalHash, ProvenWithdrawal memory p) internal view {       IDisputeGame game = p.disputeGameProxy;-      require(game.status() != GameStatus.CHALLENGER_WINS, "Portal: game lost");+      require(!disputeGameBlacklist[game], "Portal: game blacklisted");+      require(game.status() == GameStatus.DEFENDER_WINS, "Portal: game not won");+      require(block.timestamp - game.resolvedAt().raw() > DISPUTE_GAME_FINALITY_DELAY, "Portal: finality delay");       require(block.timestamp - p.timestamp > PROOF_MATURITY_DELAY, "Portal: proof not mature");   }
Resolution

Fixed as recommended with a 3.5-day finality delay and a guardian blacklist. Differential tests against the specification behaviour pass.

OR-04MediumResolvedDenial of service

Withdrawals can be forced to fail with insufficient gas and are then replayable at a time of anyone's choosing

contracts/L1/Portal.sol:L338–L372 (finalizeWithdrawalTransaction)

Likelihood
Medium
Impact
Medium
Weakness
CWE-691
Description

Anyone may finalise any proven withdrawal. The Portal executes the withdrawal's target call with gasleft() and, if the call fails, leaves finalizedWithdrawals[hash] unset so that it can be retried.

Two problems follow. First, a griefer can finalise someone else's withdrawal with just enough gas for the Portal's own logic but too little for the target call; because of the 63/64 rule the call fails while the outer transaction succeeds. Second, the failed withdrawal remains open indefinitely and can be executed later by anyone, at a moment the original sender did not choose, for example after the target contract has changed state.

The specification addresses this by recording the withdrawal as finalised whether or not the call succeeds and by refusing to make the call unless enough gas remains to forward the withdrawal's declared gas limit.

Preconditions
  • A proven withdrawal whose target call requires more than trivial gas.
Exploit scenario
  1. 1.A user proves a withdrawal that calls a vault on L1 to deposit the withdrawn ETH.
  2. 2.A griefer calls finalizeWithdrawalTransaction with a gas limit chosen so the vault call runs out of gas.
  3. 3.The withdrawal is left unfinalised. Weeks later, after the vault has been paused and migrated, the griefer finalises it again; the call now sends ETH to a deprecated contract.
Impact

Withdrawals fail or execute in unexpected states; funds can be stranded in contracts that no longer handle them.

Recommendation

Mark the withdrawal finalised before the call, and require SafeCall.hasMinGas(gasLimit, RESERVED_GAS) so that the declared gas limit is always forwarded.

Fix
contracts/L1/Portal.sol (remediation commit)diff
+      finalizedWithdrawals[withdrawalHash] = true;+      require(SafeCall.hasMinGas(_tx.gasLimit, RECEIVE_DEFAULT_GAS_LIMIT), "Portal: insufficient gas");       l2Sender = _tx.sender;       bool success = SafeCall.callWithMinGas(_tx.target, _tx.gasLimit, _tx.value, _tx.data);       l2Sender = Constants.DEFAULT_L2_SENDER;-      if (success) finalizedWithdrawals[withdrawalHash] = true;       emit WithdrawalFinalized(withdrawalHash, success);
Resolution

Fixed. Withdrawals are final after one attempt and the declared gas is guaranteed to the target.

OR-05MediumResolvedDenial of service

Batcher does not detect L1 reorganisations of submitted batches, stalling the safe head

op-sequencer/batcher/tx_manager.go:L210–L268, op-sequencer/batcher/channel_manager.go:L144

Likelihood
Medium
Impact
Medium
Weakness
CWE-754
Description

The batcher records a frame as confirmed when its transaction receipt is returned and then prunes the frame from memory. Confirmation is checked once, at the first receipt. If the L1 block containing the batch is reorganised out, the transaction may never be re-included (the batcher has already moved its nonce forward and later transactions fill the gap), and the frame is gone.

Derivation on L2 cannot advance the safe head past the missing frame until the channel times out, after which the corresponding L2 blocks are re-derived without those transactions. In practice this halts finality for the duration of the channel timeout and can reorder user transactions.

Preconditions
  • An L1 reorganisation deeper than one block affecting a block that contains a batch.
Exploit scenario
  1. 1.A two-block L1 reorg drops the block containing frame 3 of a channel.
  2. 2.The batcher has pruned frame 3 and keeps submitting frames 4 and 5.
  3. 3.Derivation waits for frame 3 until the channel times out; the safe head stalls for about 50 minutes.
Impact

Liveness degradation of L2 finality; bridges and exchanges relying on the safe head stop crediting deposits.

Recommendation

Track confirmations by block hash, keep frames until they are buried under a configurable confirmation depth, and requeue frames whose inclusion block is no longer canonical.

Fix
op-sequencer/batcher/tx_manager.go (remediation commit)diff
-  if receipt != nil {-      m.channels.FrameConfirmed(frameID)-  }+  if receipt != nil {+      m.pending[frameID] = inclusion{blockHash: receipt.BlockHash, blockNumber: receipt.BlockNumber.Uint64()}+  }+  // on each new L1 head:+  for id, inc := range m.pending {+      if !m.l1.IsCanonical(ctx, inc.blockHash) {+          m.channels.RequeueFrame(id)+          delete(m.pending, id)+      } else if head-inc.blockNumber >= m.cfg.ConfirmationDepth {+          m.channels.FrameConfirmed(id)+          delete(m.pending, id)+      }+  }
Resolution

Fixed. Frames are retained until ten confirmations and requeued when their block leaves the canonical chain; a devnet reorg test reproduces the original stall and passes after the fix.

OR-06MediumResolvedDenial of service

Deposits are not priced, allowing cheap spam that fills L2 blocks with forced transactions

contracts/L1/Portal.sol:L120–L162 (depositTransaction)

Likelihood
Medium
Impact
Medium
Weakness
CWE-770
Description

Deposits submitted to the Portal must be included at the start of L2 blocks. The Portal checks only that the requested L2 gas limit is above a minimum derived from calldata size; nothing charges the depositor for the L2 gas they reserve.

An attacker can submit many deposits with large gas limits from L1, paying only L1 gas. Each deposit reserves L2 block gas, crowding out sequenced transactions and raising L2 fees for everyone. Designs derived from the OP Stack meter deposit gas with an EIP-1559-style market on L1, burning L1 gas proportional to the L2 gas purchased.

Preconditions
  • None beyond L1 gas costs.
Exploit scenario
  1. 1.An attacker submits 200 deposits per L1 block, each requesting 15 million L2 gas, saturating deposit capacity for several hours at modest L1 cost.
Impact

Degraded L2 throughput and elevated fees; sequenced transactions may be delayed.

Recommendation

Introduce resource metering for deposits: track L2 gas purchased per L1 block, adjust a base fee toward a target, and burn the corresponding L1 gas from the depositor.

Fix
contracts/L1/Portal.sol (remediation commit)diff
-  function depositTransaction(address _to, uint256 _value, uint64 _gasLimit, bool _isCreation, bytes memory _data) public payable {+  function depositTransaction(address _to, uint256 _value, uint64 _gasLimit, bool _isCreation, bytes memory _data)+      public payable metered(_gasLimit)+  {
Resolution

Fixed with a ResourceMetering base contract and parameters stored in SystemConfig.

OR-07LowResolvedAccess control

Challenger role can delete finalised outputs

contracts/L1/OutputOracle.sol:L188 (deleteL2Outputs)

Likelihood
Low
Impact
Medium
Weakness
CWE-284
Description

deleteL2Outputs(index) lets the CHALLENGER role remove outputs from index onward. It checks that the index exists but not that the outputs are still within the finalisation period. A mistaken or compromised challenger could delete finalised outputs, invalidating proofs for withdrawals that users have already proven and forcing them to re-prove against new outputs.

Preconditions
  • Action by the trusted challenger role.
Exploit scenario
  1. 1.The challenger key deletes outputs back to an index finalised two weeks earlier; pending withdrawals must be re-proven.
Impact

Delayed withdrawals and operational disruption; no direct loss.

Recommendation

Revert if the first deleted output is already finalised.

Fix
contracts/L1/OutputOracle.sol (remediation commit)diff
+      require(block.timestamp - l2Outputs[_l2OutputIndex].timestamp < finalizationPeriodSeconds, "OutputOracle: output finalized");
Resolution

Fixed as recommended.

OR-08LowResolvedKey management

Batcher logs its configuration, including the signer key path and RPC credentials, at debug level

op-sequencer/batcher/config.go:L96

Likelihood
Low
Impact
Medium
Weakness
CWE-532
Description

On start-up the batcher prints its full configuration struct with log.Debug("config", "cfg", cfg). The struct contains the L1 RPC URL with an embedded API token and, when a local key is used instead of a remote signer, the private key in hex. Debug logging is enabled in the staging environment and logs are shipped to a shared aggregation service.

Preconditions
  • Debug logging enabled and access to the log service.
Exploit scenario
  1. 1.An engineer with log access, or an attacker who compromises the log service, reads the batcher key and posts batches as the batcher.
Impact

Compromise of the batcher key allows posting arbitrary batch data, disrupting derivation until the key is rotated in SystemConfig.

Recommendation

Implement redaction for secret fields and use a remote signer in every environment.

Fix
op-sequencer/batcher/config.go (remediation commit)diff
-  log.Debug("config", "cfg", cfg)+  log.Debug("config", "cfg", cfg.Redacted()) // PrivateKey, L1RPCURL credentials replaced with "[redacted]"
Resolution

Fixed. Secrets are redacted and staging now uses the remote signer.

OR-09InformationalAcknowledgedCode quality

SystemConfig setters do not emit dedicated events

contracts/L1/SystemConfig.sol (setGasLimit, setResourceConfig)

Likelihood
Low
Impact
Low
Description

Configuration changes emit a generic ConfigUpdate(version, type, data) event with ABI-encoded data. Monitoring has to decode the payload to distinguish a gas-limit change from a batcher rotation, which slows alerting on the most sensitive change (batcher key rotation).

Preconditions
  • None; observability only.
Exploit scenario
  1. 1.Not exploitable.
Impact

Slower detection of configuration changes.

Recommendation

Keep the generic event for compatibility and add typed events for batcher and resource-configuration changes.

Resolution

Acknowledged. Obsidian keeps the generic event for tooling compatibility and has added decoding rules to its monitoring.

06

Severity

9findings
  • Critical1 · 11%
  • High1 · 11%
  • Medium4 · 44%
  • Low2 · 22%
  • Informational1 · 11%

By vulnerability class

  • Denial of service3
  • Access control1
  • Business logic1
  • Code quality1
  • Governance manipulation1
  • Key management1
  • Unsafe proxy upgrade1

Likelihood and impact

Each finding placed by the likelihood of its preconditions and the impact of exploitation.

Findings by likelihood (columns) and impact (rows)
Impact ↓ / Likelihood →LowMediumHigh
High
Medium
0
Low
0
0

07

Remediation

7 of 9 findings are resolved, 1 acknowledged, 1 partially resolved and 0 open at remediation commit 8a3d5c7e19f0.

Remediation progress94%

Resolved and acknowledged findings count fully; partially resolved findings count half.

94%remediated
  • Resolved7 · 78%
  • Partially resolved1 · 11%
  • Acknowledged1 · 11%
  • Open0 · 0%
Findings by severity and remediation status
SeverityResolvedPartially resolvedAcknowledgedOpenTotal
Critical10001
High10001
Medium31004
Low20002
Informational00101
All findings71109

Residual items

  • OR-02MediumPartially resolvedPartially resolved. Routine upgrades now pass through a 48-hour `UpgradeTimelock`, shorter than the eight days recommended, so users still cannot always exit before an upgrade takes effect; the 4-of-7 security council retains an instant path for emergencies, which Obsidian documents in its public risk disclosures.
  • OR-09InformationalAcknowledgedAcknowledged. Obsidian keeps the generic event for tooling compatibility and has added decoding rules to its monitoring.
  • ResolvedFixed and verified in the remediation review.
  • Partially resolvedMitigated; the residual risk is documented.
  • AcknowledgedAccepted by the protocol team with a stated rationale.
  • OpenNot yet addressed at the reviewed remediation commit.

08

Formal verification

The Portal withdrawal state machine and the dispute-game bisection were specified as properties and checked with bounded symbolic execution; storage layouts were compared mechanically. Finalisation-safety properties failed on the original commit (OR-03, OR-04) and hold on the remediation commit. A global property relating output finalisation to in-progress games was not completed.

Tool: Halmos and Foundry symbolic tests; storage layouts compared with forge inspect

  • P1ProvedA withdrawal hash is finalised at most once, regardless of the target call result.Violated on the original commit when the target call failed (OR-04).
  • P2ProvedFinalisation requires a dispute game resolved as DEFENDER_WINS for at least the finality delay.Violated on the original commit (OR-03).
  • P3ProvedA bisection game terminates within MAX_GAME_DEPTH moves on each branch.
  • P4ProvedThe OutputOracle v2 storage layout is an append-only extension of v1.Checked mechanically on the remediation commit; failed on the original v2 (OR-01).
  • P5InconclusiveNo output root becomes final while a game disputing it is in progress.Requires reasoning across contracts and time; covered by differential tests instead.

A proved property holds for every execution of the model under its stated assumptions. Fuzzing and property-based results are evidence, not proofs, and are recorded as inconclusive.

09

Recommendations

  1. 01

    Gate every upgrade on a storage-layout diff

    Make the layout comparison a required CI step that produces the upgrade calldata, so a failing check cannot be bypassed manually.

  2. 02

    Publish the upgrade and emergency powers

    Document the timelock delay, the security council threshold and the conditions for emergency action alongside the rollup's risk disclosures.

  3. 03

    Run a permanent honest challenger

    Operate challenger agents from independent infrastructure and fund their bonds so that fault proofs remain effective.

  4. 04

    Monitor game lifecycle and withdrawals

    Alert on games approaching resolution, on blacklisting, and on finalisations of large withdrawals.

  5. 05

    Adopt a remote signer everywhere

    Remove local private keys from batcher and proposer configuration in all environments.

10

Methodology and limitations

  1. 01Architecture walk-through and threat modelling with the Obsidian core team
  2. 02Manual review of all in-scope Solidity and Go code by two reviewers per component
  3. 03Storage-layout comparison of every v1 and v2 implementation
  4. 04Differential tests of withdrawal proving and finalisation against the OP Stack specification behaviour
  5. 05Symbolic tests of the dispute-game bisection bounds and Portal state machine
  6. 06L1 reorganisation simulation against the batcher with a local devnet
  7. 07Remediation review of the fix commit and of the revised upgrade script

Severity classification

Critical
Direct loss of funds or permanent loss of control is achievable with little or no precondition.
High
Material loss or protocol disruption is achievable under realistic conditions.
Medium
Limited loss, degraded functionality, or impact that depends on specific conditions.
Low
Minor impact, defence-in-depth gaps, or issues with unlikely preconditions.
Informational
Code quality, documentation or best-practice observations without direct security impact.

Limitations

  • The L2 execution client, derivation pipeline and fault-proof VM were out of scope; their correctness is assumed.
  • The review covered the v2 upgrade as scripted at the stated commit; later changes to the script are not covered.
  • Reorg handling was tested on a local devnet with simulated reorganisations up to depth 4.
  • Bounded symbolic results hold up to the stated depth and game size.

No assessment can show that a system is free of vulnerabilities. This research assessment reflects the code at the commits listed. Quantix methodology

11

Technical appendix

Tools

  • FoundryUnit, differential and fork tests
  • HalmosBounded symbolic tests of the Portal and dispute game
  • forge inspect storageLayoutStorage-layout comparison between implementations
  • SlitherStatic analysis of Solidity
  • staticcheck and golangci-lintStatic analysis of Go
  • Local devnetL1 reorganisation simulation
Reviewed files (SHA-256) · 6 files
SHA-256 hashes of reviewed files
FileSHA-256
contracts/L1/Portal.sol2baceed1ad4e8858b07790c1428ee9ce160d9234583a8470babf12ea63b240d2
contracts/L1/OutputOracle.sol3f1321a65c1c7ebf481b6cc66a6df1317bcfc553bede7b2f0b832bba519d03ab
contracts/dispute/DisputeGame.sol98284271b049a3fd72e9b2a751a14c9a5fb210e963cfd9ba0613888ff976af7a
contracts/L1/BatchInbox.solceccf63e29779736d472be8070175231a1a821604a64d75d0ccdd3b78102434b
contracts/L1/SystemConfig.solf27cef5c452c607ff0d95275d113afe86b17d7afe8b53c7cbbe4c675e36e37d5
op-sequencer/batcher/tx_manager.goe87f950cd83a43ea69baabbce31cdebcc91f68efa5048424d5619af5483b8498

Notes

  • File hashes are SHA-256 digests of the reviewed sources at the audited commit.
  • OR-01 concerned an upgrade that had not been executed; no deployed contract was affected.

Technical references

  1. 1.EIP-1967: Proxy storage slots (opens in a new tab)
  2. 2.EIP-7201: Namespaced storage layout (opens in a new tab)
  3. 3.OpenZeppelin: Writing upgradeable contracts (opens in a new tab)
  4. 4.OP Stack specifications (opens in a new tab)
  5. 5.CWE-841: Improper Enforcement of Behavioral Workflow (opens in a new tab)
  6. 6.CWE-754: Improper Check for Unusual or Exceptional Conditions (opens in a new tab)
  7. 7.EIP-1559: Fee market change (opens in a new tab)
  8. 8.CWE-284: Improper Access Control (opens in a new tab)
  9. 9.OpenZeppelin Contracts: Access Control (opens in a new tab)
  10. 10.CWE-532: Insertion of Sensitive Information into Log File (opens in a new tab)
  11. 11.Solidity documentation: Events (opens in a new tab)

Quantix Technical Security Assessment QX-TSA-2026-008

Version 1.2, published 5 Feb 2026. A4 PDF generated from this assessment record.

Download Report

12

Report history

  1. 1 Dec 2025Review openedReviewed commit 2f9b6e01c4a8 in github.com/example-org/obsidian-rollup
  2. 9 Jan 2026Review closedFour reviewers over twenty-five working days, including the pending v2 upgrade and the Go sequencer
  3. After reviewRemediation reviewFixes checked at commit 8a3d5c7e19f0
  4. 5 Feb 2026Version 1.2 publishedQX-TSA-2026-008, A4 PDF generated from this record

Commission an assessment written to be verified.

Share the repository and the commit you plan to deploy. We reply with scoping questions and an estimate.