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
OutputOracledeclares, in order,startingBlockNumber,startingTimestamp,l2Outputs(a dynamic array) andfinalizationPeriodSecondsset during initialisation. The v2 implementation addsuint256 public proposalBondas the first state variable, beforestartingBlockNumber. After the upgrade every existing variable is read one slot later:startingTimestampreads the oldstartingBlockNumber, the array length ofl2Outputsis read from the oldstartingTimestampslot, andfinalizationPeriodSecondsreads the slot that previously held the length ofl2Outputs.Because
finalizationPeriodSecondswould 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 callsupgradeToAndCallwith no layout validation, so the shift would have taken effect on execution.- Preconditions
- The v2 upgrade is executed as scripted.
- Exploit scenario
- 1.The operations multisig executes
UpgradeV2.s.sol. - 2.The OutputOracle now reports a finalisation period equal to the old output count, about 3,400 seconds.
- 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.
- 1.The operations multisig executes
- 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 storageLayoutoutput against the deployed implementation and fails on any change to existing slots.
