Skip to content
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

Implement LowLevelCall library #5094

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dull-students-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`Memory`: Add library with utilities to manipulate memory
5 changes: 5 additions & 0 deletions .changeset/sharp-scissors-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`LowLevelCall`: Add a library to perform low-level calls and deal with the `returndata` more granularly.
25 changes: 16 additions & 9 deletions contracts/access/manager/AuthorityUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
pragma solidity ^0.8.20;

import {IAuthority} from "./IAuthority.sol";
import {Memory} from "../../utils/Memory.sol";
import {LowLevelCall} from "../../utils/LowLevelCall.sol";

library AuthorityUtils {
/**
Expand All @@ -17,16 +19,21 @@ library AuthorityUtils {
address target,
bytes4 selector
) internal view returns (bool immediate, uint32 delay) {
(bool success, bytes memory data) = authority.staticcall(
abi.encodeCall(IAuthority.canCall, (caller, target, selector))
Memory.Pointer ptr = Memory.getFreePointer();
bytes memory params = abi.encodeCall(IAuthority.canCall, (caller, target, selector));
(bool success, bytes32 immediateWord, bytes32 delayWord) = LowLevelCall.staticcallReturnBytes32Pair(
authority,
params
);
if (success) {
if (data.length >= 0x40) {
(immediate, delay) = abi.decode(data, (bool, uint32));
} else if (data.length >= 0x20) {
immediate = abi.decode(data, (bool));
}
Memory.setFreePointer(ptr);

if (!success) {
return (false, 0);
}
return (immediate, delay);

return (
uint256(immediateWord) != 0,
uint32(uint256(delayWord)) // Intentional overflow to truncate the higher 224 bits
);
}
}
10 changes: 10 additions & 0 deletions contracts/mocks/CallReceiverMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ contract CallReceiverMock {
return "0x1234";
}

function mockFunctionWithArgsReturn(uint256 a, uint256 b) public payable returns (uint256, uint256) {
emit MockFunctionCalledWithArgs(a, b);

return (a, b);
}

function mockFunctionNonPayable() public returns (string memory) {
emit MockFunctionCalled();

Expand All @@ -34,6 +40,10 @@ contract CallReceiverMock {
return "0x1234";
}

function mockStaticFunctionWithArgsReturn(uint256 a, uint256 b) public pure returns (uint256, uint256) {
return (a, b);
}

function mockFunctionRevertsNoReason() public payable {
revert();
}
Expand Down
2 changes: 2 additions & 0 deletions contracts/mocks/Stateless.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ import {ERC165} from "../utils/introspection/ERC165.sol";
import {ERC165Checker} from "../utils/introspection/ERC165Checker.sol";
import {ERC1967Utils} from "../proxy/ERC1967/ERC1967Utils.sol";
import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol";
import {LowLevelCall} from "../utils/LowLevelCall.sol";
import {Heap} from "../utils/structs/Heap.sol";
import {Math} from "../utils/math/Math.sol";
import {Memory} from "../utils/Memory.sol";
import {MerkleProof} from "../utils/cryptography/MerkleProof.sol";
import {MessageHashUtils} from "../utils/cryptography/MessageHashUtils.sol";
import {P256} from "../utils/cryptography/P256.sol";
Expand Down
30 changes: 14 additions & 16 deletions contracts/token/ERC20/extensions/ERC4626.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {IERC20, IERC20Metadata, ERC20} from "../ERC20.sol";
import {SafeERC20} from "../utils/SafeERC20.sol";
import {IERC4626} from "../../../interfaces/IERC4626.sol";
import {Math} from "../../../utils/math/Math.sol";
import {Memory} from "../../../utils/Memory.sol";
import {LowLevelCall} from "../../../utils/LowLevelCall.sol";

/**
* @dev Implementation of the ERC-4626 "Tokenized Vault Standard" as defined in
Expand Down Expand Up @@ -75,25 +77,21 @@ abstract contract ERC4626 is ERC20, IERC4626 {
* @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC-20 or ERC-777).
*/
constructor(IERC20 asset_) {
(bool success, uint8 assetDecimals) = _tryGetAssetDecimals(asset_);
_underlyingDecimals = success ? assetDecimals : 18;
_underlyingDecimals = _tryGetAssetDecimalsWithFallback(asset_, 18);
_asset = asset_;
}

/**
* @dev Attempts to fetch the asset decimals. A return value of false indicates that the attempt failed in some way.
*/
function _tryGetAssetDecimals(IERC20 asset_) private view returns (bool, uint8) {
(bool success, bytes memory encodedDecimals) = address(asset_).staticcall(
abi.encodeCall(IERC20Metadata.decimals, ())
);
if (success && encodedDecimals.length >= 32) {
uint256 returnedDecimals = abi.decode(encodedDecimals, (uint256));
if (returnedDecimals <= type(uint8).max) {
return (true, uint8(returnedDecimals));
}
}
return (false, 0);
function _tryGetAssetDecimalsWithFallback(IERC20 asset_, uint8 defaultValue) private view returns (uint8) {
Memory.Pointer ptr = Memory.getFreePointer();
bytes memory params = abi.encodeCall(IERC20Metadata.decimals, ());

(bool success, bytes32 rawValue) = LowLevelCall.staticcallReturnBytes32(address(asset_), params);
uint256 length = LowLevelCall.returnDataSize();
uint256 value = uint256(rawValue);

Memory.setFreePointer(ptr);

return uint8(Math.ternary(success && length >= 0x20 && value <= type(uint8).max, value, defaultValue));
}

/**
Expand Down
38 changes: 18 additions & 20 deletions contracts/token/ERC20/utils/SafeERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ pragma solidity ^0.8.20;
import {IERC20} from "../IERC20.sol";
import {IERC1363} from "../../../interfaces/IERC1363.sol";
import {Address} from "../../../utils/Address.sol";
import {Memory} from "../../../utils/Memory.sol";
import {LowLevelCall} from "../../../utils/LowLevelCall.sol";

/**
* @title SafeERC20
Expand All @@ -32,15 +34,19 @@ library SafeERC20 {
* non-reverting calls are assumed to be successful.
*/
function safeTransfer(IERC20 token, address to, uint256 value) internal {
Memory.Pointer ptr = Memory.getFreePointer();
_callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
Memory.setFreePointer(ptr);
}

/**
* @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
* calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
*/
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
Memory.Pointer ptr = Memory.getFreePointer();
_callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
Memory.setFreePointer(ptr);
}

/**
Expand Down Expand Up @@ -72,12 +78,13 @@ library SafeERC20 {
* to be set to zero before setting it to a non-zero value, such as USDT.
*/
function forceApprove(IERC20 token, address spender, uint256 value) internal {
Memory.Pointer ptr = Memory.getFreePointer();
bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));

if (!_callOptionalReturnBool(token, approvalCall)) {
_callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));
_callOptionalReturn(token, approvalCall);
}
Memory.setFreePointer(ptr);
}

/**
Expand Down Expand Up @@ -144,21 +151,18 @@ library SafeERC20 {
* This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
uint256 returnSize;
uint256 returnValue;
(bool success, bytes32 returnValue) = LowLevelCall.callReturnBytes32(address(token), data);
uint256 returnSize = LowLevelCall.returnDataSize();

assembly ("memory-safe") {
let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
// bubble errors
if iszero(success) {
let ptr := mload(0x40)
returndatacopy(ptr, 0, returndatasize())
revert(ptr, returndatasize())
// Bubble up revert reason
returndatacopy(data, 0, returnSize)
revert(data, returnSize)
}
returnSize := returndatasize()
returnValue := mload(0)
}

if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {
if (returnSize == 0 ? address(token).code.length == 0 : uint256(returnValue) != 1) {
revert SafeERC20FailedOperation(address(token));
}
}
Expand All @@ -172,14 +176,8 @@ library SafeERC20 {
* This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.
*/
function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
bool success;
uint256 returnSize;
uint256 returnValue;
assembly ("memory-safe") {
success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
returnSize := returndatasize()
returnValue := mload(0)
}
return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);
(bool success, bytes32 returnValue) = LowLevelCall.callReturnBytes32(address(token), data);
uint256 returnSize = LowLevelCall.returnDataSize();
return success && (returnSize == 0 ? address(token).code.length > 0 : uint256(returnValue) == 1);
}
}
3 changes: 2 additions & 1 deletion contracts/utils/Address.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
pragma solidity ^0.8.20;

import {Errors} from "./Errors.sol";
import {LowLevelCall} from "./LowLevelCall.sol";

/**
* @dev Collection of functions related to the address type
Expand Down Expand Up @@ -35,7 +36,7 @@ library Address {
revert Errors.InsufficientBalance(address(this).balance, amount);
}

(bool success, ) = recipient.call{value: amount}("");
bool success = LowLevelCall.callRaw(recipient, "", amount);
if (!success) {
revert Errors.FailedCall();
}
Expand Down
120 changes: 120 additions & 0 deletions contracts/utils/LowLevelCall.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Errors} from "./Errors.sol";

/**
* @dev Library of low level call functions that implement different calling strategies to deal with the return data.
*
* WARNING: Using this library requires an advanced understanding of Solidity and how the EVM works. It is recommended
* to use the {Address} library instead.
*/
library LowLevelCall {
/// === 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) {
return callRaw(target, data, 0);
}

/// @dev Same as {callRaw}, but allows to specify the value to be sent in the call.
function callRaw(address target, bytes memory data, uint256 value) internal returns (bool success) {
assembly ("memory-safe") {
success := call(gas(), target, value, add(data, 0x20), mload(data), 0, 0)
}
}

/// @dev Performs a Solidity function call using a low level `call` and returns the first 32 bytes of the result
/// in the scratch space of memory. Useful for functions that return a single-word value.
///
/// WARNING: Do not assume that the result is zero if `success` is false. Memory can be already allocated
/// and this function doesn't zero it out.
function callReturnBytes32(address target, bytes memory data) internal returns (bool success, bytes32 result) {
return callReturnBytes32(target, data, 0);
}

/// @dev Same as {callReturnBytes32}, but allows to specify the value to be sent in the call.
function callReturnBytes32(
address target,
bytes memory data,
uint256 value
) internal returns (bool success, bytes32 result) {
assembly ("memory-safe") {
success := call(gas(), target, value, add(data, 0x20), mload(data), 0, 0x20)
result := mload(0)
}
}

/// @dev Performs a Solidity function call using a low level `call` and returns the first 64 bytes of the result
/// in the scratch space of memory. Useful for functions that return a tuple of single-word values.
///
/// WARNING: Do not assume that the results are zero if `success` is false. Memory can be already allocated
/// and this function doesn't zero it out.
function callReturnBytes32Pair(
address target,
bytes memory data
) internal returns (bool success, bytes32 result1, bytes32 result2) {
return callReturnBytes32Pair(target, data, 0);
}

/// @dev Same as {callReturnBytes32Pair}, but allows to specify the value to be sent in the call.
function callReturnBytes32Pair(
address target,
bytes memory data,
uint256 value
) internal returns (bool success, bytes32 result1, bytes32 result2) {
assembly ("memory-safe") {
success := call(gas(), target, value, add(data, 0x20), mload(data), 0, 0x40)
result1 := mload(0)
result2 := mload(0x20)
}
}

/// === STATICCALL ===

/// @dev Performs a Solidity function call using a low level `staticcall` and ignoring the return data.
function staticcallRaw(address target, bytes memory data) internal view returns (bool success) {
assembly ("memory-safe") {
success := staticcall(gas(), target, add(data, 0x20), mload(data), 0, 0)
}
}

/// @dev Performs a Solidity function call using a low level `staticcall` and returns the first 32 bytes of the result
/// in the scratch space of memory. Useful for functions that return a single-word value.
///
/// WARNING: Do not assume that the result is zero if `success` is false. Memory can be already allocated
/// and this function doesn't zero it out.
function staticcallReturnBytes32(
address target,
bytes memory data
) internal view returns (bool success, bytes32 result) {
assembly ("memory-safe") {
success := staticcall(gas(), target, add(data, 0x20), mload(data), 0, 0x20)
result := mload(0)
}
}

/// @dev Performs a Solidity function call using a low level `staticcall` and returns the first 64 bytes of the result
/// in the scratch space of memory. Useful for functions that return a tuple of single-word values.
///
/// WARNING: Do not assume that the results are zero if `success` is false. Memory can be already allocated
/// and this function doesn't zero it out.
function staticcallReturnBytes32Pair(
address target,
bytes memory data
) internal view returns (bool success, bytes32 result1, bytes32 result2) {
assembly ("memory-safe") {
success := staticcall(gas(), target, add(data, 0x20), mload(data), 0, 0x40)
result1 := mload(0)
result2 := mload(0x20)
}
}

/// @dev Returns the size of the return data buffer.
function returnDataSize() internal pure returns (uint256 size) {
assembly ("memory-safe") {
size := returndatasize()
}
}
}
34 changes: 34 additions & 0 deletions contracts/utils/Memory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

/// @dev Memory utility library.
library Memory {
type Pointer is bytes32;

/// @dev Returns a memory pointer to the current free memory pointer.
function getFreePointer() internal pure returns (Pointer ptr) {
assembly ("memory-safe") {
ptr := mload(0x40)
}
}

/// @dev Sets the free memory pointer to a specific value.
///
/// WARNING: Everything after the pointer may be overwritten.
function setFreePointer(Pointer ptr) internal pure {
assembly ("memory-safe") {
mstore(0x40, ptr)
}
}

/// @dev Pointer to `bytes32`.
function asBytes32(Pointer ptr) internal pure returns (bytes32) {
return Pointer.unwrap(ptr);
}

/// @dev `bytes32` to pointer.
function asPointer(bytes32 value) internal pure returns (Pointer) {
return Pointer.wrap(value);
}
}
Loading
Loading