Skip to content

Commit 8f8d61c

Browse files
ernestognwAmxxarr00
authored
Implement LowLevelCall library (#5094)
Co-authored-by: Hadrien Croubois <[email protected]> Co-authored-by: Arr00 <[email protected]>
1 parent a060e93 commit 8f8d61c

File tree

13 files changed

+610
-79
lines changed

13 files changed

+610
-79
lines changed

.changeset/sharp-scissors-drum.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`LowLevelCall`: Add a library to perform low-level calls and deal with the `returndata` more granularly.

contracts/account/Account.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pragma solidity ^0.8.20;
66
import {PackedUserOperation, IAccount, IEntryPoint} from "../interfaces/draft-IERC4337.sol";
77
import {ERC4337Utils} from "./utils/draft-ERC4337Utils.sol";
88
import {AbstractSigner} from "../utils/cryptography/signers/AbstractSigner.sol";
9+
import {LowLevelCall} from "../utils/LowLevelCall.sol";
910

1011
/**
1112
* @dev A simple ERC4337 account implementation. This base implementation only includes the minimal logic to process
@@ -113,8 +114,7 @@ abstract contract Account is AbstractSigner, IAccount {
113114
*/
114115
function _payPrefund(uint256 missingAccountFunds) internal virtual {
115116
if (missingAccountFunds > 0) {
116-
(bool success, ) = payable(msg.sender).call{value: missingAccountFunds}("");
117-
success; // Silence warning. The entrypoint should validate the result.
117+
LowLevelCall.callNoReturn(msg.sender, missingAccountFunds, ""); // The entrypoint should validate the result.
118118
}
119119
}
120120

contracts/account/extensions/draft-AccountERC7579.sol

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from "../../interfaces/draft-IERC7579.sol";
1818
import {ERC7579Utils, Mode, CallType, ExecType} from "../../account/utils/draft-ERC7579Utils.sol";
1919
import {EnumerableSet} from "../../utils/structs/EnumerableSet.sol";
20+
import {LowLevelCall} from "../../utils/LowLevelCall.sol";
2021
import {Bytes} from "../../utils/Bytes.sol";
2122
import {Packing} from "../../utils/Packing.sol";
2223
import {Calldata} from "../../utils/Calldata.sol";
@@ -314,14 +315,10 @@ abstract contract AccountERC7579 is Account, IERC1271, IERC7579Execution, IERC75
314315
// From https://eips.ethereum.org/EIPS/eip-7579#fallback[ERC-7579 specifications]:
315316
// - MUST utilize ERC-2771 to add the original msg.sender to the calldata sent to the fallback handler
316317
// - MUST use call to invoke the fallback handler
317-
(bool success, bytes memory returndata) = handler.call{value: msg.value}(
318-
abi.encodePacked(msg.data, msg.sender)
319-
);
320-
321-
if (success) return returndata;
322-
323-
assembly ("memory-safe") {
324-
revert(add(returndata, 0x20), mload(returndata))
318+
if (LowLevelCall.callNoReturn(handler, msg.value, abi.encodePacked(msg.data, msg.sender))) {
319+
return LowLevelCall.returnData();
320+
} else {
321+
LowLevelCall.bubbleRevert();
325322
}
326323
}
327324

contracts/mocks/CallReceiverMock.sol

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,51 @@ contract CallReceiverMock {
1111

1212
function mockFunction() public payable returns (string memory) {
1313
emit MockFunctionCalled();
14+
return "0x1234";
15+
}
1416

17+
function mockFunctionWritesStorage(bytes32 slot, bytes32 value) public returns (string memory) {
18+
assembly ("memory-safe") {
19+
sstore(slot, value)
20+
}
1521
return "0x1234";
1622
}
1723

1824
function mockFunctionEmptyReturn() public payable {
1925
emit MockFunctionCalled();
2026
}
2127

28+
function mockFunctionEmptyReturnWritesStorage(bytes32 slot, bytes32 value) public payable {
29+
assembly ("memory-safe") {
30+
sstore(slot, value)
31+
}
32+
emit MockFunctionCalled();
33+
}
34+
2235
function mockFunctionWithArgs(uint256 a, uint256 b) public payable returns (string memory) {
2336
emit MockFunctionCalledWithArgs(a, b);
2437

2538
return "0x1234";
2639
}
2740

41+
function mockFunctionWithArgsReturn(uint256 a, uint256 b) public payable returns (uint256, uint256) {
42+
emit MockFunctionCalledWithArgs(a, b);
43+
return (a, b);
44+
}
45+
46+
function mockFunctionWithArgsReturnWritesStorage(
47+
bytes32 slot,
48+
bytes32 value,
49+
uint256 a,
50+
uint256 b
51+
) public payable returns (uint256, uint256) {
52+
assembly ("memory-safe") {
53+
sstore(slot, value)
54+
}
55+
emit MockFunctionCalledWithArgs(a, b);
56+
return (a, b);
57+
}
58+
2859
function mockFunctionNonPayable() public returns (string memory) {
2960
emit MockFunctionCalled();
3061

@@ -35,6 +66,10 @@ contract CallReceiverMock {
3566
return "0x1234";
3667
}
3768

69+
function mockStaticFunctionWithArgsReturn(uint256 a, uint256 b) public pure returns (uint256, uint256) {
70+
return (a, b);
71+
}
72+
3873
function mockFunctionRevertsNoReason() public payable {
3974
revert();
4075
}
@@ -53,13 +88,6 @@ contract CallReceiverMock {
5388
}
5489
}
5590

56-
function mockFunctionWritesStorage(bytes32 slot, bytes32 value) public returns (string memory) {
57-
assembly {
58-
sstore(slot, value)
59-
}
60-
return "0x1234";
61-
}
62-
6391
function mockFunctionExtra() public payable {
6492
emit MockFunctionCalledExtra(msg.sender, msg.value);
6593
}

contracts/mocks/Stateless.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ import {ERC7913P256Verifier} from "../utils/cryptography/verifiers/ERC7913P256Ve
3333
import {ERC7913RSAVerifier} from "../utils/cryptography/verifiers/ERC7913RSAVerifier.sol";
3434
import {Heap} from "../utils/structs/Heap.sol";
3535
import {InteroperableAddress} from "../utils/draft-InteroperableAddress.sol";
36+
import {LowLevelCall} from "../utils/LowLevelCall.sol";
3637
import {Math} from "../utils/math/Math.sol";
38+
import {Memory} from "../utils/Memory.sol";
3739
import {MerkleProof} from "../utils/cryptography/MerkleProof.sol";
3840
import {MessageHashUtils} from "../utils/cryptography/MessageHashUtils.sol";
3941
import {Nonces} from "../utils/Nonces.sol";
@@ -50,7 +52,6 @@ import {SignatureChecker} from "../utils/cryptography/SignatureChecker.sol";
5052
import {SignedMath} from "../utils/math/SignedMath.sol";
5153
import {StorageSlot} from "../utils/StorageSlot.sol";
5254
import {Strings} from "../utils/Strings.sol";
53-
import {Memory} from "../utils/Memory.sol";
5455
import {Time} from "../utils/types/Time.sol";
5556

5657
contract Dummy1234 {}

contracts/token/ERC20/extensions/ERC4626.sol

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ pragma solidity ^0.8.20;
66
import {IERC20, IERC20Metadata, ERC20} from "../ERC20.sol";
77
import {SafeERC20} from "../utils/SafeERC20.sol";
88
import {IERC4626} from "../../../interfaces/IERC4626.sol";
9+
import {LowLevelCall} from "../../../utils/LowLevelCall.sol";
10+
import {Memory} from "../../../utils/Memory.sol";
911
import {Math} from "../../../utils/math/Math.sol";
1012

1113
/**
@@ -84,16 +86,17 @@ abstract contract ERC4626 is ERC20, IERC4626 {
8486
* @dev Attempts to fetch the asset decimals. A return value of false indicates that the attempt failed in some way.
8587
*/
8688
function _tryGetAssetDecimals(IERC20 asset_) private view returns (bool ok, uint8 assetDecimals) {
87-
(bool success, bytes memory encodedDecimals) = address(asset_).staticcall(
89+
Memory.Pointer ptr = Memory.getFreeMemoryPointer();
90+
(bool success, bytes32 returnedDecimals, ) = LowLevelCall.staticcallReturn64Bytes(
91+
address(asset_),
8892
abi.encodeCall(IERC20Metadata.decimals, ())
8993
);
90-
if (success && encodedDecimals.length >= 32) {
91-
uint256 returnedDecimals = abi.decode(encodedDecimals, (uint256));
92-
if (returnedDecimals <= type(uint8).max) {
93-
return (true, uint8(returnedDecimals));
94-
}
95-
}
96-
return (false, 0);
94+
Memory.setFreeMemoryPointer(ptr);
95+
96+
return
97+
(success && LowLevelCall.returnDataSize() >= 32 && uint256(returnedDecimals) <= type(uint8).max)
98+
? (true, uint8(uint256(returnedDecimals)))
99+
: (false, 0);
97100
}
98101

99102
/**

contracts/utils/Address.sol

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
pragma solidity ^0.8.20;
55

66
import {Errors} from "./Errors.sol";
7+
import {LowLevelCall} from "./LowLevelCall.sol";
78

89
/**
910
* @dev Collection of functions related to the address type
@@ -34,10 +35,13 @@ library Address {
3435
if (address(this).balance < amount) {
3536
revert Errors.InsufficientBalance(address(this).balance, amount);
3637
}
37-
38-
(bool success, bytes memory returndata) = recipient.call{value: amount}("");
39-
if (!success) {
40-
_revert(returndata);
38+
if (LowLevelCall.callNoReturn(recipient, amount, "")) {
39+
// call successful, nothing to do
40+
return;
41+
} else if (LowLevelCall.returnDataSize() > 0) {
42+
LowLevelCall.bubbleRevert();
43+
} else {
44+
revert Errors.FailedCall();
4145
}
4246
}
4347

@@ -76,47 +80,74 @@ library Address {
7680
if (address(this).balance < value) {
7781
revert Errors.InsufficientBalance(address(this).balance, value);
7882
}
79-
(bool success, bytes memory returndata) = target.call{value: value}(data);
80-
return verifyCallResultFromTarget(target, success, returndata);
83+
bool success = LowLevelCall.callNoReturn(target, value, data);
84+
if (success && (LowLevelCall.returnDataSize() > 0 || target.code.length > 0)) {
85+
return LowLevelCall.returnData();
86+
} else if (success) {
87+
revert AddressEmptyCode(target);
88+
} else if (LowLevelCall.returnDataSize() > 0) {
89+
LowLevelCall.bubbleRevert();
90+
} else {
91+
revert Errors.FailedCall();
92+
}
8193
}
8294

8395
/**
8496
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
8597
* but performing a static call.
8698
*/
8799
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
88-
(bool success, bytes memory returndata) = target.staticcall(data);
89-
return verifyCallResultFromTarget(target, success, returndata);
100+
bool success = LowLevelCall.staticcallNoReturn(target, data);
101+
if (success && (LowLevelCall.returnDataSize() > 0 || target.code.length > 0)) {
102+
return LowLevelCall.returnData();
103+
} else if (success) {
104+
revert AddressEmptyCode(target);
105+
} else if (LowLevelCall.returnDataSize() > 0) {
106+
LowLevelCall.bubbleRevert();
107+
} else {
108+
revert Errors.FailedCall();
109+
}
90110
}
91111

92112
/**
93113
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
94114
* but performing a delegate call.
95115
*/
96116
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
97-
(bool success, bytes memory returndata) = target.delegatecall(data);
98-
return verifyCallResultFromTarget(target, success, returndata);
117+
bool success = LowLevelCall.delegatecallNoReturn(target, data);
118+
if (success && (LowLevelCall.returnDataSize() > 0 || target.code.length > 0)) {
119+
return LowLevelCall.returnData();
120+
} else if (success) {
121+
revert AddressEmptyCode(target);
122+
} else if (LowLevelCall.returnDataSize() > 0) {
123+
LowLevelCall.bubbleRevert();
124+
} else {
125+
revert Errors.FailedCall();
126+
}
99127
}
100128

101129
/**
102130
* @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target
103131
* was not a contract or bubbling up the revert reason (falling back to {Errors.FailedCall}) in case
104132
* of an unsuccessful call.
133+
*
134+
* NOTE: This function is DEPRECATED and may be removed in the next major release.
105135
*/
106136
function verifyCallResultFromTarget(
107137
address target,
108138
bool success,
109139
bytes memory returndata
110140
) internal view returns (bytes memory) {
111-
if (!success) {
112-
_revert(returndata);
113-
} else {
114-
// only check if target is a contract if the call was successful and the return data is empty
115-
// otherwise we already know that it was a contract
116-
if (returndata.length == 0 && target.code.length == 0) {
117-
revert AddressEmptyCode(target);
118-
}
141+
// only check if target is a contract if the call was successful and the return data is empty
142+
// otherwise we already know that it was a contract
143+
if (success && (returndata.length > 0 || target.code.length > 0)) {
119144
return returndata;
145+
} else if (success) {
146+
revert AddressEmptyCode(target);
147+
} else if (returndata.length > 0) {
148+
LowLevelCall.bubbleRevert(returndata);
149+
} else {
150+
revert Errors.FailedCall();
120151
}
121152
}
122153

@@ -125,23 +156,10 @@ library Address {
125156
* revert reason or with a default {Errors.FailedCall} error.
126157
*/
127158
function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) {
128-
if (!success) {
129-
_revert(returndata);
130-
} else {
159+
if (success) {
131160
return returndata;
132-
}
133-
}
134-
135-
/**
136-
* @dev Reverts with returndata if present. Otherwise reverts with {Errors.FailedCall}.
137-
*/
138-
function _revert(bytes memory returndata) private pure {
139-
// Look for revert reason and bubble it up if present
140-
if (returndata.length > 0) {
141-
// The easiest way to bubble the revert reason is using memory via assembly
142-
assembly ("memory-safe") {
143-
revert(add(returndata, 0x20), mload(returndata))
144-
}
161+
} else if (returndata.length > 0) {
162+
LowLevelCall.bubbleRevert(returndata);
145163
} else {
146164
revert Errors.FailedCall();
147165
}

contracts/utils/Create2.sol

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
pragma solidity ^0.8.20;
55

66
import {Errors} from "./Errors.sol";
7+
import {LowLevelCall} from "./LowLevelCall.sol";
78

89
/**
910
* @dev Helper to make usage of the `CREATE2` EVM opcode easier and safer.
@@ -43,15 +44,13 @@ library Create2 {
4344
}
4445
assembly ("memory-safe") {
4546
addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt)
46-
// if no address was created, and returndata is not empty, bubble revert
47-
if and(iszero(addr), not(iszero(returndatasize()))) {
48-
let p := mload(0x40)
49-
returndatacopy(p, 0, returndatasize())
50-
revert(p, returndatasize())
51-
}
5247
}
5348
if (addr == address(0)) {
54-
revert Errors.FailedDeployment();
49+
if (LowLevelCall.returnDataSize() == 0) {
50+
revert Errors.FailedDeployment();
51+
} else {
52+
LowLevelCall.bubbleRevert();
53+
}
5554
}
5655
}
5756

0 commit comments

Comments
 (0)