From b0f01fa6434508d8e70e4e354e041f58bb6eb9e1 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Sun, 17 Nov 2024 20:34:41 +0000 Subject: [PATCH 1/4] Add LibCall --- src/utils/LibCall.sol | 218 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 src/utils/LibCall.sol diff --git a/src/utils/LibCall.sol b/src/utils/LibCall.sol new file mode 100644 index 0000000000..7bf7e08b61 --- /dev/null +++ b/src/utils/LibCall.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/// @notice Library for making calls. +/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/LibCall.sol) +/// @author Modified from ExcessivelySafeCall (https://github.com/nomad-xyz/ExcessivelySafeCall) +/// +/// @dev Note: +/// - The arguments of the functions may differ from the libraries. +/// Please read the functions carefully before use. +library LibCall { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The target of the call is not a contract. + error TargetIsNotContract(); + + /// @dev The data is too short to contain a function selector. + error DataTooShort(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONTRACT CALL OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + // These functions will revert if called on a non-contract + // (i.e. address without code). + // They will bubble up the revert if the call fails. + + /// @dev Makes a call to `target`, with `data` and `value`. + function contractCall(address target, uint256 value, bytes memory data) + internal + returns (bytes memory result) + { + /// @solidity memory-safe-assembly + assembly { + result := mload(0x40) + if iszero(call(gas(), target, value, add(data, 0x20), mload(data), 0x00, 0x00)) { + // Bubble up the revert if the call reverts. + returndatacopy(result, 0x00, returndatasize()) + revert(result, returndatasize()) + } + if iszero(returndatasize()) { + if iszero(extcodesize(target)) { + mstore(0x00, 0x5a836a5f) // `TargetIsNotContract()`. + revert(0x1c, 0x04) + } + } + mstore(result, returndatasize()) // Store the length. + let o := add(result, 0x20) + returndatacopy(o, 0x00, returndatasize()) // Copy the returndata. + mstore(0x40, add(o, returndatasize())) // Allocate the memory. + } + } + + /// @dev Makes a call to `target`, with `data`. + function contractCall(address target, bytes memory data) + internal + returns (bytes memory result) + { + /// @solidity memory-safe-assembly + assembly { + result := mload(0x40) + if iszero(call(gas(), target, 0, add(data, 0x20), mload(data), 0x00, 0x00)) { + // Bubble up the revert if the call reverts. + returndatacopy(result, 0x00, returndatasize()) + revert(result, returndatasize()) + } + if iszero(returndatasize()) { + if iszero(extcodesize(target)) { + mstore(0x00, 0x5a836a5f) // `TargetIsNotContract()`. + revert(0x1c, 0x04) + } + } + mstore(result, returndatasize()) // Store the length. + let o := add(result, 0x20) + returndatacopy(o, 0x00, returndatasize()) // Copy the returndata. + mstore(0x40, add(o, returndatasize())) // Allocate the memory. + } + } + + /// @dev Makes a static call to `target`, with `data`. + function contractStaticCall(address target, bytes memory data) + internal + view + returns (bytes memory result) + { + /// @solidity memory-safe-assembly + assembly { + result := mload(0x40) + if iszero(staticcall(gas(), target, add(data, 0x20), mload(data), 0x00, 0x00)) { + // Bubble up the revert if the call reverts. + returndatacopy(result, 0x00, returndatasize()) + revert(result, returndatasize()) + } + if iszero(returndatasize()) { + if iszero(extcodesize(target)) { + mstore(0x00, 0x5a836a5f) // `TargetIsNotContract()`. + revert(0x1c, 0x04) + } + } + mstore(result, returndatasize()) // Store the length. + let o := add(result, 0x20) + returndatacopy(o, 0x00, returndatasize()) // Copy the returndata. + mstore(0x40, add(o, returndatasize())) // Allocate the memory. + } + } + + /// @dev Makes a delegate call to `target`, with `data`. + function contractDelegateCall(address target, bytes memory data) + internal + returns (bytes memory result) + { + /// @solidity memory-safe-assembly + assembly { + result := mload(0x40) + if iszero(delegatecall(gas(), target, add(data, 0x20), mload(data), 0x00, 0x00)) { + // Bubble up the revert if the call reverts. + returndatacopy(result, 0x00, returndatasize()) + revert(result, returndatasize()) + } + if iszero(returndatasize()) { + if iszero(extcodesize(target)) { + mstore(0x00, 0x5a836a5f) // `TargetIsNotContract()`. + revert(0x1c, 0x04) + } + } + mstore(result, returndatasize()) // Store the length. + let o := add(result, 0x20) + returndatacopy(o, 0x00, returndatasize()) // Copy the returndata. + mstore(0x40, add(o, returndatasize())) // Allocate the memory. + } + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* TRY CALL OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + // These functions enable gas limited calls to be performed, + // with a cap on the number of return data bytes to be copied. + // The can be used to ensure that the calling contract will not + // run out-of-gas. + + /// @dev Makes a call to `target`, with `data` and `value`. + /// The call is given a gas limit of `gasStipend`, + /// and up to `maxCopy` bytes of return data can be copied. + function tryCall( + address target, + uint256 value, + uint256 gasStipend, + uint16 maxCopy, + bytes memory data + ) internal returns (bool success, bool exceededMaxCopy, bytes memory result) { + /// @solidity memory-safe-assembly + assembly { + result := mload(0x40) + success := call(gasStipend, target, value, add(data, 0x20), mload(data), 0x00, 0x00) + let n := returndatasize() + if gt(returndatasize(), and(0xffff, maxCopy)) { + n := and(0xffff, maxCopy) + exceededMaxCopy := 1 + } + mstore(result, n) // Store the length. + let o := add(result, 0x20) + returndatacopy(o, 0x00, n) // Copy the returndata. + mstore(0x40, add(o, n)) // Allocate the memory. + } + } + + /// @dev Makes a call to `target`, with `data`. + /// The call is given a gas limit of `gasStipend`, + /// and up to `maxCopy` bytes of return data can be copied. + function tryStaticCall(address target, uint256 gasStipend, uint16 maxCopy, bytes memory data) + internal + view + returns (bool success, bool exceededMaxCopy, bytes memory result) + { + /// @solidity memory-safe-assembly + assembly { + result := mload(0x40) + success := staticcall(gasStipend, target, add(data, 0x20), mload(data), 0x00, 0x00) + let n := returndatasize() + if gt(returndatasize(), and(0xffff, maxCopy)) { + n := and(0xffff, maxCopy) + exceededMaxCopy := 1 + } + mstore(result, n) // Store the length. + let o := add(result, 0x20) + returndatacopy(o, 0x00, n) // Copy the returndata. + mstore(0x40, add(o, n)) // Allocate the memory. + } + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* OTHER OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Bubbles up the revert. + function bubbleUpRevert(bytes memory revertReturnData) internal pure { + /// @solidity memory-safe-assembly + assembly { + revert(add(0x20, revertReturnData), mload(revertReturnData)) + } + } + + /// @dev In-place replaces the function selector of encoded contract call data. + function setSelector(bytes memory data, bytes4 newSelector) internal pure { + /// @solidity memory-safe-assembly + assembly { + if iszero(gt(mload(data), 0x03)) { + mstore(0x00, 0x0acec8bd) // `DataTooShort()`. + revert(0x1c, 0x04) + } + let o := add(data, 0x04) + mstore(o, or(shl(32, shr(32, mload(o))), shr(224, newSelector))) + } + } +} From abf8b9295b761de70d991b53398c2cfb6cb51d60 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Sun, 17 Nov 2024 20:57:49 +0000 Subject: [PATCH 2/4] Add test --- src/utils/LibCall.sol | 8 ++++---- test/LibCall.t.sol | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 test/LibCall.t.sol diff --git a/src/utils/LibCall.sol b/src/utils/LibCall.sol index 7bf7e08b61..a777c857c1 100644 --- a/src/utils/LibCall.sol +++ b/src/utils/LibCall.sol @@ -28,7 +28,7 @@ library LibCall { // They will bubble up the revert if the call fails. /// @dev Makes a call to `target`, with `data` and `value`. - function contractCall(address target, uint256 value, bytes memory data) + function callContract(address target, uint256 value, bytes memory data) internal returns (bytes memory result) { @@ -54,7 +54,7 @@ library LibCall { } /// @dev Makes a call to `target`, with `data`. - function contractCall(address target, bytes memory data) + function callContract(address target, bytes memory data) internal returns (bytes memory result) { @@ -80,7 +80,7 @@ library LibCall { } /// @dev Makes a static call to `target`, with `data`. - function contractStaticCall(address target, bytes memory data) + function staticCallContract(address target, bytes memory data) internal view returns (bytes memory result) @@ -107,7 +107,7 @@ library LibCall { } /// @dev Makes a delegate call to `target`, with `data`. - function contractDelegateCall(address target, bytes memory data) + function delegateCallContract(address target, bytes memory data) internal returns (bytes memory result) { diff --git a/test/LibCall.t.sol b/test/LibCall.t.sol new file mode 100644 index 0000000000..15b82dbe05 --- /dev/null +++ b/test/LibCall.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "./utils/SoladyTest.sol"; +import {LibCall} from "../src/utils/LibCall.sol"; + +contract HashSetter { + mapping(uint256 => bytes32) public hashes; + + function setHash(uint256 key, bytes memory data) public payable returns (bytes memory) { + hashes[key] = keccak256(data); + return abi.encodePacked(data, keccak256(data)); + } +} + +contract LibCallTest is SoladyTest { + HashSetter hashSetter; + + function setUp() public { + hashSetter = new HashSetter(); + } + + function testCallAndStaticCall(uint256 key, bytes memory data) public { + vm.deal(address(this), 1 ether); + uint256 value = _bound(_random(), 0, 1 ether); + bytes memory result = LibCall.callContract( + address(hashSetter), value, abi.encodeWithSignature("setHash(uint256,bytes)", key, data) + ); + assertEq(hashSetter.hashes(key), keccak256(data)); + assertEq(abi.decode(result, (bytes)), abi.encodePacked(data, keccak256(data))); + assertEq( + abi.decode( + LibCall.staticCallContract( + address(hashSetter), abi.encodeWithSignature("hashes(uint256)", key) + ), + (bytes32) + ), + keccak256(data) + ); + } +} From fe960510d124333130f5fcd85af82b72d84e8327 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Sun, 17 Nov 2024 21:10:59 +0000 Subject: [PATCH 3/4] Add setSelector test --- src/utils/LibCall.sol | 2 +- test/LibCall.t.sol | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/utils/LibCall.sol b/src/utils/LibCall.sol index a777c857c1..1e5f76cc3a 100644 --- a/src/utils/LibCall.sol +++ b/src/utils/LibCall.sol @@ -204,7 +204,7 @@ library LibCall { } /// @dev In-place replaces the function selector of encoded contract call data. - function setSelector(bytes memory data, bytes4 newSelector) internal pure { + function setSelector(bytes4 newSelector, bytes memory data) internal pure { /// @solidity memory-safe-assembly assembly { if iszero(gt(mload(data), 0x03)) { diff --git a/test/LibCall.t.sol b/test/LibCall.t.sol index 15b82dbe05..fb514b3d7b 100644 --- a/test/LibCall.t.sol +++ b/test/LibCall.t.sol @@ -38,4 +38,12 @@ contract LibCallTest is SoladyTest { keccak256(data) ); } + + function testSetSelector(bytes memory dataWithoutSelector) public { + bytes4 sel = bytes4(bytes32(_random())); + bytes memory data = abi.encodePacked(sel, dataWithoutSelector); + bytes memory dataPre = abi.encodePacked(bytes4(bytes32(_random())), dataWithoutSelector); + LibCall.setSelector(sel, dataPre); + assertEq(dataPre, data); + } } From 2703808472c0e6d82cc49a25cc53f05ac6ad823a Mon Sep 17 00:00:00 2001 From: Vectorized Date: Sun, 17 Nov 2024 21:16:07 +0000 Subject: [PATCH 4/4] T --- test/LibCall.t.sol | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/test/LibCall.t.sol b/test/LibCall.t.sol index fb514b3d7b..cca09f2d62 100644 --- a/test/LibCall.t.sol +++ b/test/LibCall.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.4; import "./utils/SoladyTest.sol"; import {LibCall} from "../src/utils/LibCall.sol"; +import {LibBytes} from "../src/utils/LibBytes.sol"; contract HashSetter { mapping(uint256 => bytes32) public hashes; @@ -41,9 +42,18 @@ contract LibCallTest is SoladyTest { function testSetSelector(bytes memory dataWithoutSelector) public { bytes4 sel = bytes4(bytes32(_random())); - bytes memory data = abi.encodePacked(sel, dataWithoutSelector); - bytes memory dataPre = abi.encodePacked(bytes4(bytes32(_random())), dataWithoutSelector); - LibCall.setSelector(sel, dataPre); - assertEq(dataPre, data); + bytes memory data = abi.encodePacked(bytes4(bytes32(_random())), dataWithoutSelector); + data = this.copyAndSetSelector(sel, data); + assertEq(data, abi.encodePacked(sel, dataWithoutSelector)); + if (_randomChance(2)) { + uint256 n = _random() % 4; + vm.expectRevert(LibCall.DataTooShort.selector); + this.copyAndSetSelector(sel, LibBytes.slice(data, 0, n)); + } + } + + function copyAndSetSelector(bytes4 sel, bytes memory data) public pure returns (bytes memory) { + LibCall.setSelector(sel, data); + return data; } }