1

Consider the following function:


uint256 mySpecialNumber = 0;
function foo(address callee, uint256 newNumber) public {
  mySpecialNumber = newNumber;

  // make a low level call to the bar() function on the callee
  callee.call{gas: gasleft() - 2000}(
    abi.encodeWithSelector(ISomeOtherContract.bar.selector)
  );
}

Is it possible for the callee to DOS or prevent execution of foo()? Or is foo() always executable no matter what the callee does (if it reverts, runs out of gas, or some other error)?

If it is possible for the callee to prevent execution of foo(), how would that be done?

2
  • What is a DOS? Denial of service? Aug 15, 2022 at 11:30
  • Yes, I meant denial of service Aug 16, 2022 at 11:22

3 Answers 3

2
+200

Is it possible for a callee to DOS a function call if the caller ignores the success value?

Yes, but only if the caller doesn't ignore the returndata.

For example, consider this calling function:

function foo(address callee, uint256 newNumber) public {
  (bool success, bytes memory returndata) = callee.call(
    abi.encodeWithSelector(ISomeOtherContract.bar.selector)
  );
}

Even if we ignore success, solidity still needs to read returndata into memory. If callee returns a lot of bytes, this passage can fail due to out-of-gas, reverting the whole transaction.

In your example, where the caller doesn't retrieve the return value, there's no way for the callee to DOS.

How much gas to pass?

I notice you're making the call with gas: gasleft() - 2000. However removing 2000 or a similar small amount isn't necessary.

Indeed, EIP150 makes it that at max gasleft()*63/64 is provided to a child call. In other words, the caller contract is guaranteed to retain 1/64 of gas even if the callee tries to consume it all.

3
  • This is an excellent answer! Aug 15, 2022 at 16:16
  • I reserved 2k because I wasnt sure if its possible that the 1/64 amount is not sufficient to cover the remaining computations of the function. How much overhead is needed after the call? Aug 16, 2022 at 11:20
  • 1
    @DylanKerler I would say less than 50 gas, of course if you're not doing anything after
    – 0xSanson
    Aug 16, 2022 at 15:28
2

A call can fail for at least the following reasons (return false as success):

  • The called contract does not exist
  • The called function does not exist (and no fallback)
  • The called function reverts (there are also different kinds of reverts)
  • The execution runs out of gas (although, strictly speaking, it doesn't return anything because it runs out of gas)

If the success is checked, the first returns true for success and the next two return false.

As long as you don't give out all of your remaining gas, I don't think the bar can block the execution of foo.

For anyone who wants to test this themselves, here's a simplified code for testing:

pragma solidity ^0.8.0;

interface ISomeOtherContract {
  function bar() external;
}

contract Demo {
  event A(bool);

  function foo(address callee) public {
    // make a low level call to the bar() function on the callee
    (bool success,) = callee.call{gas: gasleft() - 2000}(
      abi.encodeWithSelector(ISomeOtherContract.bar.selector)
    );
    emit A(success);
  }
}
2
  • The called contract does not exist this returns true as success
    – 0xSanson
    Aug 15, 2022 at 15:52
  • An excellent observation! Thanks for the heads-up. Edited my answer. Aug 15, 2022 at 16:12
0

Based on the question, just to answer, if it is somehow possible to DOS the execution of foo(), there could be one way, as shown below:

pragma solidity ^0.8.0;

contract ISomeOtherContract {
    Demo d;

    constructor(address _d) {
        d = Demo(_d);
    }

    function bar() external {
        d.destroy();
    }
}

contract Demo {
    uint256 public mySpecialNumber = 0;

    function foo(address callee, uint256 newNumber) public {
        mySpecialNumber = newNumber;

        // make a low level call to the bar() function on the callee
        callee.call{gas: gasleft() - 2000}(
            abi.encodeWithSelector(ISomeOtherContract.bar.selector)
        );
    }

    function destroy() public {
        selfdestruct(payable(msg.sender));
    }
}

In any other way, as @lauri-peltonen mentions, in most cases, it returns some value, which is not altering the execution flow, as it is not used at all in the function call. But in any case, to stop the execution of foo(), the contract itself has to be destroyed. If a modifier or any other kind of require() was in play, that broadens the possibilities of DOS.

2
  • selfdestruct inside a child call will end only the child call. The execution of foo will continue, so no DOS.
    – 0xSanson
    Aug 15, 2022 at 15:53
  • Doesn't DOS also mean you interrupt the service once (or permanently)? Calling the selfdestruct will destroy the contract, thus won't allow any more calls to the foo() But I think the question had multiple sub-parts, where I answered (at least partially) for this: Or is foo() always executable no matter what the callee does
    – remedcu
    Aug 16, 2022 at 16:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.