Skip to content

Consider simplified and gas-efficient alternatives to Address.sol to perform low level calls #5685

@JacoboLansac

Description

@JacoboLansac

Motivation

I recently audited a protocol that made an external call to a contract by using functionCall() from the Address.sol library. While auditing, I realized there is a significant gas saving that could affect all protocols using functionCall(), so I wanted to make a pull-request to address it.

Impact

  • With this optimization, all calls to functionCallWithValue() that pass value=0 would save 100 gas.
  • Notably, all contract calls using Address.functionCall() would benefit from this gas optimization as it calls functionCallWithValue() with value=0 hardcoded.

Details

The function functionCall() calls functionCallWithValue() passing uint256 value=0:

function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0);

Then inside functionCallWithValue(), the library checks that address(this).balance is sufficient to cover for the value passed. Since functionCall() always calls with value=0, this will always be true, and the requirement will always pass:

function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
if (address(this).balance < value) {
revert Errors.InsufficientBalance(address(this).balance, value);
}
(bool success, bytes memory returndata) = target.call{value: value}(data);
return verifyCallResultFromTarget(target, success, returndata);

However, reading an address.balance is a gas-expensive operation (100 gas according to https://www.evm.codes/), so it is inefficient to read the balance when the passed value is zero. A more gas-efficient implementation of functionCallWithValue() would only read the address.balance when the requested value is greater than zero:

    function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
-       if (address(this).balance < value) {
+       if ((value > 0) && (address(this).balance < value)) {
            revert Errors.InsufficientBalance(address(this).balance, value);
        }
        (bool success, bytes memory returndata) = target.call{value: value}(data);
        return verifyCallResultFromTarget(target, success, returndata);
    }

In the above, the EVM compiler is smart enough to know that the address.balance opcode is only used when the first if-condition is met. Therefore, any calls to functionCallWithValue() where value==0 would not read the balance unnecessarily.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions