Skip to content

Implement LowLevelCall library #5094

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 62 commits into
base: master
Choose a base branch
from

Conversation

ernestognw
Copy link
Member

@ernestognw ernestognw commented Jun 20, 2024

Alternative to #5091
Fixes #5013
Requires #5189

PR Checklist

  • Tests
  • Documentation
  • Changeset entry (run npx changeset add)

Copy link

changeset-bot bot commented Jun 20, 2024

🦋 Changeset detected

Latest commit: 51a3c50

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
openzeppelin-solidity Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ernestognw ernestognw marked this pull request as ready for review June 26, 2024 22:55
@ernestognw ernestognw requested review from Amxx and cairoeth June 27, 2024 23:41
@ernestognw ernestognw added this to the 5.5 milestone Jun 9, 2025
/// === CALL ===

/// @dev Performs a Solidity function call using a low level `call` and ignoring the return data.
function callRaw(address target, bytes memory data) internal returns (bool success) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd name that ...ReturnEmpty or ...NoReturn for consistency with ...ReturnBytes32

Suggested change
function callRaw(address target, bytes memory data) internal returns (bool success) {
function callReturnEmpty(address target, bytes memory data) internal returns (bool success) {

Copy link
Collaborator

@Amxx Amxx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments:

  • I don't think all the memory library is necessary here (for example the loadByte function). Agreeing on the low level call part should be our priority, and IMO we should not include additional things that are independant.

  • We should thing of the "low level call" functions as an extension of the Address library, in terms of features AND naming. Ideally, in 6.0 I would move them all in the same file (IMO they are different ways, which different security guarantees of doing the same thing)

  • Part of what makes low level call efficient is when you don't use abi.encode to prepare the call. This library doesn't support that. IMO its fine for now, but we should consider adding that later.

Copy link
Collaborator

@Amxx Amxx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any case where the bubble is something other that the return data that we received from a call that was not successfull ?

I think that by definition of "bubble", it implies that its already an error that was reverted by an unsuccessfull call. Therefore, I'm would argue maybe the "low level call" function should include a boolean to select is yes/no we want to bubble any revert we get.

It could look like

    function callEmpty(address target, bytes memory data, uint256 value, bool bubble) internal returns (bool success) {
        assembly ("memory-safe") {
            success := call(gas(), target, value, add(data, 0x20), mload(data), 0x00, 0x00)
            if and(iszero(success), bubble) {
                let ptr := mload(0x40)
                returndatacopy(ptr, 0, returndatasize())
                revert(ptr, returndatasize())
            }
        }
    }

    function callScratch(address target, bytes memory data, uint256 value, bool bubble) internal returns (bool success, uint256 size, bytes32 x, bytes32 y) {
        assembly ("memory-safe") {
            success := call(gas(), target, value, add(data, 0x20), mload(data), 0x00, 0x40)
            if and(iszero(success), bubble) {
                let ptr := mload(0x40)
                returndatacopy(ptr, 0, returndatasize())
                revert(ptr, returndatasize())
            }
            size := returndatasize()
            x := mload(0x00)
            y := mload(0x20)
        }
    }

    function callFull(address target, bytes memory data, uint256 value, bool bubble) internal returns (bool success, bytes memory returndata) {
        assembly ("memory-safe") {
            success := call(gas(), target, value, add(data, 0x20), mload(data), 0x00, 0x00)

            returndata := mload(0x40)
            returndatacopy(add(returndata, 0x20), 0, returndatasize())
            if and(iszero(success), bubble) {
                revert(add(returndata, 0x20), returndatasize())
            }
            mstore(returndata, returndatasize())
            mstore(0x40, add(add(returndata, 0x20), returndatasize()))
        }
    }

Note: The RETURNDATASIZE opcode is only 2 gas, and DUPX are 3 gas. So it turns out caching the returndatasize in the stack and dupping it is counterproductive !

EDIT: We decided to NOT do that bool bubble because the deicision to bubble or not might depend on the returndatasize we get.

@Amxx Amxx requested a review from a team as a code owner July 9, 2025 20:12
@Amxx
Copy link
Collaborator

Amxx commented Jul 9, 2025

I updated the PR following our discussion of the day. I'm curious what the gas comparaison script tells us.

Note: the changes for SafeERC20 will be overwritten by the SafeERC20 PR.

Copy link
Collaborator

@Amxx Amxx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a lot of changes following yesterday's discussion. Only thing I havent done yet is polishing the contracts/utils/LowLevelCall.sol natspecs.

@ernestognw Let me know what you think !

@Amxx
Copy link
Collaborator

Amxx commented Jul 10, 2025

Gas number form CI

@Amxx
Copy link
Collaborator

Amxx commented Jul 15, 2025

Current remarquable numbers:

  • AccessManager.execute(address,bytes): -124 gas on average
  • AccountERC7579.validateUserOp: -88 gas on average (-1833 in the worst case)
  • Create2.deploy: -7500 gas on average
  • ERC20Multicall: -530 gas (for batches of 2 calls)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Consider simplified and gas-efficient alternatives to Address.sol to perform low level calls Low level call library
3 participants