Full Report
TrustSec has contributed to the Optimism ecosystem both in contributing to contests, and audits and two paid bug bounties. In this post, they talk about the security of Optimism and some of the more interesting bugs they discovered. There is a lot of background one the one bug they go through in the blog post. L2s need a way to communicate with the L1s and vice versa. In Optimism, going from the L2 to the L1 requires a Merkle proof from the trusted L2 state root. When going from Ethereum to Optimism, OptimismPortal is used which emits an event that is translated to an ETH minting call. Both of these give the capability to send arbitrary data between the chains. There is a limitation to what's above though: calls can become stuck. So, the CrossDomainMessenger (XDM) is where the other bridging functionality, like ERC20/ETH bridge and the ERC721Bridge are implemented. XDM supports resending failed messages via a mapping of either successful or failed messages. On the revert flow, there is a very important check... If the XDM failed then the ETH is has already been supplied. Additionally, the successfulMessages mapping prevents the same withdrawal from being executed more than once. When making a call from the L2, the default L2 sender is 0xDEAD. The variable is xDomainMsgSender. On a cross-chain message, this is set to the calling user. In effect, this acts as a reentrancy protection as well. At the end of the call, the storage value is set back to the default. The audit they did was for the SuperchainConfig contract upgrade. This was a single program that allowed for designated roles to pause or unpause a core contract. During this upgrade, they manually switch off the initialized bit of the contract to allow for recalling initialize() instead of using reinitializer modifier. Such a small line of code seems so simple. So, what's wrong? The xDomainMsgSender is 0xDEAD at all times except during the withdrawal process. Within the initialization code (which gets retriggered), this value is set to 0xDEAD but actually defaults to zero. Normally, this would be fine (since it should be a NOP) but that's NOT true in the context of the withdrawal code! Upgrades are permissionless once enough signers have agreed to the upgrade. Here's the flow of the attack: Store a failed delivery to the smart contracts failedMessages mapping. Wait for the upgrade to be in the mempool and ready to go. Reattempt the failed delivery: Perform the upgrade with the parameters. Now, the msgSender is set to 0xDEAD. Reenter into relayMessage passing in the withdrawal request. This will succeed because the DEFAULT 0xDEAD address was set back. The msgSender is set to L2Sender again when it shouldn't be. A double withdrawal has occurred because of the double setting of the xDomainMsgSender global variable. The stolen amount comes from any failed withdrawals. The setting of the xDomainMsgSender global variable doesn't seem important until you have the better context of what it does and why it's important. It's crazy how this reentrancy/replay protection was touched by this simple upgrade code. What an awesome find!
Analysis Summary
# Vulnerability: Replay Attack on Cross-Domain Message Relayer During Contract Upgrade
## CVE Details
- CVE ID: Not explicitly provided in the text (This is a specific bug bounty finding/audit finding, often assigned an internal ID or only disclosed through a vendor advisory, thus no public CVE is assigned yet based on this summary).
- CVSS Score: Not explicitly provided. Severity is implied as **High** due to successful asset theft (double withdrawal).
- CWE: CWE-840 (Reusable Action), CWE-841 (Improper Enforcement of State Transition), CWE-843 (Access of Released Memory - conceptual overlap with logic flaws around state reentry).
## Affected Systems
- Products: Optimism L1 and L2 Cross-Domain Messenger (XDM) functionality, specifically during contract upgrades relying on the `SuperchainConfig` mechanism.
- Versions: Versions of Optimism affected by the specific implementation of the `SuperchainConfig` upgrade logic that allowed re-triggering initialization code pathways while manipulating the `xDomainMsgSender`.
- Configurations: Upgrades involving manually switching off the `initialized` bit to allow `initialize()` recall instead of using `reinitializer`.
## Vulnerability Description
The vulnerability stems from the interaction between the withdrawal process in the CrossDomainMessenger (XDM) and a flawed contract upgrade procedure for `SuperchainConfig`.
1. **XDM Reentrancy/Replay Protection:** The `xDomainMsgSender` storage variable acts as protection: it is set to the L2 sender address (`0xDEAD` as default) before an external call is made and **reset to `0xDEAD` after the call succeeds or fails**.
2. **Upgrade Flaw:** During a contract upgrade (which could be permissionless once enough signers agreed), the upgrade code manually switched off the contract's `initialized` flag, allowing the `initialize()` function to be called again, rather than using a standard, safe reinitializer.
3. **Attack Vector:** An attacker leverages the upgrade transaction being in the mempool:
a. The attacker first schedules a withdrawal that they intentionally cause to fail (e.g., by reverting in the target contract call). This stores a failed delivery marker in `failedMessages`.
b. The attacker ensures the upgrade transaction is processed. During the execution of the upgrade context, the external calls made within the upgrade mechanism (or re-entry into the withdrawal relay logic) cause the sender context to be the L2 sender (`0xDEAD`) when it should not be, or during a period where storage manipulation occurs.
c. The attacker then calls `relayMessage` for the previously failed withdrawal. Because the withdrawal was previously set as *failed* but not yet *successful*, the relay logic attempts execution again.
d. **Double Execution:** By exploiting the timing of the upgrade and the resetting/setting of the `xDomainMsgSender` storage variable—especially if the upgrade path accidentally or intentionally interfered with the state reset—the withdrawal logic executes twice. The first execution succeeds, and the second execution, occurring before the `successfulMessages` mapping is updated/checked correctly under the transient state of the upgrade, allows for a double claim (double withdrawal) of ETH/assets associated with the failed withdrawal queue. The flaw relies on the fact that the check to prevent double execution relies on state updates that can be bypassed or incorrectly timed during the upgrade process, specifically concerning the `xDomainMsgSender` variable's role as a reentrancy/replay guard during external calls.
## Exploitation
- Status: **PoC available** (The vendor/researchers demonstrated the possibility, though the text doesn't confirm if it was deployed for live theft; assumed to be verified PoC from bounty submission).
- Complexity: **Medium** (Requires precise timing relative to a queued contract upgrade transaction and knowledge of the internal XDM state transitions).
- Attack Vector: Network (Through transaction manipulation and smart contract interaction).
## Impact
- Confidentiality: No direct impact.
- Integrity: **High** (Ability to execute state-changing logic, causing incorrect state transitions leading to monetary loss).
- Availability: Low (Temporary blockage might occur if the faulty logic runs, but the primary impact is theft).
## Remediation
### Patches
- The text implies that the vendor addressed the specific upgrade mechanism vulnerability, likely by:
1. Ensuring that initialization logic cannot be recalled as described.
2. Validating the context of state changes during upgrades, ensuring assumptions (like the default value of `xDomainMsgSender`) are confirmed via explicit checks rather than relying on transient states that arise during complex upgrade execution flows.
*(Specific patched versions are not detailed in the summary, requiring reference to official Optimism security advisories.)*
### Workarounds
- The summary suggests preventative coding practices:
1. Perform the minimum required operations in upgrades.
2. Always consider and validate the impact of upgrades being performed in the middle of an external call context.
3. Convert assumptions about state (like `xDomainMsgSender` state) into explicit validation code wherever withdrawal/relay logic is executed.
## Detection
- Indicators of Compromise: Multiple successful `relayMessage` calls or `finalizeWithdrawalTransaction` calls corresponding to a single initial withdrawal request, potentially involving transactions that appear to bypass standard reentrancy logic checks.
- Detection Methods and Tools: Monitoring XDM events and transaction traces for unusual sequences involving contract upgrades immediately preceding message finalization/relay calls.
## References
- Vendor Advisories: TrustSec Blog Post (Defanged: h t t p s : //www.trust-security.xyz/post/a-realistic-breakdown-of-optimism-part-1)
- Relevant Links: Sherlock Bug Bounty Issue (Defanged: h t t p s : //github.com/sherlock-audit/2023-01-optimism-judging/issues/87) (Context for why nonReentrant modifiers were avoided).