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 4 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
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.saveFreePointer();
bytes memory params = abi.encodeCall(IAuthority.canCall, (caller, target, selector));
(bool success, bytes32 immediateWord, bytes32 delayWord) = LowLevelCall.staticcallReturnScratchBytes32Pair(
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.loadFreePointer(ptr);

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

return (
uint256(immediateWord) != 0,
uint32(uint256(delayWord)) // Intentional overflow to truncate the higher 224 bits
);
}
}
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.saveFreePointer();
bytes memory params = abi.encodeCall(IERC20Metadata.decimals, ());

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

Memory.loadFreePointer(ptr);

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

/**
Expand Down
8 changes: 7 additions & 1 deletion contracts/token/ERC20/utils/SafeERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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";

/**
* @title SafeERC20
Expand Down Expand Up @@ -34,15 +35,19 @@ library SafeERC20 {
* non-reverting calls are assumed to be successful.
*/
function safeTransfer(IERC20 token, address to, uint256 value) internal {
Memory.Pointer ptr = Memory.saveFreePointer();
_callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
Memory.loadFreePointer(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.saveFreePointer();
_callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
Memory.loadFreePointer(ptr);
}

/**
Expand Down Expand Up @@ -74,12 +79,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.saveFreePointer();
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.loadFreePointer(ptr);
}

/**
Expand Down
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
99 changes: 99 additions & 0 deletions contracts/utils/LowLevelCall.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// 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.
*/
library LowLevelCall {
/// === CALL ===

/// @dev Performs a Solidity function call using a low level `call` and ignoring the return data.
function callRaw(address target, uint256 value, bytes memory data) 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 callReturnScratchBytes32(
address target,
uint256 value,
bytes memory data
) 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 values.
ernestognw marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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 callReturnScratchBytes32Pair(
address target,
uint256 value,
bytes memory data
) 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 staticcallReturnScratchBytes32(
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 values.
ernestognw marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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 staticcallReturnScratchBytes32Pair(
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()
}
}
}
24 changes: 24 additions & 0 deletions contracts/utils/Memory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// 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 saveFreePointer() 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 loadFreePointer(Pointer ptr) internal pure {
assembly ("memory-safe") {
mstore(0x40, ptr)
}
}
}
17 changes: 11 additions & 6 deletions contracts/utils/cryptography/SignatureChecker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ pragma solidity ^0.8.20;

import {ECDSA} from "./ECDSA.sol";
import {IERC1271} from "../../interfaces/IERC1271.sol";
import {Memory} from "../Memory.sol";
import {LowLevelCall} from "../LowLevelCall.sol";

/**
* @dev Signature verification helper that can be used instead of `ECDSA.recover` to seamlessly support both ECDSA
Expand Down Expand Up @@ -40,11 +42,14 @@ library SignatureChecker {
bytes32 hash,
bytes memory signature
) internal view returns (bool) {
(bool success, bytes memory result) = signer.staticcall(
abi.encodeCall(IERC1271.isValidSignature, (hash, signature))
);
return (success &&
result.length >= 32 &&
abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector));
bytes4 magic = IERC1271.isValidSignature.selector;

Memory.Pointer ptr = Memory.saveFreePointer();
bytes memory params = abi.encodeCall(IERC1271.isValidSignature, (hash, signature));
(bool success, bytes32 result) = LowLevelCall.staticcallReturnScratchBytes32(signer, params);
uint256 length = LowLevelCall.returnDataSize();
Memory.loadFreePointer(ptr);

return success && length >= 32 && result == bytes32(magic);
}
}