-
Notifications
You must be signed in to change notification settings - Fork 12.1k
Description
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 passvalue=0
would save 100 gas. - Notably, all contract calls using
Address.functionCall()
would benefit from this gas optimization as it callsfunctionCallWithValue()
withvalue=0
hardcoded.
Details
The function functionCall()
calls functionCallWithValue()
passing uint256 value=0
:
openzeppelin-contracts/contracts/utils/Address.sol
Lines 62 to 63 in e342516
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:
openzeppelin-contracts/contracts/utils/Address.sol
Lines 75 to 80 in e342516
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.