The formula used in ````SafeCall.callWithMinGas()```` is wrong
The formula used in `SafeCall.callWithMinGas()` is not fully complying with EIP-150 and EIP-2929, the actual gas received by the sub-contract can be less than the required `_minGas`. Withdrawal transactions can be finalized with less than specified gas limit, may lead to loss of funds.\\n```\\nFile: contracts\\libraries\\SafeCall.sol\\n    function callWithMinGas(\\n        address _target,\\n        uint256 _minGas,\\n        uint256 _value,\\n        bytes memory _calldata\\n    ) internal returns (bool) {\\n        bool _success;\\n        assembly {\\n            // Assertion: gasleft() >= ((_minGas + 200) * 64) / 63\\n            //\\n            // Because EIP-150 ensures that, a maximum of 63/64ths of the remaining gas in the call\\n            // frame may be passed to a subcontext, we need to ensure that the gas will not be\\n            // truncated to hold this function's invariant: "If a call is performed by\\n            // `callWithMinGas`, it must receive at least the specified minimum gas limit." In\\n            // addition, exactly 51 gas is consumed between the below `GAS` opcode and the `CALL`\\n            // opcode, so it is factored in with some extra room for error.\\n            if lt(gas(), div(mul(64, add(_minGas, 200)), 63)) {\\n                // Store the "Error(string)" selector in scratch space.\\n                mstore(0, 0x08c379a0)\\n                // Store the pointer to the string length in scratch space.\\n                mstore(32, 32)\\n                // Store the string.\\n                //\\n                // SAFETY:\\n                // - We pad the beginning of the string with two zero bytes as well as the\\n                // length (24) to ensure that we override the free memory pointer at offset\\n                // 0x40. This is necessary because the free memory pointer is likely to\\n                // be greater than 1 byte when this function is called, but it is incredibly\\n                // unlikely that it will be greater than 3 bytes. As for the data within\\n                // 0x60, it is ensured that it is 0 due to 0x60 being the zero offset.\\n                // - It's fine to clobber the free memory pointer, we're reverting.\\n                mstore(88, 0x0000185361666543616c6c3a204e6f7420656e6f75676820676173)\\n\\n                // Revert with 'Error("SafeCall: Not enough gas")'\\n                revert(28, 100)\\n            }\\n\\n            // The call will be supplied at least (((_minGas + 200) * 64) / 63) - 49 gas due to the\\n            // above assertion. This ensures that, in all circumstances, the call will\\n            // receive at least the minimum amount of gas specified.\\n            // We can prove this property by solving the inequalities:\\n            // ((((_minGas + 200) * 64) / 63) - 49) >= _minGas\\n            // ((((_minGas + 200) * 64) / 63) - 51) * (63 / 64) >= _minGas\\n            // Both inequalities hold true for all possible values of `_minGas`.\\n            _success := call(\\n                gas(), // gas\\n                _target, // recipient\\n                _value, // ether value\\n                add(_calldata, 32), // inloc\\n                mload(_calldata), // inlen\\n                0x00, // outloc\\n                0x00 // outlen\\n            )\\n        }\\n        return _success;\\n    }\\n```\\n\\nThe current formula used in `SafeCall.callWithMinGas()` involves two issues.\\nFirstly, the `63/64` rule is not the whole story of EIP-150 for the `CALL` opcode, let's take a look at the implementation of EIP-150, a `base` gas is subtracted before applying `63/64` rule.\\n```\\nfunc callGas(isEip150 bool, availableGas, base uint64, callCost *uint256.Int) (uint64, error) {\\n if isEip150 {\\n  availableGas = availableGas - base\\n  gas := availableGas - availableGas/64\\n  // If the bit length exceeds 64 bit we know that the newly calculated "gas" for EIP150\\n  // is smaller than the requested amount. Therefore we return the new gas instead\\n  // of returning an error.\\n  if !callCost.IsUint64() || gas < callCost.Uint64() {\\n   return gas, nil\\n  }\\n }\\n if !callCost.IsUint64() {\\n  return 0, ErrGasUintOverflow\\n }\\n\\n return callCost.Uint64(), nil\\n}\\n```\\n\\nThe `base` gas is calculated in `gasCall()` of `gas_table.go`, which is subject to\\n```\\n(1) L370~L376: call to a new account\\n(2) L377~L379: call with non zero value\\n(3) L380~L383: memory expansion\\n```\\n\\nThe `(1)` and `(3)` are irrelevant in this case, but `(2)` should be taken into account.\\n```\\nFile: core\\vm\\gas_table.go\\nfunc gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {\\n var (\\n  gas            uint64\\n  transfersValue = !stack.Back(2).IsZero()\\n  address        = common.Address(stack.Back(1).Bytes20())\\n )\\n if evm.chainRules.IsEIP158 {\\n  if transfersValue && evm.StateDB.Empty(address) {\\n   gas += params.CallNewAccountGas\\n  }\\n } else if !evm.StateDB.Exist(address) {\\n  gas += params.CallNewAccountGas\\n }\\n if transfersValue {\\n  gas += params.CallValueTransferGas\\n }\\n memoryGas, err := memoryGasCost(mem, memorySize)\\n if err != nil {\\n  return 0, err\\n }\\n var overflow bool\\n if gas, overflow = math.SafeAdd(gas, memoryGas); overflow {\\n  return 0, ErrGasUintOverflow\\n }\\n\\n evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0))\\n if err != nil {\\n  return 0, err\\n }\\n if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow {\\n  return 0, ErrGasUintOverflow\\n }\\n return gas, nil\\n}\\n```\\n\\nThe `raw` extra gas for transferring value is\\n```\\nparams.CallValueTransferGas - params.CallStipend * 64 / 63 = 9000 - 2300 * 64 / 63 = 6664\\n```\\n\\nSecondly, EIP-2929 also affects the gas cost of `CALL` opcode.\\n```\\nFile: core\\vm\\operations_acl.go\\n gasCallEIP2929         = makeCallVariantGasCallEIP2929(gasCall)\\n\\nFile: core\\vm\\operations_acl.go\\nfunc makeCallVariantGasCallEIP2929(oldCalculator gasFunc) gasFunc {\\n return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {\\n  addr := common.Address(stack.Back(1).Bytes20())\\n  // Check slot presence in the access list\\n  warmAccess := evm.StateDB.AddressInAccessList(addr)\\n  // The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so\\n  // the cost to charge for cold access, if any, is Cold - Warm\\n  coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929\\n  if !warmAccess {\\n   evm.StateDB.AddAddressToAccessList(addr)\\n   // Charge the remaining difference here already, to correctly calculate available\\n   // gas for call\\n   if !contract.UseGas(coldCost) {\\n    return 0, ErrOutOfGas\\n   }\\n  }\\n  // Now call the old calculator, which takes into account\\n  // - create new account\\n  // - transfer value\\n  // - memory expansion\\n  // - 63/64ths rule\\n  gas, err := oldCalculator(evm, contract, stack, mem, memorySize)\\n  if warmAccess || err != nil {\\n   return gas, err\\n  }\\n  // In case of a cold access, we temporarily add the cold charge back, and also\\n  // add it to the returned gas. By adding it to the return, it will be charged\\n  // outside of this function, as part of the dynamic gas, and that will make it\\n  // also become correctly reported to tracers.\\n  contract.Gas += coldCost\\n  return gas + coldCost, nil\\n }\\n}\\n```\\n\\nHere is a test script to show the impact of the two aspects mentioned above\\n```\\n// SPDX-License-Identifier: MIT\\npragma solidity 0.8.15;\\n\\nimport "forge-std/Test.sol";\\nimport "forge-std/console.sol";\\n\\nlibrary SafeCall {\\n    function callWithMinGas(\\n        address _target,\\n        uint256 _minGas,\\n        uint256 _value,\\n        bytes memory _calldata\\n    ) internal returns (bool) {\\n        bool _success;\\n        uint256 gasSent;\\n        assembly {\\n            // Assertion: gasleft() >= ((_minGas + 200) * 64) / 63\\n            //\\n            // Because EIP-150 ensures that, a maximum of 63/64ths of the remaining gas in the call\\n            // frame may be passed to a subcontext, we need to ensure that the gas will not be\\n            // truncated to hold this function's invariant: "If a call is performed by\\n            // `callWithMinGas`, it must receive at least the specified minimum gas limit." In\\n            // addition, exactly 51 gas is consumed between the below `GAS` opcode and the `CALL`\\n            // opcode, so it is factored in with some extra room for error.\\n            if lt(gas(), div(mul(64, add(_minGas, 200)), 63)) {\\n                // Store the "Error(string)" selector in scratch space.\\n                mstore(0, 0x08c379a0)\\n                // Store the pointer to the string length in scratch space.\\n                mstore(32, 32)\\n                // Store the string.\\n                //\\n                // SAFETY:\\n                // - We pad the beginning of the string with two zero bytes as well as the\\n                // length (24) to ensure that we override the free memory pointer at offset\\n                // 0x40. This is necessary because the free memory pointer is likely to\\n                // be greater than 1 byte when this function is called, but it is incredibly\\n                // unlikely that it will be greater than 3 bytes. As for the data within\\n                // 0x60, it is ensured that it is 0 due to 0x60 being the zero offset.\\n                // - It's fine to clobber the free memory pointer, we're reverting.\\n                mstore(\\n                    88,\\n                    0x0000185361666543616c6c3a204e6f7420656e6f75676820676173\\n                )\\n\\n                // Revert with 'Error("SafeCall: Not enough gas")'\\n                revert(28, 100)\\n            }\\n\\n            // The call will be supplied at least (((_minGas + 200) * 64) / 63) - 49 gas due to the\\n            // above assertion. This ensures that, in all circumstances, the call will\\n            // receive at least the minimum amount of gas specified.\\n            // We can prove this property by solving the inequalities:\\n            // ((((_minGas + 200) * 64) / 63) - 49) >= _minGas\\n            // ((((_minGas + 200) * 64) / 63) - 51) * (63 / 64) >= _minGas\\n            // Both inequalities hold true for all possible values of `_minGas`.\\n            gasSent := gas() // @audit this operation costs 2 gas\\n            _success := call(\\n                gas(), // gas\\n                _target, // recipient\\n                _value, // ether value\\n                add(_calldata, 32), // inloc\\n                mload(_calldata), // inlen\\n                0x00, // outloc\\n                0x00 // outlen\\n            )\\n        }\\n        console.log("gasSent =", gasSent);\\n        return _success;\\n    }\\n}\\n\\ncontract Callee {\\n    fallback() external payable {\\n        uint256 gas = gasleft();\\n        console.log("gasReceived =", gas);\\n    }\\n}\\n\\ncontract Caller {\\n    function execute(\\n        address _target,\\n        uint256 _minGas,\\n        bytes memory _calldata\\n    ) external payable {\\n        SafeCall.callWithMinGas(_target, _minGas, msg.value, _calldata);\\n    }\\n}\\n\\ncontract TestCallWithMinGas is Test {\\n    address callee;\\n    Caller caller;\\n\\n    function setUp() public {\\n        callee = address(new Callee());\\n        caller = new Caller();\\n    }\\n\\n    function testCallWithMinGas() public {\\n        console.log("-------1st call------");\\n        caller.execute{gas: 64_855}(callee, 63_000, "");\\n\\n        console.log("\\n  -------2nd call------");\\n        caller.execute{gas: 64_855}(callee, 63_000, "");\\n\\n        console.log("\\n  -------3rd call------");\\n        caller.execute{gas: 62_555, value: 1}(callee, 63_000, "");\\n    }\\n}\\n```\\n\\nAnd the log would be\\n```\\nRunning 1 test for test/TestCallWithMinGas.sol:TestCallWithMinGas\\n[PASS] testCallWithMinGas() (gas: 36065)\\nLogs:\\n  -------1st call------\\n  gasReceived = 60582\\n  gasSent = 64200\\n\\n  -------2nd call------\\n  gasReceived = 63042\\n  gasSent = 64200\\n\\n  -------3rd call------\\n  gasReceived = 56483\\n  gasSent = 64200\\n```\\n\\nThe difference between `1st call` and `2nd call` is caused by EIP-2929, and the difference between `2nd call` and `3rd call` is caused by transferring value. We can see the actual received gas in the sub-contract is less than the 63,000 `_minGas` limit in both 1st and `3rd call`.
The migration logic may look like\\n```\\nif (_value == 0) {\\n     gasleft() >= ((_minGas + 200) * 64) / 63 + 2600\\n} else {\\n     gasleft() >= ((_minGas + 200) * 64) / 63 + 2600 + 6700\\n}\\n```\\n
`SafeCall.callWithMinGas()` is a key design to ensure withdrawal transactions will be executed with more gas than the limit specified by users. This issue breaks the specification. Finalizing withdrawal transactions with less than specified gas limit may fail unexpectedly due to out of gas, lead to loss of funds.
```\\nFile: contracts\\libraries\\SafeCall.sol\\n    function callWithMinGas(\\n        address _target,\\n        uint256 _minGas,\\n        uint256 _value,\\n        bytes memory _calldata\\n    ) internal returns (bool) {\\n        bool _success;\\n        assembly {\\n            // Assertion: gasleft() >= ((_minGas + 200) * 64) / 63\\n            //\\n            // Because EIP-150 ensures that, a maximum of 63/64ths of the remaining gas in the call\\n            // frame may be passed to a subcontext, we need to ensure that the gas will not be\\n            // truncated to hold this function's invariant: "If a call is performed by\\n            // `callWithMinGas`, it must receive at least the specified minimum gas limit." In\\n            // addition, exactly 51 gas is consumed between the below `GAS` opcode and the `CALL`\\n            // opcode, so it is factored in with some extra room for error.\\n            if lt(gas(), div(mul(64, add(_minGas, 200)), 63)) {\\n                // Store the "Error(string)" selector in scratch space.\\n                mstore(0, 0x08c379a0)\\n                // Store the pointer to the string length in scratch space.\\n                mstore(32, 32)\\n                // Store the string.\\n                //\\n                // SAFETY:\\n                // - We pad the beginning of the string with two zero bytes as well as the\\n                // length (24) to ensure that we override the free memory pointer at offset\\n                // 0x40. This is necessary because the free memory pointer is likely to\\n                // be greater than 1 byte when this function is called, but it is incredibly\\n                // unlikely that it will be greater than 3 bytes. As for the data within\\n                // 0x60, it is ensured that it is 0 due to 0x60 being the zero offset.\\n                // - It's fine to clobber the free memory pointer, we're reverting.\\n                mstore(88, 0x0000185361666543616c6c3a204e6f7420656e6f75676820676173)\\n\\n                // Revert with 'Error("SafeCall: Not enough gas")'\\n                revert(28, 100)\\n            }\\n\\n            // The call will be supplied at least (((_minGas + 200) * 64) / 63) - 49 gas due to the\\n            // above assertion. This ensures that, in all circumstances, the call will\\n            // receive at least the minimum amount of gas specified.\\n            // We can prove this property by solving the inequalities:\\n            // ((((_minGas + 200) * 64) / 63) - 49) >= _minGas\\n            // ((((_minGas + 200) * 64) / 63) - 51) * (63 / 64) >= _minGas\\n            // Both inequalities hold true for all possible values of `_minGas`.\\n            _success := call(\\n                gas(), // gas\\n                _target, // recipient\\n                _value, // ether value\\n                add(_calldata, 32), // inloc\\n                mload(_calldata), // inlen\\n                0x00, // outloc\\n                0x00 // outlen\\n            )\\n        }\\n        return _success;\\n    }\\n```\\n