// SPDX-License-Identifier: MIT pragma solidity 0.8.15; import { Test } from "forge-std/Test.sol"; import { StdUtils } from "forge-std/StdUtils.sol"; import { Vm } from "forge-std/Vm.sol"; import { SafeCall } from "../../libraries/SafeCall.sol"; contract SafeCall_Succeeds_Invariants is Test { SafeCaller_Actor actor; function setUp() public { // Create a new safe caller actor. actor = new SafeCaller_Actor(vm, false); // Set the caller to this contract targetSender(address(this)); // Target the safe caller actor. targetContract(address(actor)); // Give the actor some ETH to work with vm.deal(address(actor), type(uint128).max); } /** * @custom:invariant If `callWithMinGas` performs a call, then it must always * provide at least the specified minimum gas limit to the subcontext. * * If the check for remaining gas in `SafeCall.callWithMinGas` passes, the * subcontext of the call below it must be provided at least `minGas` gas. */ function invariant_callWithMinGas_alwaysForwardsMinGas_succeeds() public { assertEq(actor.numCalls(), 0, "no failed calls allowed"); } function performSafeCallMinGas(address to, uint64 minGas) external payable { SafeCall.callWithMinGas(to, minGas, msg.value, hex""); } } contract SafeCall_Fails_Invariants is Test { SafeCaller_Actor actor; function setUp() public { // Create a new safe caller actor. actor = new SafeCaller_Actor(vm, true); // Set the caller to this contract targetSender(address(this)); // Target the safe caller actor. targetContract(address(actor)); // Give the actor some ETH to work with vm.deal(address(actor), type(uint128).max); } /** * @custom:invariant `callWithMinGas` reverts if there is not enough gas to pass * to the subcontext. * * If there is not enough gas in the callframe to ensure that `callWithMinGas` * can provide the specified minimum gas limit to the subcontext of the call, * then `callWithMinGas` must revert. */ function invariant_callWithMinGas_neverForwardsMinGas_reverts() public { assertEq(actor.numCalls(), 0, "no successful calls allowed"); } function performSafeCallMinGas(address to, uint64 minGas) external payable { SafeCall.callWithMinGas(to, minGas, msg.value, hex""); } } contract SafeCaller_Actor is StdUtils { bool internal immutable FAILS; Vm internal vm; uint256 public numCalls; constructor(Vm _vm, bool _fails) { vm = _vm; FAILS = _fails; } function performSafeCallMinGas( uint64 gas, uint64 minGas, address to, uint8 value ) external { // Only send to EOAs - we exclude the console as it has no code but reverts when called // with a selector that doesn't exist due to the foundry hook. vm.assume(to.code.length == 0 && to != 0x000000000000000000636F6e736F6c652e6c6f67); // Bound the minimum gas amount to [2500, type(uint48).max] minGas = uint64(bound(minGas, 2500, type(uint48).max)); if (FAILS) { // Bound the gas passed to [minGas, ((minGas * 64) / 63)] gas = uint64(bound(gas, minGas, (minGas * 64) / 63)); } else { // Bound the gas passed to // [((minGas * 64) / 63) + 40_000 + 1000, type(uint64).max] // The extra 1000 gas is to account for the gas used by the `SafeCall.call` call // itself. gas = uint64(bound(gas, ((minGas * 64) / 63) + 40_000 + 1000, type(uint64).max)); } vm.expectCallMinGas(to, value, minGas, hex""); bool success = SafeCall.call( msg.sender, gas, value, abi.encodeWithSelector( SafeCall_Succeeds_Invariants.performSafeCallMinGas.selector, to, minGas ) ); if (success && FAILS) numCalls++; if (!FAILS && !success) numCalls++; } }