From ac43bb2e2f4cdba9116bf36dae07fdfdf905eefd Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 24 Dec 2024 15:57:26 +0300 Subject: [PATCH 01/11] Improve readability --- src/Facets/GasZipFacet.sol | 4 +++- src/Periphery/GasZipPeriphery.sol | 3 ++- src/Periphery/Permit2Proxy.sol | 6 ++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Facets/GasZipFacet.sol b/src/Facets/GasZipFacet.sol index 7af03cd92..b236121c7 100644 --- a/src/Facets/GasZipFacet.sol +++ b/src/Facets/GasZipFacet.sol @@ -18,6 +18,8 @@ import { InvalidCallData, CannotBridgeToSameNetwork, InvalidAmount } from "lifi/ contract GasZipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { using SafeTransferLib for address; + uint256 internal constant MAX_CHAINID_LENGTH_ALLOWED = 32; + error OnlyNativeAllowed(); error TooManyChainIds(); @@ -130,7 +132,7 @@ contract GasZipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { ) external pure returns (uint256 destinationChains) { uint256 length = _chainIds.length; - if (length > 32) revert TooManyChainIds(); + if (length > MAX_CHAINID_LENGTH_ALLOWED) revert TooManyChainIds(); for (uint256 i; i < length; ++i) { // Shift destinationChains left by 8 bits and add the next chainID diff --git a/src/Periphery/GasZipPeriphery.sol b/src/Periphery/GasZipPeriphery.sol index 1c4617597..e895ffc66 100644 --- a/src/Periphery/GasZipPeriphery.sol +++ b/src/Periphery/GasZipPeriphery.sol @@ -29,6 +29,7 @@ contract GasZipPeriphery is /// State /// IGasZip public immutable gasZipRouter; address public immutable liFiDEXAggregator; + uint256 internal constant MAX_CHAINID_LENGTH_ALLOWED = 32; /// Errors /// error TooManyChainIds(); @@ -110,7 +111,7 @@ contract GasZipPeriphery is ) external pure returns (uint256 destinationChains) { uint256 length = _chainIds.length; - if (length > 32) revert TooManyChainIds(); + if (length > MAX_CHAINID_LENGTH_ALLOWED) revert TooManyChainIds(); for (uint256 i; i < length; ++i) { // Shift destinationChains left by 8 bits and add the next chainID diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index ca82ae5bc..fcbedc8ba 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -201,7 +201,9 @@ contract Permit2Proxy is WithdrawablePeriphery { _assetId, _amount ); - bytes32 permit = _getTokenPermissionsHash(tokenPermissions); + bytes32 tokenPermissionsHash = _getTokenPermissionsHash( + tokenPermissions + ); // Witness Permit2Proxy.LiFiCall memory lifiCall = LiFiCall( @@ -213,7 +215,7 @@ contract Permit2Proxy is WithdrawablePeriphery { // PermitTransferWithWitness msgHash = _getPermitWitnessTransferFromHash( PERMIT2.DOMAIN_SEPARATOR(), - permit, + tokenPermissionsHash, address(this), _nonce, _deadline, From 80c68b14bfa3e50d95f5624c9d4a8a5105a9dc0c Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 24 Dec 2024 16:16:24 +0300 Subject: [PATCH 02/11] Small fixes --- src/Facets/GasZipFacet.sol | 4 ++-- src/Libraries/LibAsset.sol | 3 +-- src/Periphery/GasZipPeriphery.sol | 11 +---------- src/Periphery/ReceiverAcrossV3.sol | 3 +-- 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/Facets/GasZipFacet.sol b/src/Facets/GasZipFacet.sol index b236121c7..d0d27eb94 100644 --- a/src/Facets/GasZipFacet.sol +++ b/src/Facets/GasZipFacet.sol @@ -24,7 +24,7 @@ contract GasZipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { error TooManyChainIds(); /// State /// - address public constant NON_EVM_RECEIVER_IDENTIFIER = + address public constant NON_EVM_ADDRESS = 0x11f111f111f111F111f111f111F111f111f111F1; IGasZip public immutable gasZipRouter; @@ -106,7 +106,7 @@ contract GasZipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { // validate that receiverAddress matches with bridgeData in case of EVM target chain if ( - _bridgeData.receiver != NON_EVM_RECEIVER_IDENTIFIER && + _bridgeData.receiver != NON_EVM_ADDRESS && _gasZipData.receiverAddress != bytes32(uint256(uint160(_bridgeData.receiver))) ) revert InvalidCallData(); diff --git a/src/Libraries/LibAsset.sol b/src/Libraries/LibAsset.sol index f4e9d7d55..8ca9f3255 100644 --- a/src/Libraries/LibAsset.sol +++ b/src/Libraries/LibAsset.sol @@ -67,8 +67,7 @@ library LibAsset { } if (assetId.allowance(address(this), spender) < amount) { - SafeERC20.safeApprove(IERC20(assetId), spender, 0); - SafeERC20.safeApprove(IERC20(assetId), spender, MAX_UINT); + SafeERC20.forceApprove(IERC20(assetId), spender, MAX_UINT); } } diff --git a/src/Periphery/GasZipPeriphery.sol b/src/Periphery/GasZipPeriphery.sol index e895ffc66..8d0f99335 100644 --- a/src/Periphery/GasZipPeriphery.sol +++ b/src/Periphery/GasZipPeriphery.sol @@ -6,10 +6,7 @@ import { IGasZip } from "../Interfaces/IGasZip.sol"; import { LibSwap } from "../Libraries/LibSwap.sol"; import { LibAsset, IERC20 } from "../Libraries/LibAsset.sol"; import { LibUtil } from "../Libraries/LibUtil.sol"; -import { ReentrancyGuard } from "../Helpers/ReentrancyGuard.sol"; -import { SwapperV2 } from "../Helpers/SwapperV2.sol"; import { WithdrawablePeriphery } from "../Helpers/WithdrawablePeriphery.sol"; -import { Validatable } from "../Helpers/Validatable.sol"; import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; import { InvalidCallData } from "../Errors/GenericErrors.sol"; @@ -17,13 +14,7 @@ import { InvalidCallData } from "../Errors/GenericErrors.sol"; /// @author LI.FI (https://li.fi) /// @notice Provides functionality to swap ERC20 tokens to use the gas.zip protocol as a pre-bridge step (https://www.gas.zip/) /// @custom:version 1.0.0 -contract GasZipPeriphery is - ILiFi, - ReentrancyGuard, - SwapperV2, - Validatable, - WithdrawablePeriphery -{ +contract GasZipPeriphery is ILiFi, WithdrawablePeriphery { using SafeTransferLib for address; /// State /// diff --git a/src/Periphery/ReceiverAcrossV3.sol b/src/Periphery/ReceiverAcrossV3.sol index 42e3ef150..c29380585 100644 --- a/src/Periphery/ReceiverAcrossV3.sol +++ b/src/Periphery/ReceiverAcrossV3.sol @@ -105,8 +105,7 @@ contract ReceiverAcrossV3 is ILiFi, TransferrableOwnership { address payable receiver, uint256 amount ) private { - assetId.safeApprove(address(executor), 0); - assetId.safeApprove(address(executor), amount); + assetId.safeApproveWithRetry(address(executor), amount); try executor.swapAndCompleteBridgeTokens( _transactionId, From bdf16c01ac845ecc839d5f7f81a28e4c09f673b0 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 27 Dec 2024 14:08:23 +0300 Subject: [PATCH 03/11] Add frontrun resistance --- src/Periphery/Permit2Proxy.sol | 27 ++++++---- test/solidity/Periphery/Permit2Proxy.t.sol | 58 ++++++++++++++++++++++ 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index fcbedc8ba..dc099f757 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -82,15 +82,24 @@ contract Permit2Proxy is WithdrawablePeriphery { bytes calldata diamondCalldata ) public payable returns (bytes memory) { // call permit on token contract to register approval using signature - ERC20Permit(tokenAddress).permit( - msg.sender, // Ensure msg.sender is same wallet that signed permit - address(this), - amount, - deadline, - v, - r, - s - ); + try + ERC20Permit(tokenAddress).permit( + msg.sender, // Ensure msg.sender is same wallet that signed permit + address(this), + amount, + deadline, + v, + r, + s + ) + {} catch Error(string memory reason) { + if ( + IERC20(tokenAddress).allowance(msg.sender, address(this)) < + amount + ) { + revert(reason); + } + } // deposit assets LibAsset.transferFromERC20( diff --git a/test/solidity/Periphery/Permit2Proxy.t.sol b/test/solidity/Periphery/Permit2Proxy.t.sol index 7719f2f98..6f7fdb9ed 100644 --- a/test/solidity/Periphery/Permit2Proxy.t.sol +++ b/test/solidity/Periphery/Permit2Proxy.t.sol @@ -484,6 +484,64 @@ contract Permit2ProxyTest is TestBase { assertEq(permit2Proxy.nextNonceAfter(address(this), 1), 2); } + function test_eip2612_flow_is_resistant_to_frontrun_attack() + public + returns (TestDataEIP2612 memory) + { + // Record initial balance + uint256 initialBalance = ERC20(ADDRESS_USDC).balanceOf(PERMIT2_USER); + + vm.startPrank(PERMIT2_USER); + + bytes32 domainSeparator = ERC20Permit(ADDRESS_USDC).DOMAIN_SEPARATOR(); + + TestDataEIP2612 + memory testdata = _getTestDataEIP2612SignedByPERMIT2_USER( + ADDRESS_USDC, + domainSeparator, + block.timestamp + 1000 + ); + + vm.stopPrank(); + + vm.startPrank(address(0xA)); + // Attacker calls ERC20.permit directly + ERC20Permit(ADDRESS_USDC).permit( + PERMIT2_USER, //victim address + address(permit2Proxy), + defaultUSDCAmount, + testdata.deadline, + testdata.v, + testdata.r, + testdata.s + ); + vm.stopPrank(); + + vm.startPrank(PERMIT2_USER); + + // User's TX should succeed + permit2Proxy.callDiamondWithEIP2612Signature( + ADDRESS_USDC, + defaultUSDCAmount, + testdata.deadline, + testdata.v, + testdata.r, + testdata.s, + testdata.diamondCalldata + ); + vm.stopPrank(); + + // Verify tokens were moved from PERMIT2_USER + uint256 finalBalance = ERC20(ADDRESS_USDC).balanceOf(PERMIT2_USER); + assertEq( + finalBalance, + initialBalance - defaultUSDCAmount, + "User balance should have decreased by defaultUSDCAmount" + ); + + return testdata; + } + /// Helper Functions /// function _getPermit2TransferFromParamsSignedByPERMIT2_USER() From 32bdfa12c873462d2b91dd5afb91677ad800f43a Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 2 Jan 2025 07:58:32 +0300 Subject: [PATCH 04/11] Fix max approve --- src/Periphery/GasZipPeriphery.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Periphery/GasZipPeriphery.sol b/src/Periphery/GasZipPeriphery.sol index 8d0f99335..f0555d109 100644 --- a/src/Periphery/GasZipPeriphery.sol +++ b/src/Periphery/GasZipPeriphery.sol @@ -51,7 +51,7 @@ contract GasZipPeriphery is ILiFi, WithdrawablePeriphery { LibAsset.maxApproveERC20( IERC20(_swapData.sendingAssetId), liFiDEXAggregator, - type(uint256).max + _swapData.fromAmount ); // execute swap using LiFiDEXAggregator From 2e4563d6506ec2e174dbef5688260aac3337f8ac Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 3 Jan 2025 09:48:50 +0300 Subject: [PATCH 05/11] replace .call with safeTransferETH --- src/Facets/GenericSwapFacetV3.sol | 29 +++++++++------------------- src/Facets/RelayFacet.sol | 11 +++++------ src/Periphery/FeeCollector.sol | 8 ++------ src/Periphery/LiFiDEXAggregator.sol | 22 ++++----------------- src/Periphery/LiFuelFeeCollector.sol | 6 ++---- src/Periphery/Permit2Proxy.sol | 8 +++----- src/Periphery/Receiver.sol | 13 ++++--------- src/Periphery/ReceiverAcrossV3.sol | 3 +-- src/Periphery/ReceiverStargateV2.sol | 12 ++++-------- src/Periphery/RelayerCelerIM.sol | 13 +++---------- src/Periphery/TokenWrapper.sol | 6 ++---- 11 files changed, 39 insertions(+), 92 deletions(-) diff --git a/src/Facets/GenericSwapFacetV3.sol b/src/Facets/GenericSwapFacetV3.sol index 752fed19a..b43b9329d 100644 --- a/src/Facets/GenericSwapFacetV3.sol +++ b/src/Facets/GenericSwapFacetV3.sol @@ -111,9 +111,7 @@ contract GenericSwapFacetV3 is ILiFi { revert CumulativeSlippageTooHigh(_minAmountOut, amountReceived); // transfer funds to receiver - // solhint-disable-next-line avoid-low-level-calls - (bool success, ) = _receiver.call{ value: amountReceived }(""); - if (!success) revert NativeAssetTransferFailed(); + SafeTransferLib.safeTransferETH(_receiver, amountReceived); // emit events (both required for tracking) address sendingAssetId = _swapData.sendingAssetId; @@ -163,10 +161,9 @@ contract GenericSwapFacetV3 is ILiFi { ) revert ContractCallNotAllowed(); // execute swap + SafeTransferLib.safeTransferETH(callTo, msg.value); // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory res) = callTo.call{ value: msg.value }( - _swapData.callData - ); + (bool success, bytes memory res) = callTo.call(_swapData.callData); if (!success) { LibUtil.revertWith(res); } @@ -411,12 +408,10 @@ contract GenericSwapFacetV3 is ILiFi { if (LibAsset.isNativeAsset(sendingAssetId)) { // Native // execute the swap - (success, returnData) = currentSwap.callTo.call{ - value: currentSwap.fromAmount - }(currentSwap.callData); - if (!success) { - LibUtil.revertWith(returnData); - } + SafeTransferLib.safeTransferETH( + currentSwap.callTo, + currentSwap.fromAmount + ); // return any potential leftover sendingAsset tokens // but only for swaps, not for fee collections (otherwise the whole amount would be returned before the actual swap) @@ -521,11 +516,7 @@ contract GenericSwapFacetV3 is ILiFi { revert CumulativeSlippageTooHigh(_minAmountOut, amountReceived); // transfer funds to receiver - // solhint-disable-next-line avoid-low-level-calls - (bool success, ) = _receiver.call{ value: amountReceived }(""); - if (!success) { - revert NativeAssetTransferFailed(); - } + SafeTransferLib.safeTransferETH(_receiver, amountReceived); // emit event emit ILiFi.LiFiGenericSwapCompleted( @@ -563,9 +554,7 @@ contract GenericSwapFacetV3 is ILiFi { uint256 nativeBalance = address(this).balance; if (nativeBalance > 0) { - // solhint-disable-next-line avoid-low-level-calls - (bool success, ) = receiver.call{ value: nativeBalance }(""); - if (!success) revert NativeAssetTransferFailed(); + SafeTransferLib.safeTransferETH(receiver, nativeBalance); } } } diff --git a/src/Facets/RelayFacet.sol b/src/Facets/RelayFacet.sol index 27acfc735..cd4ee6153 100644 --- a/src/Facets/RelayFacet.sol +++ b/src/Facets/RelayFacet.sol @@ -9,6 +9,7 @@ import { ReentrancyGuard } from "../Helpers/ReentrancyGuard.sol"; import { SwapperV2 } from "../Helpers/SwapperV2.sol"; import { Validatable } from "../Helpers/Validatable.sol"; import { ECDSA } from "solady/utils/ECDSA.sol"; +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; /// @title Relay Facet /// @author LI.FI (https://li.fi) @@ -171,12 +172,10 @@ contract RelayFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { // Native // Send Native to relayReceiver along with requestId as extra data - (bool success, bytes memory reason) = relayReceiver.call{ - value: _bridgeData.minAmount - }(abi.encode(_relayData.requestId)); - if (!success) { - revert(LibUtil.getRevertMsg(reason)); - } + SafeTransferLib.safeTransferETH( + relayReceiver, + _bridgeData.minAmount + ); } else { // ERC20 diff --git a/src/Periphery/FeeCollector.sol b/src/Periphery/FeeCollector.sol index df18fe45a..5aaefb322 100644 --- a/src/Periphery/FeeCollector.sol +++ b/src/Periphery/FeeCollector.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.17; import { LibAsset } from "../Libraries/LibAsset.sol"; import { TransferrableOwnership } from "../Helpers/TransferrableOwnership.sol"; +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; /// @title Fee Collector /// @author LI.FI (https://li.fi) @@ -84,12 +85,7 @@ contract FeeCollector is TransferrableOwnership { // Prevent extra native token from being locked in the contract if (remaining > 0) { // solhint-disable-next-line avoid-low-level-calls - (bool success, ) = payable(msg.sender).call{ value: remaining }( - "" - ); - if (!success) { - revert TransferFailure(); - } + SafeTransferLib.safeTransferETH(msg.sender, remaining); } emit FeesCollected( LibAsset.NULL_ADDRESS, diff --git a/src/Periphery/LiFiDEXAggregator.sol b/src/Periphery/LiFiDEXAggregator.sol index da3dd3541..01faec4e2 100644 --- a/src/Periphery/LiFiDEXAggregator.sol +++ b/src/Periphery/LiFiDEXAggregator.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.17; import { SafeERC20, IERC20, IERC20Permit } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; address constant NATIVE_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; address constant IMPOSSIBLE_POOL_ADDRESS = 0x0000000000000000000000000000000000000001; @@ -130,14 +131,7 @@ contract LiFiDEXAggregator is Ownable { address to, bytes memory route ) external payable lock returns (uint256 amountOut) { - (bool success, bytes memory returnBytes) = transferValueTo.call{ - value: amountValueTransfer - }(""); - if (!success) { - assembly { - revert(add(32, returnBytes), mload(returnBytes)) - } - } + SafeTransferLib.safeTransferETH(transferValueTo, amountValueTransfer); return processRouteInternal( tokenIn, @@ -370,11 +364,7 @@ contract LiFiDEXAggregator is Ownable { ); IWETH(tokenIn).withdraw(amountIn); } - (bool success, ) = payable(to).call{ value: amountIn }(""); - require( - success, - "RouteProcessor.wrapNative: Native token transfer failed" - ); + SafeTransferLib.safeTransferETH(to, amountIn); } } @@ -750,11 +740,7 @@ contract LiFiDEXAggregator is Ownable { if (to != address(this)) { if (tokenOut == NATIVE_ADDRESS) { - (bool success, ) = payable(to).call{ value: amountOut }(""); - require( - success, - "RouteProcessor.swapCurve: Native token transfer failed" - ); + SafeTransferLib.safeTransferETH(to, amountOut); } else { IERC20(tokenOut).safeTransfer(to, amountOut); } diff --git a/src/Periphery/LiFuelFeeCollector.sol b/src/Periphery/LiFuelFeeCollector.sol index e33ccb7ce..2dfd0af15 100644 --- a/src/Periphery/LiFuelFeeCollector.sol +++ b/src/Periphery/LiFuelFeeCollector.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.17; import { LibAsset } from "../Libraries/LibAsset.sol"; import { TransferrableOwnership } from "../Helpers/TransferrableOwnership.sol"; +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; /// @title LiFuelFeeCollector /// @author LI.FI (https://li.fi) @@ -65,10 +66,7 @@ contract LiFuelFeeCollector is TransferrableOwnership { ); uint256 amountMinusFees = msg.value - feeAmount; if (amountMinusFees > 0) { - (bool success, ) = msg.sender.call{ value: amountMinusFees }(""); - if (!success) { - revert TransferFailure(); - } + SafeTransferLib.safeTransferETH(msg.sender, amountMinusFees); } } diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index ca82ae5bc..479438e37 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -6,6 +6,7 @@ import { LibAsset, IERC20 } from "lifi/Libraries/LibAsset.sol"; import { PermitHash } from "permit2/libraries/PermitHash.sol"; import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; import { WithdrawablePeriphery } from "lifi/Helpers/WithdrawablePeriphery.sol"; +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; /// @title Permit2Proxy /// @author LI.FI (https://li.fi) @@ -269,12 +270,9 @@ contract Permit2Proxy is WithdrawablePeriphery { bytes memory diamondCalldata ) internal returns (bytes memory) { // call diamond with provided calldata + SafeTransferLib.safeTransferETH(LIFI_DIAMOND, msg.value); // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory data) = LIFI_DIAMOND.call{ - value: msg.value - }(diamondCalldata); - // throw error to make sure tx reverts if low-level call was - // unsuccessful + (bool success, bytes memory data) = LIFI_DIAMOND.call(diamondCalldata); if (!success) { revert CallToDiamondFailed(data); } diff --git a/src/Periphery/Receiver.sol b/src/Periphery/Receiver.sol index faabba7b8..ba82a54f0 100644 --- a/src/Periphery/Receiver.sol +++ b/src/Periphery/Receiver.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.17; import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; import { ReentrancyGuard } from "../Helpers/ReentrancyGuard.sol"; import { LibSwap } from "../Libraries/LibSwap.sol"; import { LibAsset } from "../Libraries/LibAsset.sol"; @@ -177,9 +178,7 @@ contract Receiver is ILiFi, ReentrancyGuard, TransferrableOwnership { uint256 amount ) external onlyOwner { if (LibAsset.isNativeAsset(assetId)) { - // solhint-disable-next-line avoid-low-level-calls - (bool success, ) = receiver.call{ value: amount }(""); - if (!success) revert ExternalCallFailed(); + SafeTransferLib.safeTransferETH(receiver, amount); } else { IERC20(assetId).safeTransfer(receiver, amount); } @@ -209,9 +208,7 @@ contract Receiver is ILiFi, ReentrancyGuard, TransferrableOwnership { uint256 cacheGasLeft = gasleft(); if (reserveRecoverGas && cacheGasLeft < _recoverGas) { // case 1a: not enough gas left to execute calls - // solhint-disable-next-line avoid-low-level-calls - (bool success, ) = receiver.call{ value: amount }(""); - if (!success) revert ExternalCallFailed(); + SafeTransferLib.safeTransferETH(receiver, amount); emit LiFiTransferRecovered( _transactionId, @@ -231,9 +228,7 @@ contract Receiver is ILiFi, ReentrancyGuard, TransferrableOwnership { gas: cacheGasLeft - _recoverGas }(_transactionId, _swapData, assetId, receiver) {} catch { - // solhint-disable-next-line avoid-low-level-calls - (bool success, ) = receiver.call{ value: amount }(""); - if (!success) revert ExternalCallFailed(); + SafeTransferLib.safeTransferETH(receiver, amount); emit LiFiTransferRecovered( _transactionId, diff --git a/src/Periphery/ReceiverAcrossV3.sol b/src/Periphery/ReceiverAcrossV3.sol index 42e3ef150..fe9efa3a8 100644 --- a/src/Periphery/ReceiverAcrossV3.sol +++ b/src/Periphery/ReceiverAcrossV3.sol @@ -82,8 +82,7 @@ contract ReceiverAcrossV3 is ILiFi, TransferrableOwnership { ) external onlyOwner { if (LibAsset.isNativeAsset(assetId)) { // solhint-disable-next-line avoid-low-level-calls - (bool success, ) = receiver.call{ value: amount }(""); - if (!success) revert ExternalCallFailed(); + SafeTransferLib.safeTransferETH(receiver, amount); } else { assetId.safeTransfer(receiver, amount); } diff --git a/src/Periphery/ReceiverStargateV2.sol b/src/Periphery/ReceiverStargateV2.sol index fad75ee0d..8b813ece0 100644 --- a/src/Periphery/ReceiverStargateV2.sol +++ b/src/Periphery/ReceiverStargateV2.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.17; import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { LibSwap } from "../Libraries/LibSwap.sol"; +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; import { LibAsset } from "../Libraries/LibAsset.sol"; import { OFTComposeMsgCodec } from "../Libraries/OFTComposeMsgCodec.sol"; import { ILiFi } from "../Interfaces/ILiFi.sol"; @@ -125,8 +126,7 @@ contract ReceiverStargateV2 is ) external onlyOwner { if (LibAsset.isNativeAsset(assetId)) { // solhint-disable-next-line avoid-low-level-calls - (bool success, ) = receiver.call{ value: amount }(""); - if (!success) revert ExternalCallFailed(); + SafeTransferLib.safeTransferETH(receiver, amount); } else { IERC20(assetId).safeTransfer(receiver, amount); } @@ -153,9 +153,7 @@ contract ReceiverStargateV2 is // case 1: native asset if (cacheGasLeft < recoverGas) { // case 1a: not enough gas left to execute calls - // solhint-disable-next-line avoid-low-level-calls - (bool success, ) = receiver.call{ value: amount }(""); - if (!success) revert ExternalCallFailed(); + SafeTransferLib.safeTransferETH(receiver, amount); emit LiFiTransferRecovered( _transactionId, @@ -175,9 +173,7 @@ contract ReceiverStargateV2 is gas: cacheGasLeft - recoverGas }(_transactionId, _swapData, assetId, receiver) {} catch { - // solhint-disable-next-line avoid-low-level-calls - (bool success, ) = receiver.call{ value: amount }(""); - if (!success) revert ExternalCallFailed(); + SafeTransferLib.safeTransferETH(receiver, amount); emit LiFiTransferRecovered( _transactionId, diff --git a/src/Periphery/RelayerCelerIM.sol b/src/Periphery/RelayerCelerIM.sol index e1d6935e7..5f67c455c 100644 --- a/src/Periphery/RelayerCelerIM.sol +++ b/src/Periphery/RelayerCelerIM.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.17; import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; import { LibSwap } from "../Libraries/LibSwap.sol"; import { ContractCallNotAllowed, ExternalCallFailed, InvalidConfig, UnAuthorized, WithdrawFailed } from "../Errors/GenericErrors.sol"; import { LibAsset } from "../Libraries/LibAsset.sol"; @@ -368,11 +369,7 @@ contract RelayerCelerIM is ILiFi, TransferrableOwnership { { success = true; } catch { - // solhint-disable-next-line avoid-low-level-calls - (bool fundsSent, ) = refundAddress.call{ value: amount }(""); - if (!fundsSent) { - revert ExternalCallFailed(); - } + SafeTransferLib.safeTransferETH(refundAddress, amount); } } else { IERC20 token = IERC20(assetId); @@ -415,11 +412,7 @@ contract RelayerCelerIM is ILiFi, TransferrableOwnership { uint256 amount ) external onlyOwner { if (LibAsset.isNativeAsset(assetId)) { - // solhint-disable-next-line avoid-low-level-calls - (bool success, ) = receiver.call{ value: amount }(""); - if (!success) { - revert WithdrawFailed(); - } + SafeTransferLib.safeTransferETH(receiver, amount); } else { IERC20(assetId).safeTransfer(receiver, amount); } diff --git a/src/Periphery/TokenWrapper.sol b/src/Periphery/TokenWrapper.sol index 03f409437..53a7b1709 100644 --- a/src/Periphery/TokenWrapper.sol +++ b/src/Periphery/TokenWrapper.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.17; import { LibAsset } from "../Libraries/LibAsset.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; /// External wrapper interface interface IWrapper { @@ -46,10 +47,7 @@ contract TokenWrapper { uint256 wad = IERC20(wrappedToken).balanceOf(msg.sender); IERC20(wrappedToken).transferFrom(msg.sender, address(this), wad); IWrapper(wrappedToken).withdraw(wad); - (bool success, ) = payable(msg.sender).call{ value: wad }(""); - if (!success) { - revert WithdrawFailure(); - } + SafeTransferLib.safeTransferETH(msg.sender, wad); } // Needs to be able to receive native on `withdraw` From 19743252b0e12ed90bd522c9d34a43796ce452ca Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 3 Jan 2025 09:58:20 +0300 Subject: [PATCH 06/11] bump versions --- src/Facets/GasZipFacet.sol | 2 +- src/Libraries/LibAsset.sol | 2 +- src/Periphery/GasZipPeriphery.sol | 2 +- src/Periphery/Permit2Proxy.sol | 2 +- src/Periphery/ReceiverAcrossV3.sol | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Facets/GasZipFacet.sol b/src/Facets/GasZipFacet.sol index d0d27eb94..e275409a4 100644 --- a/src/Facets/GasZipFacet.sol +++ b/src/Facets/GasZipFacet.sol @@ -14,7 +14,7 @@ import { InvalidCallData, CannotBridgeToSameNetwork, InvalidAmount } from "lifi/ /// @title GasZipFacet /// @author LI.FI (https://li.fi) /// @notice Provides functionality to swap ERC20 tokens to native and deposit them to the gas.zip protocol (https://www.gas.zip/) -/// @custom:version 2.0.0 +/// @custom:version 2.0.1 contract GasZipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { using SafeTransferLib for address; diff --git a/src/Libraries/LibAsset.sol b/src/Libraries/LibAsset.sol index 8ca9f3255..e4462eb52 100644 --- a/src/Libraries/LibAsset.sol +++ b/src/Libraries/LibAsset.sol @@ -6,7 +6,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { LibSwap } from "./LibSwap.sol"; /// @title LibAsset -/// @custom:version 1.0.1 +/// @custom:version 1.0.2 /// @notice This library contains helpers for dealing with onchain transfers /// of assets, including accounting for the native asset `assetId` /// conventions and any noncompliant ERC20 transfers diff --git a/src/Periphery/GasZipPeriphery.sol b/src/Periphery/GasZipPeriphery.sol index f0555d109..8196a53d9 100644 --- a/src/Periphery/GasZipPeriphery.sol +++ b/src/Periphery/GasZipPeriphery.sol @@ -13,7 +13,7 @@ import { InvalidCallData } from "../Errors/GenericErrors.sol"; /// @title GasZipPeriphery /// @author LI.FI (https://li.fi) /// @notice Provides functionality to swap ERC20 tokens to use the gas.zip protocol as a pre-bridge step (https://www.gas.zip/) -/// @custom:version 1.0.0 +/// @custom:version 1.0.1 contract GasZipPeriphery is ILiFi, WithdrawablePeriphery { using SafeTransferLib for address; diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index dc099f757..c4eb6892d 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -11,7 +11,7 @@ import { WithdrawablePeriphery } from "lifi/Helpers/WithdrawablePeriphery.sol"; /// @author LI.FI (https://li.fi) /// @notice Proxy contract allowing gasless calls via Permit2 as well as making /// token approvals via ERC20 Permit (EIP-2612) to our diamond contract -/// @custom:version 1.0.0 +/// @custom:version 1.0.1 contract Permit2Proxy is WithdrawablePeriphery { /// Storage /// diff --git a/src/Periphery/ReceiverAcrossV3.sol b/src/Periphery/ReceiverAcrossV3.sol index c29380585..c77e07b75 100644 --- a/src/Periphery/ReceiverAcrossV3.sol +++ b/src/Periphery/ReceiverAcrossV3.sol @@ -12,7 +12,7 @@ import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; /// @title ReceiverAcrossV3 /// @author LI.FI (https://li.fi) /// @notice Arbitrary execution contract used for cross-chain swaps and message passing via AcrossV3 -/// @custom:version 1.0.1 +/// @custom:version 1.0.2 contract ReceiverAcrossV3 is ILiFi, TransferrableOwnership { using SafeTransferLib for address; From 090d7e2ba0b527665852a03c14c63ce582c629b6 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 3 Jan 2025 10:01:58 +0300 Subject: [PATCH 07/11] bump versions --- src/Facets/GenericSwapFacetV3.sol | 2 +- src/Facets/RelayFacet.sol | 2 +- src/Periphery/FeeCollector.sol | 2 +- src/Periphery/LiFiDEXAggregator.sol | 2 +- src/Periphery/LiFuelFeeCollector.sol | 2 +- src/Periphery/Permit2Proxy.sol | 2 +- src/Periphery/Receiver.sol | 2 +- src/Periphery/ReceiverAcrossV3.sol | 2 +- src/Periphery/ReceiverStargateV2.sol | 2 +- src/Periphery/RelayerCelerIM.sol | 2 +- src/Periphery/TokenWrapper.sol | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Facets/GenericSwapFacetV3.sol b/src/Facets/GenericSwapFacetV3.sol index b43b9329d..13c9b068b 100644 --- a/src/Facets/GenericSwapFacetV3.sol +++ b/src/Facets/GenericSwapFacetV3.sol @@ -13,7 +13,7 @@ import { ERC20, SafeTransferLib } from "solmate/utils/SafeTransferLib.sol"; /// @author LI.FI (https://li.fi) /// @notice Provides gas-optimized functionality for fee collection and for swapping through any APPROVED DEX /// @dev Can only execute calldata for APPROVED function selectors -/// @custom:version 1.0.1 +/// @custom:version 1.0.2 contract GenericSwapFacetV3 is ILiFi { using SafeTransferLib for ERC20; diff --git a/src/Facets/RelayFacet.sol b/src/Facets/RelayFacet.sol index cd4ee6153..2a8db0253 100644 --- a/src/Facets/RelayFacet.sol +++ b/src/Facets/RelayFacet.sol @@ -14,7 +14,7 @@ import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; /// @title Relay Facet /// @author LI.FI (https://li.fi) /// @notice Provides functionality for bridging through Relay Protocol -/// @custom:version 1.0.0 +/// @custom:version 1.0.1 contract RelayFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { // Receiver for native transfers address public immutable relayReceiver; diff --git a/src/Periphery/FeeCollector.sol b/src/Periphery/FeeCollector.sol index 5aaefb322..bb5c8e3d3 100644 --- a/src/Periphery/FeeCollector.sol +++ b/src/Periphery/FeeCollector.sol @@ -8,7 +8,7 @@ import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; /// @title Fee Collector /// @author LI.FI (https://li.fi) /// @notice Provides functionality for collecting integrator fees -/// @custom:version 1.0.0 +/// @custom:version 1.0.1 contract FeeCollector is TransferrableOwnership { /// State /// diff --git a/src/Periphery/LiFiDEXAggregator.sol b/src/Periphery/LiFiDEXAggregator.sol index 01faec4e2..0bd818fe6 100644 --- a/src/Periphery/LiFiDEXAggregator.sol +++ b/src/Periphery/LiFiDEXAggregator.sol @@ -23,7 +23,7 @@ uint160 constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970 /// @title LiFi DEX Aggregator /// @author Ilya Lyalin (contract copied from: https://github.com/sushiswap/sushiswap/blob/c8c80dec821003eb72eb77c7e0446ddde8ca9e1e/protocols/route-processor/contracts/RouteProcessor4.sol) /// @notice Processes calldata to swap using various DEXs -/// @custom:version 1.5.0 +/// @custom:version 1.5.1 contract LiFiDEXAggregator is Ownable { using SafeERC20 for IERC20; using Approve for IERC20; diff --git a/src/Periphery/LiFuelFeeCollector.sol b/src/Periphery/LiFuelFeeCollector.sol index 2dfd0af15..bd097ae44 100644 --- a/src/Periphery/LiFuelFeeCollector.sol +++ b/src/Periphery/LiFuelFeeCollector.sol @@ -8,7 +8,7 @@ import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; /// @title LiFuelFeeCollector /// @author LI.FI (https://li.fi) /// @notice Provides functionality for collecting fees for LiFuel -/// @custom:version 1.0.1 +/// @custom:version 1.0.2 contract LiFuelFeeCollector is TransferrableOwnership { /// Errors /// error TransferFailure(); diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index 2df1aef41..5b52c9569 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -12,7 +12,7 @@ import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; /// @author LI.FI (https://li.fi) /// @notice Proxy contract allowing gasless calls via Permit2 as well as making /// token approvals via ERC20 Permit (EIP-2612) to our diamond contract -/// @custom:version 1.0.1 +/// @custom:version 1.0.2 contract Permit2Proxy is WithdrawablePeriphery { /// Storage /// diff --git a/src/Periphery/Receiver.sol b/src/Periphery/Receiver.sol index ba82a54f0..86a269c7e 100644 --- a/src/Periphery/Receiver.sol +++ b/src/Periphery/Receiver.sol @@ -14,7 +14,7 @@ import { ExternalCallFailed, UnAuthorized } from "../Errors/GenericErrors.sol"; /// @title Receiver /// @author LI.FI (https://li.fi) /// @notice Arbitrary execution contract used for cross-chain swaps and message passing -/// @custom:version 2.0.2 +/// @custom:version 2.0.3 contract Receiver is ILiFi, ReentrancyGuard, TransferrableOwnership { using SafeERC20 for IERC20; diff --git a/src/Periphery/ReceiverAcrossV3.sol b/src/Periphery/ReceiverAcrossV3.sol index 44745bec1..3c3f5b7db 100644 --- a/src/Periphery/ReceiverAcrossV3.sol +++ b/src/Periphery/ReceiverAcrossV3.sol @@ -12,7 +12,7 @@ import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; /// @title ReceiverAcrossV3 /// @author LI.FI (https://li.fi) /// @notice Arbitrary execution contract used for cross-chain swaps and message passing via AcrossV3 -/// @custom:version 1.0.2 +/// @custom:version 1.0.3 contract ReceiverAcrossV3 is ILiFi, TransferrableOwnership { using SafeTransferLib for address; diff --git a/src/Periphery/ReceiverStargateV2.sol b/src/Periphery/ReceiverStargateV2.sol index 8b813ece0..c75d9d9bd 100644 --- a/src/Periphery/ReceiverStargateV2.sol +++ b/src/Periphery/ReceiverStargateV2.sol @@ -35,7 +35,7 @@ interface ILayerZeroComposer { /// @title ReceiverStargateV2 /// @author LI.FI (https://li.fi) /// @notice Arbitrary execution contract used for cross-chain swaps and message passing via Stargate V2 -/// @custom:version 1.0.0 +/// @custom:version 1.0.1 contract ReceiverStargateV2 is ILiFi, TransferrableOwnership, diff --git a/src/Periphery/RelayerCelerIM.sol b/src/Periphery/RelayerCelerIM.sol index 5f67c455c..820051d55 100644 --- a/src/Periphery/RelayerCelerIM.sol +++ b/src/Periphery/RelayerCelerIM.sol @@ -19,7 +19,7 @@ import { IBridge as ICBridge } from "celer-network/contracts/interfaces/IBridge. /// @title RelayerCelerIM /// @author LI.FI (https://li.fi) /// @notice Relayer contract for CelerIM that forwards calls and handles refunds on src side and acts receiver on dest -/// @custom:version 2.0.0 +/// @custom:version 2.0.1 contract RelayerCelerIM is ILiFi, TransferrableOwnership { using SafeERC20 for IERC20; diff --git a/src/Periphery/TokenWrapper.sol b/src/Periphery/TokenWrapper.sol index 53a7b1709..ac72718dd 100644 --- a/src/Periphery/TokenWrapper.sol +++ b/src/Periphery/TokenWrapper.sol @@ -15,7 +15,7 @@ interface IWrapper { /// @title TokenWrapper /// @author LI.FI (https://li.fi) /// @notice Provides functionality for wrapping and unwrapping tokens -/// @custom:version 1.0.0 +/// @custom:version 1.0.1 contract TokenWrapper { uint256 private constant MAX_INT = 2 ** 256 - 1; address public wrappedToken; From a8bde0f655cdb958a2b01a8240682b9e33361fbe Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 3 Jan 2025 11:55:35 +0300 Subject: [PATCH 08/11] Revert unintentional change to RelayFacet --- src/Facets/RelayFacet.sol | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Facets/RelayFacet.sol b/src/Facets/RelayFacet.sol index 2a8db0253..27acfc735 100644 --- a/src/Facets/RelayFacet.sol +++ b/src/Facets/RelayFacet.sol @@ -9,12 +9,11 @@ import { ReentrancyGuard } from "../Helpers/ReentrancyGuard.sol"; import { SwapperV2 } from "../Helpers/SwapperV2.sol"; import { Validatable } from "../Helpers/Validatable.sol"; import { ECDSA } from "solady/utils/ECDSA.sol"; -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; /// @title Relay Facet /// @author LI.FI (https://li.fi) /// @notice Provides functionality for bridging through Relay Protocol -/// @custom:version 1.0.1 +/// @custom:version 1.0.0 contract RelayFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { // Receiver for native transfers address public immutable relayReceiver; @@ -172,10 +171,12 @@ contract RelayFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { // Native // Send Native to relayReceiver along with requestId as extra data - SafeTransferLib.safeTransferETH( - relayReceiver, - _bridgeData.minAmount - ); + (bool success, bytes memory reason) = relayReceiver.call{ + value: _bridgeData.minAmount + }(abi.encode(_relayData.requestId)); + if (!success) { + revert(LibUtil.getRevertMsg(reason)); + } } else { // ERC20 From ff98810898e8f4e5d2f164333200eb6f8b4a6c4b Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 6 Jan 2025 06:31:51 +0300 Subject: [PATCH 09/11] revert changes to GenericSwapFacetV3 --- src/Facets/GenericSwapFacetV3.sol | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/Facets/GenericSwapFacetV3.sol b/src/Facets/GenericSwapFacetV3.sol index 13c9b068b..752fed19a 100644 --- a/src/Facets/GenericSwapFacetV3.sol +++ b/src/Facets/GenericSwapFacetV3.sol @@ -13,7 +13,7 @@ import { ERC20, SafeTransferLib } from "solmate/utils/SafeTransferLib.sol"; /// @author LI.FI (https://li.fi) /// @notice Provides gas-optimized functionality for fee collection and for swapping through any APPROVED DEX /// @dev Can only execute calldata for APPROVED function selectors -/// @custom:version 1.0.2 +/// @custom:version 1.0.1 contract GenericSwapFacetV3 is ILiFi { using SafeTransferLib for ERC20; @@ -111,7 +111,9 @@ contract GenericSwapFacetV3 is ILiFi { revert CumulativeSlippageTooHigh(_minAmountOut, amountReceived); // transfer funds to receiver - SafeTransferLib.safeTransferETH(_receiver, amountReceived); + // solhint-disable-next-line avoid-low-level-calls + (bool success, ) = _receiver.call{ value: amountReceived }(""); + if (!success) revert NativeAssetTransferFailed(); // emit events (both required for tracking) address sendingAssetId = _swapData.sendingAssetId; @@ -161,9 +163,10 @@ contract GenericSwapFacetV3 is ILiFi { ) revert ContractCallNotAllowed(); // execute swap - SafeTransferLib.safeTransferETH(callTo, msg.value); // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory res) = callTo.call(_swapData.callData); + (bool success, bytes memory res) = callTo.call{ value: msg.value }( + _swapData.callData + ); if (!success) { LibUtil.revertWith(res); } @@ -408,10 +411,12 @@ contract GenericSwapFacetV3 is ILiFi { if (LibAsset.isNativeAsset(sendingAssetId)) { // Native // execute the swap - SafeTransferLib.safeTransferETH( - currentSwap.callTo, - currentSwap.fromAmount - ); + (success, returnData) = currentSwap.callTo.call{ + value: currentSwap.fromAmount + }(currentSwap.callData); + if (!success) { + LibUtil.revertWith(returnData); + } // return any potential leftover sendingAsset tokens // but only for swaps, not for fee collections (otherwise the whole amount would be returned before the actual swap) @@ -516,7 +521,11 @@ contract GenericSwapFacetV3 is ILiFi { revert CumulativeSlippageTooHigh(_minAmountOut, amountReceived); // transfer funds to receiver - SafeTransferLib.safeTransferETH(_receiver, amountReceived); + // solhint-disable-next-line avoid-low-level-calls + (bool success, ) = _receiver.call{ value: amountReceived }(""); + if (!success) { + revert NativeAssetTransferFailed(); + } // emit event emit ILiFi.LiFiGenericSwapCompleted( @@ -554,7 +563,9 @@ contract GenericSwapFacetV3 is ILiFi { uint256 nativeBalance = address(this).balance; if (nativeBalance > 0) { - SafeTransferLib.safeTransferETH(receiver, nativeBalance); + // solhint-disable-next-line avoid-low-level-calls + (bool success, ) = receiver.call{ value: nativeBalance }(""); + if (!success) revert NativeAssetTransferFailed(); } } } From e4f27b0c4f572e5c64ca132dc2e496a3fe6c7c1d Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 6 Jan 2025 07:31:54 +0300 Subject: [PATCH 10/11] fix tests --- test/solidity/Periphery/ReceiverAcrossV3.t.sol | 4 ++-- test/solidity/Periphery/ReceiverStargateV2.t.sol | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/solidity/Periphery/ReceiverAcrossV3.t.sol b/test/solidity/Periphery/ReceiverAcrossV3.t.sol index d726c3259..97d67f284 100644 --- a/test/solidity/Periphery/ReceiverAcrossV3.t.sol +++ b/test/solidity/Periphery/ReceiverAcrossV3.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; import { Test, TestBase, Vm, LiFiDiamond, DSTest, ILiFi, LibSwap, LibAllowList, console, InvalidAmount, ERC20, UniswapV2Router02 } from "../utils/TestBase.sol"; -import { OnlyContractOwner, UnAuthorized, ExternalCallFailed } from "src/Errors/GenericErrors.sol"; +import { OnlyContractOwner, UnAuthorized } from "src/Errors/GenericErrors.sol"; import { ReceiverAcrossV3 } from "lifi/Periphery/ReceiverAcrossV3.sol"; import { stdJson } from "forge-std/Script.sol"; @@ -88,7 +88,7 @@ contract ReceiverAcrossV3Test is TestBase { vm.startPrank(USER_DIAMOND_OWNER); - vm.expectRevert(ExternalCallFailed.selector); + vm.expectRevert(abi.encodeWithSignature("ETHTransferFailed()")); receiver.pullToken( address(0), diff --git a/test/solidity/Periphery/ReceiverStargateV2.t.sol b/test/solidity/Periphery/ReceiverStargateV2.t.sol index 6b0e51ef2..485bd5ace 100644 --- a/test/solidity/Periphery/ReceiverStargateV2.t.sol +++ b/test/solidity/Periphery/ReceiverStargateV2.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; import { Test, TestBase, Vm, LiFiDiamond, DSTest, ILiFi, LibSwap, LibAllowList, console, InvalidAmount, ERC20, UniswapV2Router02 } from "../utils/TestBase.sol"; -import { OnlyContractOwner, UnAuthorized, ExternalCallFailed } from "src/Errors/GenericErrors.sol"; +import { OnlyContractOwner, UnAuthorized } from "src/Errors/GenericErrors.sol"; import { ReceiverStargateV2 } from "lifi/Periphery/ReceiverStargateV2.sol"; import { stdJson } from "forge-std/Script.sol"; @@ -95,7 +95,7 @@ contract ReceiverStargateV2Test is TestBase { vm.startPrank(USER_DIAMOND_OWNER); - vm.expectRevert(ExternalCallFailed.selector); + vm.expectRevert(abi.encodeWithSignature("ETHTransferFailed()")); receiver.pullToken( address(0), From e59844ebf501d57e11e349d44634a88ac19c9933 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 10 Jan 2025 08:44:01 +0300 Subject: [PATCH 11/11] add audit report and update log --- audit/auditLog.json | 56 ++++++++++++++++++ .../2025.01.09_SafeTransferETHUpdate.pdf | Bin 0 -> 31843 bytes 2 files changed, 56 insertions(+) create mode 100644 audit/reports/2025.01.09_SafeTransferETHUpdate.pdf diff --git a/audit/auditLog.json b/audit/auditLog.json index 56bde21e9..4dca8c05d 100644 --- a/audit/auditLog.json +++ b/audit/auditLog.json @@ -69,6 +69,7 @@ "auditorGitHandle": "sujithsomraaj", "auditReportPath": "./audit/reports/2024.12.05_DeBridgeDlnFacet(v1.0.0).pdf", "auditCommitHash": "d72cb05510acc5bb794bde05af550ba2ef85d06d" + }, "audit20241206": { "auditCompletedOn": "06.12.2024", "auditedBy": "Sujith Somraaj (individual security researcher)", @@ -82,6 +83,13 @@ "auditorGitHandle": "sujithsomraaj", "auditReportPath": "./audit/reports/2025.01.06_AcrossFacetV3(v1.1.0).pdf", "auditCommitHash": "fb4c7a4e02c5fd214d78b8cdc5d4a03fa0b06ab9" + }, + "audit20250109_3": { + "auditCompletedOn": "09.01.2025", + "auditedBy": "Sujith Somraaj (individual security researcher)", + "auditorGitHandle": "sujithsomraaj", + "auditReportPath": "./audit/reports/2025.01.09_SafeTransferETHUpdate.pdf", + "auditCommitHash": "e4f27b0c4f572e5c64ca132dc2e496a3fe6c7c1d" } }, "auditedContracts": { @@ -114,14 +122,25 @@ "audit20241105" ] }, + "FeeCollector": { + "1.0.1": [ + "audit20250109_3" + ] + }, "GasZipFacet": { "2.0.0": [ "audit20241107" + ], + "2.0.1": [ + "audit20250109_3" ] }, "GasZipPeriphery": { "1.0.0": [ "audit20241107" + ], + "1.0.1": [ + "audit20250109_3" ] }, "IGasZip": { @@ -132,16 +151,35 @@ "LibAsset": { "1.0.1": [ "audit20241202" + ], + "1.0.2": [ + "audit20250109_3" ] }, "LiFiDEXAggregator": { "1.5.0": [ "audit20241203" + ], + "1.5.1": [ + "audit20250109_3" + ] + }, + "LiFuelFeeCollector": { + "1.0.2": [ + "audit20250109_3" ] }, "Permit2Proxy": { "1.0.0": [ "audit20241122" + ], + "1.0.2": [ + "audit20250109_3" + ] + }, + "Receiver": { + "2.0.3": [ + "audit20250109_3" ] }, "ReceiverAcrossV3": { @@ -150,6 +188,19 @@ ], "1.0.1": [ "audit20241206" + ], + "1.0.3": [ + "audit20250109_3" + ] + }, + "ReceiverStargateV2": { + "1.0.1": [ + "audit20250109_3" + ] + }, + "RelayerCelerIM": { + "2.0.1": [ + "audit20250109_3" ] }, "RelayFacet": { @@ -162,6 +213,11 @@ "audit20240814" ] }, + "TokenWrapper": { + "1.0.1": [ + "audit20250109_3" + ] + }, "WithdrawablePeriphery": { "1.0.0": [ "audit20241014" diff --git a/audit/reports/2025.01.09_SafeTransferETHUpdate.pdf b/audit/reports/2025.01.09_SafeTransferETHUpdate.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c02dca184811c4d0f2f57099a3f17fdabf3b5563 GIT binary patch literal 31843 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