From 5dfb2b3e7d39bf53556b5b20d37fcc75e3acf755 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 19 Jun 2024 21:43:14 -0600 Subject: [PATCH 01/20] Implement LowLevelCall library --- contracts/access/manager/AuthorityUtils.sol | 25 ++-- contracts/token/ERC20/extensions/ERC4626.sol | 30 ++-- contracts/token/ERC20/utils/SafeERC20.sol | 8 +- contracts/utils/Address.sol | 3 +- contracts/utils/LowLevelCall.sol | 140 ++++++++++++++++++ contracts/utils/Memory.sol | 19 +++ .../utils/cryptography/SignatureChecker.sol | 17 ++- 7 files changed, 209 insertions(+), 33 deletions(-) create mode 100644 contracts/utils/LowLevelCall.sol create mode 100644 contracts/utils/Memory.sol diff --git a/contracts/access/manager/AuthorityUtils.sol b/contracts/access/manager/AuthorityUtils.sol index fb3018ca805..c35bb31f141 100644 --- a/contracts/access/manager/AuthorityUtils.sol +++ b/contracts/access/manager/AuthorityUtils.sol @@ -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 { /** @@ -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.staticCallReturnBytes32Tuple( + 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 + ); } } diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index c71b14ad48c..1c63174c155 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -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 @@ -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.staticCallReturnBytes32(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)); } /** diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index 58f9fcf4d68..08bae09ce06 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -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 @@ -34,7 +35,9 @@ 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); } /** @@ -42,7 +45,9 @@ library SafeERC20 { * 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); } /** @@ -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); } /** diff --git a/contracts/utils/Address.sol b/contracts/utils/Address.sol index 53a3c442049..bb371e9fa28 100644 --- a/contracts/utils/Address.sol +++ b/contracts/utils/Address.sol @@ -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 @@ -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(); } diff --git a/contracts/utils/LowLevelCall.sol b/contracts/utils/LowLevelCall.sol new file mode 100644 index 00000000000..42c7f24ba9c --- /dev/null +++ b/contracts/utils/LowLevelCall.sol @@ -0,0 +1,140 @@ +// 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 callReturnBytes32( + 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. + /// + /// 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 callReturnBytes32Tuple( + 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) + } + } + + /// @dev Performs a Solidity function call using a low level `call` and writes the result to the memory location + /// specified by `resultPtr`. + /// + /// IMPORTANT: This function assumes that the length of the memory array is stored in the first 32 bytes of the array and uses it for truncating + /// returndata if it's longer than the allocated memory to avoid corrupting to further places in memory. The `resultPtr` should be a + /// memory location that is already allocated with a predefined length. + /// + /// WARNING: Do not use if writing to `resultPtr` is not safe according to + /// the https://docs.soliditylang.org/en/latest/assembly.html#memory-safety[Solidity documentation]. + function callReturnOverride( + address target, + uint256 value, + bytes memory data, + bytes memory resultPtr + ) internal returns (bool success) { + assembly ("memory-safe") { + let maxSize := mload(resultPtr) + success := call(gas(), target, value, add(data, 0x20), mload(data), resultPtr, maxSize) + } + } + + /// === 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 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 staticCallReturnBytes32Tuple( + 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 Performs a Solidity function call using a low level `staticcall` and writes the result to the memory location + /// specified by `resultPtr`. + /// + /// IMPORTANT: This function assumes that the length of the memory array is stored in the first 32 bytes of the array and uses it for truncating + /// returndata if it's longer than the allocated memory to avoid corrupting to further places in memory. The `resultPtr` should be a + /// memory location that is already allocated with a predefined length. + /// + /// WARNING: Do not use if writing to `resultPtr` is not safe according to + /// the https://docs.soliditylang.org/en/latest/assembly.html#memory-safety[Solidity documentation]. + function staticCallReturnOverride( + address target, + bytes memory data, + bytes memory resultPtr + ) internal view returns (bool success) { + assembly ("memory-safe") { + let maxSize := mload(resultPtr) + success := staticcall(gas(), target, add(data, 0x20), mload(data), resultPtr, maxSize) + } + } + + /// @dev Returns the size of the return data buffer. + function returnDataSize() internal pure returns (uint256 size) { + assembly ("memory-safe") { + size := returndatasize() + } + } +} diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol new file mode 100644 index 00000000000..3f0ea05f70c --- /dev/null +++ b/contracts/utils/Memory.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +library Memory { + type Pointer is bytes32; + + function saveFreePointer() internal pure returns (Pointer ptr) { + assembly ("memory-safe") { + ptr := mload(0x40) + } + } + + function loadFreePointer(Pointer ptr) internal pure { + assembly ("memory-safe") { + mstore(0x40, ptr) + } + } +} diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index 9aaa2e0716c..b0d8c3c644e 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -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 @@ -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.staticCallReturnBytes32(signer, params); + uint256 length = LowLevelCall.returnDataSize(); + Memory.loadFreePointer(ptr); + + return success && length >= 32 && result == bytes32(magic); } } From e8e8438b860b8f68c487fe3324012279e05125e0 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 19 Jun 2024 21:52:39 -0600 Subject: [PATCH 02/20] Add comments to Memory library --- contracts/utils/Memory.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol index 3f0ea05f70c..f959a88bf7c 100644 --- a/contracts/utils/Memory.sol +++ b/contracts/utils/Memory.sol @@ -2,15 +2,20 @@ 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) From 8e1cc9c38b36116ec9ea75b9abcd8f0fb1d9c2aa Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sat, 22 Jun 2024 14:50:15 -0600 Subject: [PATCH 03/20] Update names --- contracts/access/manager/AuthorityUtils.sol | 2 +- contracts/token/ERC20/extensions/ERC4626.sol | 2 +- contracts/utils/LowLevelCall.sol | 49 ++----------------- .../utils/cryptography/SignatureChecker.sol | 2 +- 4 files changed, 7 insertions(+), 48 deletions(-) diff --git a/contracts/access/manager/AuthorityUtils.sol b/contracts/access/manager/AuthorityUtils.sol index c35bb31f141..4f9b6c90c94 100644 --- a/contracts/access/manager/AuthorityUtils.sol +++ b/contracts/access/manager/AuthorityUtils.sol @@ -21,7 +21,7 @@ library AuthorityUtils { ) internal view returns (bool immediate, uint32 delay) { Memory.Pointer ptr = Memory.saveFreePointer(); bytes memory params = abi.encodeCall(IAuthority.canCall, (caller, target, selector)); - (bool success, bytes32 immediateWord, bytes32 delayWord) = LowLevelCall.staticCallReturnBytes32Tuple( + (bool success, bytes32 immediateWord, bytes32 delayWord) = LowLevelCall.staticcallReturnScratchBytes32Pair( authority, params ); diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index 1c63174c155..c5f70604bcf 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -85,7 +85,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { Memory.Pointer ptr = Memory.saveFreePointer(); bytes memory params = abi.encodeCall(IERC20Metadata.decimals, ()); - (bool success, bytes32 rawValue) = LowLevelCall.staticCallReturnBytes32(address(asset_), params); + (bool success, bytes32 rawValue) = LowLevelCall.staticcallReturnScratchBytes32(address(asset_), params); uint256 length = LowLevelCall.returnDataSize(); uint256 value = uint256(rawValue); diff --git a/contracts/utils/LowLevelCall.sol b/contracts/utils/LowLevelCall.sol index 42c7f24ba9c..03bc6015bba 100644 --- a/contracts/utils/LowLevelCall.sol +++ b/contracts/utils/LowLevelCall.sol @@ -22,7 +22,7 @@ library LowLevelCall { /// /// 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( + function callReturnScratchBytes32( address target, uint256 value, bytes memory data @@ -38,7 +38,7 @@ library LowLevelCall { /// /// 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 callReturnBytes32Tuple( + function callReturnScratchBytes32Pair( address target, uint256 value, bytes memory data @@ -50,27 +50,6 @@ library LowLevelCall { } } - /// @dev Performs a Solidity function call using a low level `call` and writes the result to the memory location - /// specified by `resultPtr`. - /// - /// IMPORTANT: This function assumes that the length of the memory array is stored in the first 32 bytes of the array and uses it for truncating - /// returndata if it's longer than the allocated memory to avoid corrupting to further places in memory. The `resultPtr` should be a - /// memory location that is already allocated with a predefined length. - /// - /// WARNING: Do not use if writing to `resultPtr` is not safe according to - /// the https://docs.soliditylang.org/en/latest/assembly.html#memory-safety[Solidity documentation]. - function callReturnOverride( - address target, - uint256 value, - bytes memory data, - bytes memory resultPtr - ) internal returns (bool success) { - assembly ("memory-safe") { - let maxSize := mload(resultPtr) - success := call(gas(), target, value, add(data, 0x20), mload(data), resultPtr, maxSize) - } - } - /// === STATICCALL === /// @dev Performs a Solidity function call using a low level `staticcall` and ignoring the return data. @@ -85,7 +64,7 @@ library LowLevelCall { /// /// 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( + function staticcallReturnScratchBytes32( address target, bytes memory data ) internal view returns (bool success, bytes32 result) { @@ -100,7 +79,7 @@ library LowLevelCall { /// /// 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 staticCallReturnBytes32Tuple( + function staticcallReturnScratchBytes32Pair( address target, bytes memory data ) internal view returns (bool success, bytes32 result1, bytes32 result2) { @@ -111,26 +90,6 @@ library LowLevelCall { } } - /// @dev Performs a Solidity function call using a low level `staticcall` and writes the result to the memory location - /// specified by `resultPtr`. - /// - /// IMPORTANT: This function assumes that the length of the memory array is stored in the first 32 bytes of the array and uses it for truncating - /// returndata if it's longer than the allocated memory to avoid corrupting to further places in memory. The `resultPtr` should be a - /// memory location that is already allocated with a predefined length. - /// - /// WARNING: Do not use if writing to `resultPtr` is not safe according to - /// the https://docs.soliditylang.org/en/latest/assembly.html#memory-safety[Solidity documentation]. - function staticCallReturnOverride( - address target, - bytes memory data, - bytes memory resultPtr - ) internal view returns (bool success) { - assembly ("memory-safe") { - let maxSize := mload(resultPtr) - success := staticcall(gas(), target, add(data, 0x20), mload(data), resultPtr, maxSize) - } - } - /// @dev Returns the size of the return data buffer. function returnDataSize() internal pure returns (uint256 size) { assembly ("memory-safe") { diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index b0d8c3c644e..9e1d0d3e29f 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -46,7 +46,7 @@ library SignatureChecker { Memory.Pointer ptr = Memory.saveFreePointer(); bytes memory params = abi.encodeCall(IERC1271.isValidSignature, (hash, signature)); - (bool success, bytes32 result) = LowLevelCall.staticCallReturnBytes32(signer, params); + (bool success, bytes32 result) = LowLevelCall.staticcallReturnScratchBytes32(signer, params); uint256 length = LowLevelCall.returnDataSize(); Memory.loadFreePointer(ptr); From 7c7be9ae95d0ec9e2f467e4bda1d2af5db57331d Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sat, 22 Jun 2024 14:51:16 -0600 Subject: [PATCH 04/20] Update name --- contracts/utils/LowLevelCall.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/LowLevelCall.sol b/contracts/utils/LowLevelCall.sol index 03bc6015bba..f2d76f1f18c 100644 --- a/contracts/utils/LowLevelCall.sol +++ b/contracts/utils/LowLevelCall.sol @@ -53,7 +53,7 @@ library LowLevelCall { /// === 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) { + 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) } From 4ad1b2358cdcde70dc87fea4b758d2c1137cd3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Mon, 24 Jun 2024 18:38:28 -0600 Subject: [PATCH 05/20] Apply suggestions from code review --- contracts/utils/LowLevelCall.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/utils/LowLevelCall.sol b/contracts/utils/LowLevelCall.sol index f2d76f1f18c..086c9ff65f3 100644 --- a/contracts/utils/LowLevelCall.sol +++ b/contracts/utils/LowLevelCall.sol @@ -34,7 +34,7 @@ library LowLevelCall { } /// @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. + /// 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. @@ -75,7 +75,7 @@ library LowLevelCall { } /// @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. + /// 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. From 5240323a19eaf01f5f9cfe811ee0c7fffe08ed94 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 25 Jun 2024 10:44:47 -0600 Subject: [PATCH 06/20] Add LowLevelCall to SafeERC20 --- contracts/token/ERC20/utils/SafeERC20.sol | 30 +++++++------------ .../utils/cryptography/SignatureChecker.sol | 4 +-- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index a5a3b31d173..92109af755a 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -7,6 +7,7 @@ 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 @@ -150,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.callReturnScratchBytes32(address(token), 0, 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)); } } @@ -178,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.callReturnScratchBytes32(address(token), 0, data); + uint256 returnSize = LowLevelCall.returnDataSize(); + return success && (returnSize == 0 ? address(token).code.length > 0 : uint256(returnValue) == 1); } } diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index 9e1d0d3e29f..1e1982e8881 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -42,14 +42,12 @@ library SignatureChecker { bytes32 hash, bytes memory signature ) internal view returns (bool) { - 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); + return success && length >= 32 && result == bytes32(IERC1271.isValidSignature.selector); } } From 58b4a96c06fc678220c936d41dd46b4f9188124e Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 25 Jun 2024 10:53:07 -0600 Subject: [PATCH 07/20] Add value versions to call --- contracts/token/ERC20/utils/SafeERC20.sol | 4 ++-- contracts/utils/Address.sol | 2 +- contracts/utils/LowLevelCall.sol | 27 ++++++++++++++++++++--- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index 92109af755a..acacf6b29c1 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -151,7 +151,7 @@ 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 { - (bool success, bytes32 returnValue) = LowLevelCall.callReturnScratchBytes32(address(token), 0, data); + (bool success, bytes32 returnValue) = LowLevelCall.callReturnScratchBytes32(address(token), data); uint256 returnSize = LowLevelCall.returnDataSize(); assembly ("memory-safe") { @@ -176,7 +176,7 @@ 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, bytes32 returnValue) = LowLevelCall.callReturnScratchBytes32(address(token), 0, data); + (bool success, bytes32 returnValue) = LowLevelCall.callReturnScratchBytes32(address(token), data); uint256 returnSize = LowLevelCall.returnDataSize(); return success && (returnSize == 0 ? address(token).code.length > 0 : uint256(returnValue) == 1); } diff --git a/contracts/utils/Address.sol b/contracts/utils/Address.sol index bb371e9fa28..099a3d5405e 100644 --- a/contracts/utils/Address.sol +++ b/contracts/utils/Address.sol @@ -36,7 +36,7 @@ library Address { revert Errors.InsufficientBalance(address(this).balance, amount); } - bool success = LowLevelCall.callRaw(recipient, amount, ""); + bool success = LowLevelCall.callRaw(recipient, "", amount); if (!success) { revert Errors.FailedCall(); } diff --git a/contracts/utils/LowLevelCall.sol b/contracts/utils/LowLevelCall.sol index 086c9ff65f3..2659c5bedd7 100644 --- a/contracts/utils/LowLevelCall.sol +++ b/contracts/utils/LowLevelCall.sol @@ -11,7 +11,12 @@ 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) { + 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) } @@ -24,8 +29,16 @@ library LowLevelCall { /// and this function doesn't zero it out. function callReturnScratchBytes32( address target, - uint256 value, bytes memory data + ) internal returns (bool success, bytes32 result) { + return callReturnScratchBytes32(target, data, 0); + } + + /// @dev Same as {callReturnScratchBytes32}, but allows to specify the value to be sent in the call. + function callReturnScratchBytes32( + 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) @@ -40,8 +53,16 @@ library LowLevelCall { /// 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) { + return callReturnScratchBytes32Pair(target, data, 0); + } + + /// @dev Same as {callReturnScratchBytes32Pair}, but allows to specify the value to be sent in the call. + function callReturnScratchBytes32Pair( + 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) From bbb6aa16dc1e9e0ca97c2ec3eb49ec614c1aeeae Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 26 Jun 2024 14:37:52 -0600 Subject: [PATCH 08/20] Add documentation --- contracts/access/manager/AuthorityUtils.sol | 2 +- contracts/token/ERC20/extensions/ERC4626.sol | 2 +- contracts/token/ERC20/utils/SafeERC20.sol | 4 +- contracts/utils/LowLevelCall.sol | 26 +++++------ contracts/utils/README.adoc | 3 ++ .../utils/cryptography/SignatureChecker.sol | 2 +- docs/modules/ROOT/pages/utilities.adoc | 44 +++++++++++++++++++ 7 files changed, 65 insertions(+), 18 deletions(-) diff --git a/contracts/access/manager/AuthorityUtils.sol b/contracts/access/manager/AuthorityUtils.sol index 4f9b6c90c94..d476f053d25 100644 --- a/contracts/access/manager/AuthorityUtils.sol +++ b/contracts/access/manager/AuthorityUtils.sol @@ -21,7 +21,7 @@ library AuthorityUtils { ) internal view returns (bool immediate, uint32 delay) { Memory.Pointer ptr = Memory.saveFreePointer(); bytes memory params = abi.encodeCall(IAuthority.canCall, (caller, target, selector)); - (bool success, bytes32 immediateWord, bytes32 delayWord) = LowLevelCall.staticcallReturnScratchBytes32Pair( + (bool success, bytes32 immediateWord, bytes32 delayWord) = LowLevelCall.staticcallReturnBytes32Pair( authority, params ); diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index c5f70604bcf..4d8c1064383 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -85,7 +85,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { Memory.Pointer ptr = Memory.saveFreePointer(); bytes memory params = abi.encodeCall(IERC20Metadata.decimals, ()); - (bool success, bytes32 rawValue) = LowLevelCall.staticcallReturnScratchBytes32(address(asset_), params); + (bool success, bytes32 rawValue) = LowLevelCall.staticcallReturnBytes32(address(asset_), params); uint256 length = LowLevelCall.returnDataSize(); uint256 value = uint256(rawValue); diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index acacf6b29c1..41a21d32a7d 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -151,7 +151,7 @@ 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 { - (bool success, bytes32 returnValue) = LowLevelCall.callReturnScratchBytes32(address(token), data); + (bool success, bytes32 returnValue) = LowLevelCall.callReturnBytes32(address(token), data); uint256 returnSize = LowLevelCall.returnDataSize(); assembly ("memory-safe") { @@ -176,7 +176,7 @@ 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, bytes32 returnValue) = LowLevelCall.callReturnScratchBytes32(address(token), data); + (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); } diff --git a/contracts/utils/LowLevelCall.sol b/contracts/utils/LowLevelCall.sol index 2659c5bedd7..bf552a3e78a 100644 --- a/contracts/utils/LowLevelCall.sol +++ b/contracts/utils/LowLevelCall.sol @@ -6,6 +6,9 @@ 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 === @@ -27,15 +30,12 @@ library LowLevelCall { /// /// 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, - bytes memory data - ) internal returns (bool success, bytes32 result) { - return callReturnScratchBytes32(target, data, 0); + function callReturnBytes32(address target, bytes memory data) internal returns (bool success, bytes32 result) { + return callReturnBytes32(target, data, 0); } - /// @dev Same as {callReturnScratchBytes32}, but allows to specify the value to be sent in the call. - function callReturnScratchBytes32( + /// @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 @@ -51,15 +51,15 @@ library LowLevelCall { /// /// 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( + function callReturnBytes32Pair( address target, bytes memory data ) internal returns (bool success, bytes32 result1, bytes32 result2) { - return callReturnScratchBytes32Pair(target, data, 0); + return callReturnBytes32Pair(target, data, 0); } - /// @dev Same as {callReturnScratchBytes32Pair}, but allows to specify the value to be sent in the call. - function callReturnScratchBytes32Pair( + /// @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 @@ -85,7 +85,7 @@ library LowLevelCall { /// /// 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( + function staticcallReturnBytes32( address target, bytes memory data ) internal view returns (bool success, bytes32 result) { @@ -100,7 +100,7 @@ library LowLevelCall { /// /// 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( + function staticcallReturnBytes32Pair( address target, bytes memory data ) internal view returns (bool success, bytes32 result1, bytes32 result2) { diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 056ad3331ac..f1bec98fd6d 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -36,6 +36,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {Context}: An utility for abstracting the sender and calldata in the current execution context. * {Packing}: A library for packing and unpacking multiple values into bytes32 * {Panic}: A library to revert with https://docs.soliditylang.org/en/v0.8.20/control-structures.html#panic-via-assert-and-error-via-require[Solidity panic codes]. + * {LowLevelCall}: Collection of functions to perform calls with low-level assembly. [NOTE] ==== @@ -127,3 +128,5 @@ Ethereum contracts have no native concept of an interface, so applications must {{Packing}} {{Panic}} + +{{LowLevelCall}} diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index 1e1982e8881..10bcc0fff76 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -44,7 +44,7 @@ library SignatureChecker { ) internal view returns (bool) { Memory.Pointer ptr = Memory.saveFreePointer(); bytes memory params = abi.encodeCall(IERC1271.isValidSignature, (hash, signature)); - (bool success, bytes32 result) = LowLevelCall.staticcallReturnScratchBytes32(signer, params); + (bool success, bytes32 result) = LowLevelCall.staticcallReturnBytes32(signer, params); uint256 length = LowLevelCall.returnDataSize(); Memory.loadFreePointer(ptr); diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 506a581dfad..2f39c610d07 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -318,3 +318,47 @@ await instance.multicall([ instance.interface.encodeFunctionData("bar") ]); ---- + +=== LowLevelCall + +The `LowLevelCall` library contains a set of functions to perform external calls with low-level assembly, allowing them to deal with the callee's `returndata` in different ways. This is especially useful to make a call in a way that is safe against return bombing (i.e. the callee allocates too much memory using a long returndata). + +The functions in the library efficiently allocates a fixed sized of the `returndata` up to 64 bytes. You can either ignore the returned data, or get 1 or 2 `bytes32` values. + +[source,solidity] +---- +using LowLevelCall for address; + +function _foo(address target, bytes memory data) internal { + bool success; + bytes32 returnValue1; + bytes32 returnValue2; + + // Ignore return data + success = target.callRaw(data); + + // Copy only 32 bytes from return data + (success, returnValue1) = target.callReturnBytes32(data); + + // Copy two (32 bytes) EVM words from returndata + (success, returnValue1, returnValue2) = target.callReturnBytes32Pair(data); +} +---- + +There are cases where you would like to check the size of the returned data, either to make sure it fits the expected size, or to check it before loading it to memory. In those case, the library also includes a function to separately check the return data size: + + +[source,solidity] +---- +using LowLevelCall for address; + +function _foo(address target, bytes memory data) internal returns (bool returnBool) { + if (!target.callRaw(data)) { + // Unsuccessful call + return false; + } + + // As long as the contract returned data, its content doesn't matter + return LowLevelCall.returnDataSize() >= 32; +} +---- From 044fbbef08a6e1ccaca7916c0f49094ac0756823 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 26 Jun 2024 14:40:52 -0600 Subject: [PATCH 09/20] Add changeset --- .changeset/sharp-scissors-drum.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sharp-scissors-drum.md diff --git a/.changeset/sharp-scissors-drum.md b/.changeset/sharp-scissors-drum.md new file mode 100644 index 00000000000..b701eccf3fa --- /dev/null +++ b/.changeset/sharp-scissors-drum.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`LowLevelCall`: Add a library to perform low-level calls and deal with the `returndata` more granularly. From 85ce0785941c4ca89766bdfb2caf9897f271f216 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 26 Jun 2024 16:28:32 -0600 Subject: [PATCH 10/20] Add LowLevelCall tests --- contracts/mocks/CallReceiverMock.sol | 10 ++ test/utils/LowLevelCall.test.js | 204 +++++++++++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 test/utils/LowLevelCall.test.js diff --git a/contracts/mocks/CallReceiverMock.sol b/contracts/mocks/CallReceiverMock.sol index e371c7db800..496386c4a7c 100644 --- a/contracts/mocks/CallReceiverMock.sol +++ b/contracts/mocks/CallReceiverMock.sol @@ -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(); @@ -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(); } diff --git a/test/utils/LowLevelCall.test.js b/test/utils/LowLevelCall.test.js new file mode 100644 index 00000000000..d83b485cba0 --- /dev/null +++ b/test/utils/LowLevelCall.test.js @@ -0,0 +1,204 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +async function fixture() { + const [recipient, other] = await ethers.getSigners(); + + const mock = await ethers.deployContract('$LowLevelCall'); + const target = await ethers.deployContract('CallReceiverMock'); + const targetEther = await ethers.deployContract('EtherReceiverMock'); + + return { recipient, other, mock, target, targetEther, value: BigInt(1e18) }; +} + +describe('LowLevelCall', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('callRaw', function () { + beforeEach(async function () { + this.call = this.target.interface.encodeFunctionData('mockFunction'); + }); + + it('calls the requested function and returns true', async function () { + await expect(this.mock.$callRaw(this.target, this.call)) + .to.emit(this.target, 'MockFunctionCalled') + .to.emit(this.mock, 'return$callRaw_address_bytes') + .withArgs(true); + }); + + it('calls the requested function with value and returns true', async function () { + await this.other.sendTransaction({ to: this.mock, value: this.value }); + + const tx = this.mock['$callRaw(address,bytes,uint256)'](this.target, this.call, this.value); + await expect(tx).to.changeEtherBalance(this.target, this.value); + await expect(tx).to.emit(this.mock, 'return$callRaw_address_bytes_uint256').withArgs(true); + }); + + it("calls the requested function and returns false if the caller doesn't have enough balance", async function () { + const tx = this.mock['$callRaw(address,bytes,uint256)'](this.target, this.call, this.value); + await expect(tx).to.not.changeEtherBalance(this.target, this.value); + await expect(tx).to.emit(this.mock, 'return$callRaw_address_bytes_uint256').withArgs(false); + }); + + it('calls the requested function and returns false if the subcall reverts', async function () { + const call = this.target.interface.encodeFunctionData('mockFunctionRevertsNoReason'); + const tx = await this.mock.$callRaw(this.target, call); + expect(tx).to.emit(this.mock, 'return$callRaw_address_bytes').withArgs(false); + }); + }); + + describe('callReturnBytes32', function () { + beforeEach(async function () { + this.returnValue = ethers.id('returnDataBytes32'); + this.call = this.target.interface.encodeFunctionData('mockFunctionWithArgsReturn', [ + this.returnValue, + ethers.ZeroHash, + ]); + }); + + it('calls the requested function and returns true', async function () { + await expect(this.mock.$callReturnBytes32(this.target, this.call)) + .to.emit(this.target, 'MockFunctionCalledWithArgs') + .withArgs(this.returnValue, ethers.ZeroHash) + .to.emit(this.mock, 'return$callReturnBytes32_address_bytes') + .withArgs(true, this.returnValue); + }); + + it('calls the requested function with value and returns true', async function () { + await this.other.sendTransaction({ to: this.mock, value: this.value }); + + const tx = this.mock['$callReturnBytes32(address,bytes,uint256)'](this.target, this.call, this.value); + await expect(tx).to.changeEtherBalance(this.target, this.value); + await expect(tx) + .to.emit(this.mock, 'return$callReturnBytes32_address_bytes_uint256') + .withArgs(true, this.returnValue); + }); + + it("calls the requested function and returns false if the caller doesn't have enough balance", async function () { + const tx = this.mock['$callReturnBytes32(address,bytes,uint256)'](this.target, this.call, this.value); + await expect(tx).to.not.changeEtherBalance(this.target, this.value); + await expect(tx) + .to.emit(this.mock, 'return$callReturnBytes32_address_bytes_uint256') + .withArgs(false, ethers.ZeroHash); + }); + + it('calls the requested function and returns false if the subcall reverts', async function () { + const call = this.target.interface.encodeFunctionData('mockFunctionRevertsNoReason'); + const tx = await this.mock.$callReturnBytes32(this.target, call); + expect(tx).to.emit(this.mock, 'return$callReturnBytes32_address_bytes').withArgs(false, ethers.ZeroHash); + }); + }); + + describe('callReturnBytes32Pair', function () { + beforeEach(async function () { + this.returnValue1 = ethers.id('returnDataBytes32Pair1'); + this.returnValue2 = ethers.id('returnDataBytes32Pair2'); + this.call = this.target.interface.encodeFunctionData('mockFunctionWithArgsReturn', [ + this.returnValue1, + this.returnValue2, + ]); + }); + + it('calls the requested function and returns true', async function () { + await expect(this.mock.$callReturnBytes32Pair(this.target, this.call)) + .to.emit(this.target, 'MockFunctionCalledWithArgs') + .withArgs(this.returnValue1, this.returnValue2) + .to.emit(this.mock, 'return$callReturnBytes32Pair_address_bytes') + .withArgs(true, this.returnValue1, this.returnValue2); + }); + + it('calls the requested function with value and returns true', async function () { + await this.other.sendTransaction({ to: this.mock, value: this.value }); + + const tx = this.mock['$callReturnBytes32Pair(address,bytes,uint256)'](this.target, this.call, this.value); + await expect(tx).to.changeEtherBalance(this.target, this.value); + await expect(tx) + .to.emit(this.mock, 'return$callReturnBytes32Pair_address_bytes_uint256') + .withArgs(true, this.returnValue1, this.returnValue2); + }); + + it("calls the requested function and returns false if the caller doesn't have enough balance", async function () { + const tx = this.mock['$callReturnBytes32Pair(address,bytes,uint256)'](this.target, this.call, this.value); + await expect(tx).to.not.changeEtherBalance(this.target, this.value); + await expect(tx) + .to.emit(this.mock, 'return$callReturnBytes32Pair_address_bytes_uint256') + .withArgs(false, ethers.ZeroHash, ethers.ZeroHash); + }); + + it('calls the requested function and returns false if the subcall reverts', async function () { + const call = this.target.interface.encodeFunctionData('mockFunctionRevertsNoReason'); + const tx = await this.mock.$callReturnBytes32Pair(this.target, call); + expect(tx) + .to.emit(this.mock, 'return$callReturnBytes32Pair_address_bytes') + .withArgs(false, ethers.ZeroHash, ethers.ZeroHash); + }); + }); + + describe('staticcallRaw', function () { + it('calls the requested function and returns true', async function () { + const call = this.target.interface.encodeFunctionData('mockStaticFunction'); + expect(await this.mock.$staticcallRaw(this.target, call)).to.equal(true); + }); + + it('calls the requested function and returns false if the subcall reverts', async function () { + const interface = new ethers.Interface(['function mockFunctionDoesNotExist()']); + + const call = interface.encodeFunctionData('mockFunctionDoesNotExist'); + expect(await this.mock.$staticcallRaw(this.target, call)).to.equal(false); + }); + }); + + describe('staticcallReturnBytes32', function () { + beforeEach(async function () { + this.returnValue = ethers.id('returnDataBytes32'); + }); + + it('calls the requested function and returns true', async function () { + const call = this.target.interface.encodeFunctionData('mockStaticFunctionWithArgsReturn', [ + this.returnValue, + ethers.ZeroHash, + ]); + expect(await this.mock.$staticcallReturnBytes32(this.target, call)).to.deep.equal([true, this.returnValue]); + }); + + it('calls the requested function and returns false if the subcall reverts', async function () { + const interface = new ethers.Interface(['function mockFunctionDoesNotExist()']); + + const call = interface.encodeFunctionData('mockFunctionDoesNotExist'); + expect(await this.mock.$staticcallReturnBytes32(this.target, call)).to.deep.equal([false, ethers.ZeroHash]); + }); + }); + + describe('staticcallReturnBytes32Pair', function () { + beforeEach(async function () { + this.returnValue1 = ethers.id('returnDataBytes32Pair1'); + this.returnValue2 = ethers.id('returnDataBytes32Pair2'); + }); + + it('calls the requested function and returns true', async function () { + const call = this.target.interface.encodeFunctionData('mockStaticFunctionWithArgsReturn', [ + this.returnValue1, + this.returnValue2, + ]); + expect(await this.mock.$staticcallReturnBytes32Pair(this.target, call)).to.deep.equal([ + true, + this.returnValue1, + this.returnValue2, + ]); + }); + + it('calls the requested function and returns false if the subcall reverts', async function () { + const interface = new ethers.Interface(['function mockFunctionDoesNotExist()']); + + const call = interface.encodeFunctionData('mockFunctionDoesNotExist'); + expect(await this.mock.$staticcallReturnBytes32Pair(this.target, call)).to.deep.equal([ + false, + ethers.ZeroHash, + ethers.ZeroHash, + ]); + }); + }); +}); From bb1a5554f1ca4bb3a815ba871879744c5edfce47 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 26 Jun 2024 16:53:57 -0600 Subject: [PATCH 11/20] Add tests to memory --- contracts/access/manager/AuthorityUtils.sol | 4 +-- contracts/mocks/MemoryMock.sol | 29 +++++++++++++++++++ contracts/token/ERC20/extensions/ERC4626.sol | 4 +-- contracts/token/ERC20/utils/SafeERC20.sol | 12 ++++---- contracts/utils/Memory.sol | 4 +-- .../utils/cryptography/SignatureChecker.sol | 4 +-- test/utils/Memory.test.js | 23 +++++++++++++++ 7 files changed, 66 insertions(+), 14 deletions(-) create mode 100644 contracts/mocks/MemoryMock.sol create mode 100644 test/utils/Memory.test.js diff --git a/contracts/access/manager/AuthorityUtils.sol b/contracts/access/manager/AuthorityUtils.sol index d476f053d25..d6caeff3c61 100644 --- a/contracts/access/manager/AuthorityUtils.sol +++ b/contracts/access/manager/AuthorityUtils.sol @@ -19,13 +19,13 @@ library AuthorityUtils { address target, bytes4 selector ) internal view returns (bool immediate, uint32 delay) { - Memory.Pointer ptr = Memory.saveFreePointer(); + 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 ); - Memory.loadFreePointer(ptr); + Memory.setFreePointer(ptr); if (!success) { return (false, 0); diff --git a/contracts/mocks/MemoryMock.sol b/contracts/mocks/MemoryMock.sol new file mode 100644 index 00000000000..34b7bd628ca --- /dev/null +++ b/contracts/mocks/MemoryMock.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Memory} from "../utils/Memory.sol"; + +contract MemoryMock { + bytes32 private _ptr; + + modifier _rememberPtr() { + assembly { + mstore(0x40, sload(_ptr.slot)) + } + _; + } + + function _setPointer(bytes32 ptr) public { + _ptr = ptr; + } + + function $setFreePointer(bytes32 ptr) public { + _setPointer(ptr); + return Memory.setFreePointer(Memory.Pointer.wrap(ptr)); + } + + function $getFreePointer() public view _rememberPtr returns (bytes32) { + return Memory.Pointer.unwrap(Memory.getFreePointer()); + } +} diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index 4d8c1064383..e1604792fc6 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -82,14 +82,14 @@ abstract contract ERC4626 is ERC20, IERC4626 { } function _tryGetAssetDecimalsWithFallback(IERC20 asset_, uint8 defaultValue) private view returns (uint8) { - Memory.Pointer ptr = Memory.saveFreePointer(); + 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.loadFreePointer(ptr); + Memory.setFreePointer(ptr); return uint8(Math.ternary(success && length >= 0x20 && value <= type(uint8).max, value, defaultValue)); } diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index 41a21d32a7d..16e40305ee1 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -34,9 +34,9 @@ library SafeERC20 { * non-reverting calls are assumed to be successful. */ function safeTransfer(IERC20 token, address to, uint256 value) internal { - Memory.Pointer ptr = Memory.saveFreePointer(); + Memory.Pointer ptr = Memory.getFreePointer(); _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value))); - Memory.loadFreePointer(ptr); + Memory.setFreePointer(ptr); } /** @@ -44,9 +44,9 @@ library SafeERC20 { * 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(); + Memory.Pointer ptr = Memory.getFreePointer(); _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value))); - Memory.loadFreePointer(ptr); + Memory.setFreePointer(ptr); } /** @@ -78,13 +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.saveFreePointer(); + 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.loadFreePointer(ptr); + Memory.setFreePointer(ptr); } /** diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol index f959a88bf7c..a26e6516e7b 100644 --- a/contracts/utils/Memory.sol +++ b/contracts/utils/Memory.sol @@ -7,7 +7,7 @@ library Memory { type Pointer is bytes32; /// @dev Returns a memory pointer to the current free memory pointer. - function saveFreePointer() internal pure returns (Pointer ptr) { + function getFreePointer() internal pure returns (Pointer ptr) { assembly ("memory-safe") { ptr := mload(0x40) } @@ -16,7 +16,7 @@ library Memory { /// @dev Sets the free memory pointer to a specific value. /// /// WARNING: Everything after the pointer may be overwritten. - function loadFreePointer(Pointer ptr) internal pure { + function setFreePointer(Pointer ptr) internal pure { assembly ("memory-safe") { mstore(0x40, ptr) } diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index 10bcc0fff76..2a80d1017d4 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -42,11 +42,11 @@ library SignatureChecker { bytes32 hash, bytes memory signature ) internal view returns (bool) { - Memory.Pointer ptr = Memory.saveFreePointer(); + Memory.Pointer ptr = Memory.getFreePointer(); bytes memory params = abi.encodeCall(IERC1271.isValidSignature, (hash, signature)); (bool success, bytes32 result) = LowLevelCall.staticcallReturnBytes32(signer, params); uint256 length = LowLevelCall.returnDataSize(); - Memory.loadFreePointer(ptr); + Memory.setFreePointer(ptr); return success && length >= 32 && result == bytes32(IERC1271.isValidSignature.selector); } diff --git a/test/utils/Memory.test.js b/test/utils/Memory.test.js new file mode 100644 index 00000000000..523bf886d26 --- /dev/null +++ b/test/utils/Memory.test.js @@ -0,0 +1,23 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +async function fixture() { + const mock = await ethers.deployContract('MemoryMock'); + + return { mock }; +} + +describe('Memory', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('free pointer', function () { + it('returns the new memory pointer after it has been altered', async function () { + const ptr = '0x00000000000000000000000000000000000000000000000000000000000000a0'; + await this.mock.$setFreePointer(ptr); + expect(await this.mock.$getFreePointer()).to.eq(ptr); + }); + }); +}); From cf31c38ae2ec5c3b8009ee6b608fe86f5327ef14 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 26 Jun 2024 16:55:09 -0600 Subject: [PATCH 12/20] Add missing check --- .changeset/dull-students-eat.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dull-students-eat.md diff --git a/.changeset/dull-students-eat.md b/.changeset/dull-students-eat.md new file mode 100644 index 00000000000..94c4fc21ef2 --- /dev/null +++ b/.changeset/dull-students-eat.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Memory`: Add library with utilities to manipulate memory From 7d4196bc0205470a9f9f8fd13b69d121efbcec63 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 26 Jun 2024 18:06:53 -0600 Subject: [PATCH 13/20] Add to stateless --- contracts/mocks/Stateless.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/mocks/Stateless.sol b/contracts/mocks/Stateless.sol index 6bf78babc7d..ba9984464e8 100644 --- a/contracts/mocks/Stateless.sol +++ b/contracts/mocks/Stateless.sol @@ -22,7 +22,9 @@ 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 {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 {Panic} from "../utils/Panic.sol"; From 0323b38cc0aa8b6b92d2be20a8328e79ed4c0590 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 27 Jun 2024 16:35:43 -0600 Subject: [PATCH 14/20] Try to fix tests --- contracts/mocks/MemoryMock.sol | 10 +++++----- test/utils/LowLevelCall.test.js | 10 +++++----- test/utils/Memory.test.js | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/mocks/MemoryMock.sol b/contracts/mocks/MemoryMock.sol index 34b7bd628ca..128f66c0c11 100644 --- a/contracts/mocks/MemoryMock.sol +++ b/contracts/mocks/MemoryMock.sol @@ -8,8 +8,8 @@ contract MemoryMock { bytes32 private _ptr; modifier _rememberPtr() { - assembly { - mstore(0x40, sload(_ptr.slot)) + assembly ("memory-safe") { + mstore(0x00, sload(_ptr.slot)) } _; } @@ -18,12 +18,12 @@ contract MemoryMock { _ptr = ptr; } - function $setFreePointer(bytes32 ptr) public { + function _setFreePointer(bytes32 ptr) public { _setPointer(ptr); - return Memory.setFreePointer(Memory.Pointer.wrap(ptr)); + return Memory.setFreePointer(ptr); } - function $getFreePointer() public view _rememberPtr returns (bytes32) { + function _getFreePointer() public view _rememberPtr returns (bytes32) { return Memory.Pointer.unwrap(Memory.getFreePointer()); } } diff --git a/test/utils/LowLevelCall.test.js b/test/utils/LowLevelCall.test.js index d83b485cba0..717ace7d607 100644 --- a/test/utils/LowLevelCall.test.js +++ b/test/utils/LowLevelCall.test.js @@ -18,7 +18,7 @@ describe('LowLevelCall', function () { }); describe('callRaw', function () { - beforeEach(async function () { + beforeEach(function () { this.call = this.target.interface.encodeFunctionData('mockFunction'); }); @@ -51,7 +51,7 @@ describe('LowLevelCall', function () { }); describe('callReturnBytes32', function () { - beforeEach(async function () { + beforeEach(function () { this.returnValue = ethers.id('returnDataBytes32'); this.call = this.target.interface.encodeFunctionData('mockFunctionWithArgsReturn', [ this.returnValue, @@ -93,7 +93,7 @@ describe('LowLevelCall', function () { }); describe('callReturnBytes32Pair', function () { - beforeEach(async function () { + beforeEach(function () { this.returnValue1 = ethers.id('returnDataBytes32Pair1'); this.returnValue2 = ethers.id('returnDataBytes32Pair2'); this.call = this.target.interface.encodeFunctionData('mockFunctionWithArgsReturn', [ @@ -152,7 +152,7 @@ describe('LowLevelCall', function () { }); describe('staticcallReturnBytes32', function () { - beforeEach(async function () { + beforeEach(function () { this.returnValue = ethers.id('returnDataBytes32'); }); @@ -173,7 +173,7 @@ describe('LowLevelCall', function () { }); describe('staticcallReturnBytes32Pair', function () { - beforeEach(async function () { + beforeEach(function () { this.returnValue1 = ethers.id('returnDataBytes32Pair1'); this.returnValue2 = ethers.id('returnDataBytes32Pair2'); }); diff --git a/test/utils/Memory.test.js b/test/utils/Memory.test.js index 523bf886d26..9ae373ce411 100644 --- a/test/utils/Memory.test.js +++ b/test/utils/Memory.test.js @@ -16,8 +16,8 @@ describe('Memory', function () { describe('free pointer', function () { it('returns the new memory pointer after it has been altered', async function () { const ptr = '0x00000000000000000000000000000000000000000000000000000000000000a0'; - await this.mock.$setFreePointer(ptr); - expect(await this.mock.$getFreePointer()).to.eq(ptr); + await this.mock._setFreePointer(ptr); + expect(await this.mock._getFreePointer()).to.eq(ptr); }); }); }); From 28889bd553d7577100f5d418a51ef06026134efb Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 27 Jun 2024 16:49:24 -0600 Subject: [PATCH 15/20] Rollback --- contracts/mocks/MemoryMock.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/mocks/MemoryMock.sol b/contracts/mocks/MemoryMock.sol index 128f66c0c11..6494581a4ec 100644 --- a/contracts/mocks/MemoryMock.sol +++ b/contracts/mocks/MemoryMock.sol @@ -20,7 +20,7 @@ contract MemoryMock { function _setFreePointer(bytes32 ptr) public { _setPointer(ptr); - return Memory.setFreePointer(ptr); + return Memory.setFreePointer(Memory.Pointer.wrap(ptr)); } function _getFreePointer() public view _rememberPtr returns (bytes32) { From 39edc84984ce9a163e62ad6252120a5a070fcab9 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 27 Jun 2024 17:25:57 -0600 Subject: [PATCH 16/20] FV for Memory --- contracts/mocks/MemoryMock.sol | 29 ----------------------------- contracts/utils/Memory.sol | 10 ++++++++++ test/utils/Memory.t.sol | 16 ++++++++++++++++ test/utils/Memory.test.js | 23 ----------------------- 4 files changed, 26 insertions(+), 52 deletions(-) delete mode 100644 contracts/mocks/MemoryMock.sol create mode 100644 test/utils/Memory.t.sol delete mode 100644 test/utils/Memory.test.js diff --git a/contracts/mocks/MemoryMock.sol b/contracts/mocks/MemoryMock.sol deleted file mode 100644 index 6494581a4ec..00000000000 --- a/contracts/mocks/MemoryMock.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {Memory} from "../utils/Memory.sol"; - -contract MemoryMock { - bytes32 private _ptr; - - modifier _rememberPtr() { - assembly ("memory-safe") { - mstore(0x00, sload(_ptr.slot)) - } - _; - } - - function _setPointer(bytes32 ptr) public { - _ptr = ptr; - } - - function _setFreePointer(bytes32 ptr) public { - _setPointer(ptr); - return Memory.setFreePointer(Memory.Pointer.wrap(ptr)); - } - - function _getFreePointer() public view _rememberPtr returns (bytes32) { - return Memory.Pointer.unwrap(Memory.getFreePointer()); - } -} diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol index a26e6516e7b..a0fc881e318 100644 --- a/contracts/utils/Memory.sol +++ b/contracts/utils/Memory.sol @@ -21,4 +21,14 @@ library Memory { 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); + } } diff --git a/test/utils/Memory.t.sol b/test/utils/Memory.t.sol new file mode 100644 index 00000000000..793b1b6f3fc --- /dev/null +++ b/test/utils/Memory.t.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Memory} from "@openzeppelin/contracts/utils/Memory.sol"; + +contract MemoryTest is Test { + using Memory for *; + + function testSymbolicGetSetFreePointer(uint256 ptr) public { + Memory.Pointer memoryPtr = Memory.asPointer(bytes32(ptr)); + Memory.setFreePointer(memoryPtr); + assertEq(Memory.getFreePointer().asBytes32(), memoryPtr.asBytes32()); + } +} diff --git a/test/utils/Memory.test.js b/test/utils/Memory.test.js deleted file mode 100644 index 9ae373ce411..00000000000 --- a/test/utils/Memory.test.js +++ /dev/null @@ -1,23 +0,0 @@ -const { ethers } = require('hardhat'); -const { expect } = require('chai'); -const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); - -async function fixture() { - const mock = await ethers.deployContract('MemoryMock'); - - return { mock }; -} - -describe('Memory', function () { - beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); - }); - - describe('free pointer', function () { - it('returns the new memory pointer after it has been altered', async function () { - const ptr = '0x00000000000000000000000000000000000000000000000000000000000000a0'; - await this.mock._setFreePointer(ptr); - expect(await this.mock._getFreePointer()).to.eq(ptr); - }); - }); -}); From a5918deaa77248e2e0383ba7ae2c1eadc2747598 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 27 Jun 2024 17:27:24 -0600 Subject: [PATCH 17/20] Simplify --- test/utils/Memory.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/utils/Memory.t.sol b/test/utils/Memory.t.sol index 793b1b6f3fc..9beb3096597 100644 --- a/test/utils/Memory.t.sol +++ b/test/utils/Memory.t.sol @@ -8,8 +8,8 @@ import {Memory} from "@openzeppelin/contracts/utils/Memory.sol"; contract MemoryTest is Test { using Memory for *; - function testSymbolicGetSetFreePointer(uint256 ptr) public { - Memory.Pointer memoryPtr = Memory.asPointer(bytes32(ptr)); + function testSymbolicGetSetFreePointer(bytes32 ptr) public { + Memory.Pointer memoryPtr = Memory.asPointer(ptr); Memory.setFreePointer(memoryPtr); assertEq(Memory.getFreePointer().asBytes32(), memoryPtr.asBytes32()); } From 29e0c7a097f897e86860dbc587ed4ffb7de1a78b Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 27 Jun 2024 17:29:58 -0600 Subject: [PATCH 18/20] Simplify --- test/utils/Memory.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/Memory.t.sol b/test/utils/Memory.t.sol index 9beb3096597..4cc60b88f9c 100644 --- a/test/utils/Memory.t.sol +++ b/test/utils/Memory.t.sol @@ -9,7 +9,7 @@ contract MemoryTest is Test { using Memory for *; function testSymbolicGetSetFreePointer(bytes32 ptr) public { - Memory.Pointer memoryPtr = Memory.asPointer(ptr); + Memory.Pointer memoryPtr = ptr.asPointer(); Memory.setFreePointer(memoryPtr); assertEq(Memory.getFreePointer().asBytes32(), memoryPtr.asBytes32()); } From 652df3f16014086621902545c3ab08b8da9b8e0f Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 27 Jun 2024 18:09:40 -0600 Subject: [PATCH 19/20] Fix coverage --- test/utils/cryptography/Memory.test.js | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 test/utils/cryptography/Memory.test.js diff --git a/test/utils/cryptography/Memory.test.js b/test/utils/cryptography/Memory.test.js new file mode 100644 index 00000000000..5698728dcfd --- /dev/null +++ b/test/utils/cryptography/Memory.test.js @@ -0,0 +1,41 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +async function fixture() { + const mock = await ethers.deployContract('$Memory'); + + return { mock }; +} + +describe('Memory', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('free pointer', function () { + it('sets memory pointer', async function () { + const ptr = '0x00000000000000000000000000000000000000000000000000000000000000a0'; + expect(await this.mock.$setFreePointer(ptr)).to.not.be.reverted; + }); + + it('gets memory pointer', async function () { + expect(await this.mock.$getFreePointer()).to.equal( + // Default pointer + '0x0000000000000000000000000000000000000000000000000000000000000080', + ); + }); + + it('asBytes32', async function () { + const ptr = ethers.toBeHex('0x1234', 32); + await this.mock.$setFreePointer(ptr); + expect(await this.mock.$asBytes32(ptr)).to.equal(ptr); + }); + + it('asPointer', async function () { + const ptr = ethers.toBeHex('0x1234', 32); + await this.mock.$setFreePointer(ptr); + expect(await this.mock.$asPointer(ptr)).to.equal(ptr); + }); + }); +}); From 60d33d415ba69415060be55167e7e36778b79cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 4 Sep 2024 11:35:51 -0600 Subject: [PATCH 20/20] Move memory tests --- test/utils/{cryptography => }/Memory.test.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/utils/{cryptography => }/Memory.test.js (100%) diff --git a/test/utils/cryptography/Memory.test.js b/test/utils/Memory.test.js similarity index 100% rename from test/utils/cryptography/Memory.test.js rename to test/utils/Memory.test.js