-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add callWithExactGas lib * add fuzz test * dup code to make it more safe * add _callWithExactGasEvenIfTargetIsNoContract * make args order consistent * add callWithExactGasEvenIfTargetIsNoContract tests * improve tests * update snapshot * properly name fuzz test * fix comments * improve testing logic * return gas used in _callWithExactGasSafeReturnData (#11106) * fix function selector blacklist in fuzz test * Update shared.gas-snapshot * use constant revert reasons * fix solhint * fix ref * style nits and let all return success * fix solhint
- Loading branch information
Showing
7 changed files
with
707 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,7 @@ | |
"prepublishOnly": "pnpm compile && ./scripts/prepublish_generate_abi_folder", | ||
"publish-beta": "pnpm publish --tag beta", | ||
"publish-prod": "npm dist-tag add @chainlink/[email protected] latest", | ||
"solhint": "solhint --max-warnings 369 \"./src/v0.8/**/*.sol\"" | ||
"solhint": "solhint --max-warnings 376 \"./src/v0.8/**/*.sol\"" | ||
}, | ||
"files": [ | ||
"src/v0.8", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.0; | ||
|
||
/// @notice This library contains various callWithExactGas functions. All of them are | ||
/// safe from gas bomb attacks. | ||
/// @dev There is code duplication in this library. This is done to not leave the assembly | ||
/// the blocks. | ||
library CallWithExactGas { | ||
error NoContract(); | ||
error NoGasForCallExactCheck(); | ||
error NotEnoughGasForCall(); | ||
|
||
bytes4 internal constant NO_CONTRACT_SIG = 0x0c3b563c; | ||
bytes4 internal constant NO_GAS_FOR_CALL_EXACT_CHECK_SIG = 0xafa32a2c; | ||
bytes4 internal constant NOT_ENOUGH_GAS_FOR_CALL_SIG = 0x37c3be29; | ||
|
||
/// @notice calls target address with exactly gasAmount gas and payload as calldata. | ||
/// Accounts for gasForCallExactCheck gas that will be used by this function. Will revert | ||
/// if the target is not a contact. Will revert when there is not enough gas to call the | ||
/// target with gasAmount gas. | ||
/// @dev Ignores the return data, which makes it immune to gas bomb attacks. | ||
/// @return success whether the call succeeded | ||
function _callWithExactGas( | ||
bytes memory payload, | ||
address target, | ||
uint256 gasLimit, | ||
uint16 gasForCallExactCheck | ||
) internal returns (bool success) { | ||
assembly { | ||
// solidity calls check that a contract actually exists at the destination, so we do the same | ||
// Note we do this check prior to measuring gas so gasForCallExactCheck (our "cushion") | ||
// doesn't need to account for it. | ||
if iszero(extcodesize(target)) { | ||
mstore(0x0, NO_CONTRACT_SIG) | ||
revert(0x0, 0x4) | ||
} | ||
|
||
let g := gas() | ||
// Compute g -= gasForCallExactCheck and check for underflow | ||
// The gas actually passed to the callee is _min(gasAmount, 63//64*gas available). | ||
// We want to ensure that we revert if gasAmount > 63//64*gas available | ||
// as we do not want to provide them with less, however that check itself costs | ||
// gas. gasForCallExactCheck ensures we have at least enough gas to be able | ||
// to revert if gasAmount > 63//64*gas available. | ||
if lt(g, gasForCallExactCheck) { | ||
mstore(0x0, NO_GAS_FOR_CALL_EXACT_CHECK_SIG) | ||
revert(0x0, 0x4) | ||
} | ||
g := sub(g, gasForCallExactCheck) | ||
// if g - g//64 <= gasAmount, revert. We subtract g//64 because of EIP-150 | ||
if iszero(gt(sub(g, div(g, 64)), gasLimit)) { | ||
mstore(0x0, NOT_ENOUGH_GAS_FOR_CALL_SIG) | ||
revert(0x0, 0x4) | ||
} | ||
|
||
// call and return whether we succeeded. ignore return data | ||
// call(gas,addr,value,argsOffset,argsLength,retOffset,retLength) | ||
success := call(gasLimit, target, 0, add(payload, 0x20), mload(payload), 0x0, 0x0) | ||
} | ||
return success; | ||
} | ||
|
||
/// @notice calls target address with exactly gasAmount gas and payload as calldata. | ||
/// Account for gasForCallExactCheck gas that will be used by this function. Will revert | ||
/// if the target is not a contact. Will revert when there is not enough gas to call the | ||
/// target with gasAmount gas. | ||
/// @dev Caps the return data length, which makes it immune to gas bomb attacks. | ||
/// @dev Return data cap logic borrowed from | ||
/// https://github.com/nomad-xyz/ExcessivelySafeCall/blob/main/src/ExcessivelySafeCall.sol. | ||
/// @return success whether the call succeeded | ||
/// @return retData the return data from the call, capped at maxReturnBytes bytes | ||
/// @return gasUsed the gas used by the external call. Does not include the overhead of this function. | ||
function _callWithExactGasSafeReturnData( | ||
bytes memory payload, | ||
address target, | ||
uint256 gasLimit, | ||
uint16 gasForCallExactCheck, | ||
uint16 maxReturnBytes | ||
) internal returns (bool success, bytes memory retData, uint256 gasUsed) { | ||
// allocate retData memory ahead of time | ||
retData = new bytes(maxReturnBytes); | ||
|
||
assembly { | ||
// solidity calls check that a contract actually exists at the destination, so we do the same | ||
// Note we do this check prior to measuring gas so gasForCallExactCheck (our "cushion") | ||
// doesn't need to account for it. | ||
if iszero(extcodesize(target)) { | ||
mstore(0x0, NO_CONTRACT_SIG) | ||
revert(0x0, 0x4) | ||
} | ||
|
||
let g := gas() | ||
// Compute g -= gasForCallExactCheck and check for underflow | ||
// The gas actually passed to the callee is _min(gasAmount, 63//64*gas available). | ||
// We want to ensure that we revert if gasAmount > 63//64*gas available | ||
// as we do not want to provide them with less, however that check itself costs | ||
// gas. gasForCallExactCheck ensures we have at least enough gas to be able | ||
// to revert if gasAmount > 63//64*gas available. | ||
if lt(g, gasForCallExactCheck) { | ||
mstore(0x0, NO_GAS_FOR_CALL_EXACT_CHECK_SIG) | ||
revert(0x0, 0x4) | ||
} | ||
g := sub(g, gasForCallExactCheck) | ||
// if g - g//64 <= gasAmount, revert. We subtract g//64 because of EIP-150 | ||
if iszero(gt(sub(g, div(g, 64)), gasLimit)) { | ||
mstore(0x0, NOT_ENOUGH_GAS_FOR_CALL_SIG) | ||
revert(0x0, 0x4) | ||
} | ||
|
||
// We save the gas before the call so we can calculate how much gas the call used | ||
let gasBeforeCall := gas() | ||
// call and return whether we succeeded. ignore return data | ||
// call(gas,addr,value,argsOffset,argsLength,retOffset,retLength) | ||
success := call(gasLimit, target, 0, add(payload, 0x20), mload(payload), 0x0, 0x0) | ||
gasUsed := sub(gasBeforeCall, gas()) | ||
|
||
// limit our copy to maxReturnBytes bytes | ||
let toCopy := returndatasize() | ||
if gt(toCopy, maxReturnBytes) { | ||
toCopy := maxReturnBytes | ||
} | ||
// Store the length of the copied bytes | ||
mstore(retData, toCopy) | ||
// copy the bytes from retData[0:_toCopy] | ||
returndatacopy(add(retData, 0x20), 0x0, toCopy) | ||
} | ||
return (success, retData, gasUsed); | ||
} | ||
|
||
/// @notice Calls target address with exactly gasAmount gas and payload as calldata | ||
/// or reverts if at least gasLimit gas is not available. | ||
/// @dev Does not check if target is a contract. If it is not a contract, the low-level | ||
/// call will still be made and it will succeed. | ||
/// @dev Ignores the return data, which makes it immune to gas bomb attacks. | ||
/// @return success whether the call succeeded | ||
/// @return sufficientGas Whether there was enough gas to make the call | ||
function _callWithExactGasEvenIfTargetIsNoContract( | ||
bytes memory payload, | ||
address target, | ||
uint256 gasLimit, | ||
uint16 gasForCallExactCheck | ||
) internal returns (bool success, bool sufficientGas) { | ||
assembly { | ||
let g := gas() | ||
// Compute g -= CALL_WITH_EXACT_GAS_CUSHION and check for underflow. We | ||
// need the cushion since the logic following the above call to gas also | ||
// costs gas which we cannot account for exactly. So cushion is a | ||
// conservative upper bound for the cost of this logic. | ||
if iszero(lt(g, gasForCallExactCheck)) { | ||
g := sub(g, gasForCallExactCheck) | ||
// If g - g//64 <= gasAmount, we don't have enough gas. We subtract g//64 because of EIP-150. | ||
if gt(sub(g, div(g, 64)), gasLimit) { | ||
// Call and ignore success/return data. Note that we did not check | ||
// whether a contract actually exists at the target address. | ||
success := call(gasLimit, target, 0, add(payload, 0x20), mload(payload), 0x0, 0x0) | ||
sufficientGas := true | ||
} | ||
} | ||
} | ||
return (success, sufficientGas); | ||
} | ||
} |
Oops, something went wrong.