LI.FI contest - hake's results

Bridge & DEX Aggregation.

General Information

Platform: Code4rena

Start Date: 24/03/2022

Pot Size: $75,000 USDC

Total HM: 15

Participants: 59

Period: 7 days

Judge: gzeon

Id: 103

League: ETH

LI.FI

Findings Distribution

Researcher Performance

Rank: 6/59

Findings: 6

Award: $4,591.30

🌟 Selected for report: 3

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: 0xDjango

Also found by: hake, kirk-baird, rayn, shenwilly

Labels

bug
duplicate
3 (High Risk)

Awards

2219.3568 USDC - $2,219.36

External Links

Lines of code

LibSwap.swap swapTokensGeneric

Vulnerability details

Impact

Users could input incongruent values for _lifiData and _swapData leading to a swap no being processed correctly and users not getting any of the expected _lifiData.receivingAssetId.

It can also damage reputation because LiFi could use this to scam their users through a front end by changing inputs deliberatly.

Proof of Concept

  • User calls swapTokensGeneric but accidentally inputs two different addresses for _lifiData.receivingAssetId(DAI) and _swapData.receivingAssetId(ADA).
function swapTokensGeneric(LiFiData memory _lifiData, LibSwap.SwapData[] calldata _swapData)
  • LibSwap.swap uses _swapData.receivingAssetId(ADA) to make the swap, however GenericSwapFacet.swapTokensGeneric.postSwapBalance uses _lifiData.receivingAssetId(DAI) to calculate the amount to be transferred based on its balance before the swap.
uint256 toAmount = LibAsset.getOwnBalance(_swapData.receivingAssetId);

vs

uint256 postSwapBalance = LibAsset.getOwnBalance(_lifiData.receivingAssetId) - receivingAssetIdBalance;
  • The balance of DAI has not changed on LiFi, so the user will not get any, however the swap for ADA was successful and was sent to LiFis contract instead.
LibAsset.transferAsset(_lifiData.receivingAssetId, payable(msg.sender), postSwapBalance);

Tools Used

Implement a check to make sure _lifiData and _swapData values are the same.

#0 - H3xept

2022-04-11T10:22:22Z

Duplicate of #137

#1 - gzeoneth

2022-04-16T17:46:02Z

Agree with sponsor this does not pose a functional risk. Changing to Low/QA.

#2 - gzeoneth

2022-04-16T18:52:58Z

Consider with warden's QA Report #68

#3 - gzeoneth

2022-05-11T17:16:10Z

This should be a duplicate of https://github.com/code-423n4/2022-03-lifinance-findings/issues/75 instead, I made a mistake during the deduplication process.

Findings Information

🌟 Selected for report: hake

Also found by: Kenshin, Ruhum, VAD37, WatchPug, csanuragjain, hickuphh3, hyh, kirk-baird, obront, pmerkleplant, rayn, shw, tintin, wuwe1

Labels

bug
2 (Med Risk)
sponsor acknowledged

Awards

77.3842 USDC - $77.38

External Links

Lines of code

LibSwap.swap GenericSwapFacet.sol

Vulnerability details

Impact

Remaining or unaccounted ERC20 balance could be freely taken through swapTokensGenerics and swap.

Proof of Concept

function swap(bytes32 transactionId, SwapData calldata _swapData) internal { uint256 fromAmount = _swapData.fromAmount; uint256 toAmount = LibAsset.getOwnBalance(_swapData.receivingAssetId); address fromAssetId = _swapData.sendingAssetId; if (!LibAsset.isNativeAsset(fromAssetId) && LibAsset.getOwnBalance(fromAssetId) < fromAmount) { LibAsset.transferFromERC20(fromAssetId, msg.sender, address(this), fromAmount); } if (!LibAsset.isNativeAsset(fromAssetId)) { LibAsset.approveERC20(IERC20(fromAssetId), _swapData.approveTo, fromAmount); } // solhint-disable-next-line avoid-low-level-calls (bool success, bytes memory res) = _swapData.callTo.call{ value: msg.value }(_swapData.callData); if (!success) { string memory reason = LibUtil.getRevertMsg(res); revert(reason); } toAmount = LibAsset.getOwnBalance(_swapData.receivingAssetId) - toAmount;

Given:

  • There has been a deposit to LiFi of a non-native ERC20 that makes LibAsset.getOwnBalance(fromAssetId) a desirable amount.

  • Attacker calls swapTokensGeneric with a _swapData.fromAmount value just below LibAsset.getOwnBalance(fromAssetId).

  • First if statement in swap is skipped (no funds are tranferred to LiFis contract).

if (!LibAsset.isNativeAsset(fromAssetId) && LibAsset.getOwnBalance(fromAssetId) < fromAmount) { LibAsset.transferFromERC20(fromAssetId, msg.sender, address(this), fromAmount); }
  • Swap happens and increases LibAsset.getOwnBalance(_lifiData.receivingAssetId)
  • Difference of LiFis balance of the receiving token before and after swap is calculated using postSwapBalance and transfered to attacker.

Tools Used

Ensure funds are always subtracted from users account in swap, even if LiFi has enough balance to do the swap.

#0 - H3xept

2022-04-12T08:37:45Z

We are aware that the contract allows users to use latent funds, although we disagree on it being an issue as no funds (ERC20 or native) should ever lay in the contract. To make sure that no value is ever kept by the diamond, we now provide refunds for outstanding user value (after bridges/swaps).

#1 - H3xept

2022-04-12T08:40:04Z

We are aware that the contract allows users to use latent funds, although we disagree on it being an issue as no funds (ERC20 or native) should ever lay in the contract. To make sure that no value is ever kept by the diamond, we now provide refunds for outstanding user value (after bridges/swaps).

#2 - gzeoneth

2022-04-16T16:09:24Z

Keeping this and all duplicates as Med Risk. There can be fund leftover in the contract under normal operation, for example this tx. In fact, ~$300 worth of token is left in the LI.Fi smart contract on ETH mainnet 0x5a9fd7c39a6c488e715437d7b1f3c823d5596ed1 as of block 14597316. I don't think this is High Risk because the max amount lost is no more than allowed slippage, which can be loss to MEV too.

#3 - ezynda3

2022-05-03T10:09:16Z

This has been fixed in the most recent version of src/Helpers/Swapper.sol which sweeps any latent funds back to the user's wallet.

Findings Information

🌟 Selected for report: kirk-baird

Also found by: ACai, hake, rayn

Labels

bug
duplicate
2 (Med Risk)

Awards

924.732 USDC - $924.73

External Links

Lines of code

LibSwap.swap: L29-46 LibAsset.approveERC20

Vulnerability details

Impact

If a whitelisted dex gets compromised or becomes ill intentioned it can withdraw all funds in LiFi via approveERC20 and/or a reentrancy attack through the LibSwap.swap function. By the time the dex is blacklisted it might be too late, as there are no inbuilt boundaries or mechanisms to mitigate an attack. This means that LiFi is only as secure as the dexs it allows/approves.

Proof of Concept

LibSwap.swap: L29-46

The call in line 42 (bool success, bytes memory res) = _swapData.callTo.call{ value: msg.value }(_swapData.callData); has no reentrancy guard an could be used as a reentrancy vector if any approved callTo turned malicious.

LibAsset.approveERC20

The LibAsset.approveERC20 function allows any approveTo address to withdraw as much as they want from LiFi with no limits or boundaries as we can see in the last three lines of code below.

function approveERC20( IERC20 assetId, address spender, uint256 amount ) internal { if (isNativeAsset(address(assetId))) return; uint256 allowance = assetId.allowance(address(this), spender); if (allowance < amount) { if (allowance > 0) SafeERC20.safeApprove(IERC20(assetId), spender, 0); SafeERC20.safeApprove(IERC20(assetId), spender, MAX_INT); } }

Tools Used

Manual Review

Implement a reentrancy guard at swapAndStartBridgeTokensViaNXTP and similar functions. I recommend using ReentrancyGuard from OpenZeppelin. Set allowance dynamically equal to the amount to be transfered/swapped instead of an unlimited amount. Implement checks for making sure _swapData and _nxtpData are congruent with each other.

#0 - H3xept

2022-04-11T12:25:57Z

Duplicate of #109

Findings Information

🌟 Selected for report: hake

Also found by: Jujic, WatchPug, catchup, danb, defsec, kirk-baird, nedodn, shenwilly, sorrynotsorry

Labels

bug
2 (Med Risk)
sponsor acknowledged

Awards

196.5762 USDC - $196.58

External Links

Lines of code

DiamondCutFacet.sol WithdrawFacet.sol DexManagerFacet.sol

Vulnerability details

Impact

contractOwner has complete freedom to change any functionality and withdraw/rug all assets. Even if well intended the project could still be called out resulting in a damaged reputation like in this example

Proof of Concept

https://twitter.com/RugDocIO/status/1411732108029181960

Tools Used

Recommend implementing extra safeguards such as:

  • Limiting the time period where sensitive functions can be used.
  • Having a waiting period before pushed update is executed.
  • Using a multisig to mitigate single point of failure in case contractOwner private key leaks.

#0 - H3xept

2022-04-12T09:24:31Z

The bridges/swaps ecosystem is continually changing. Our mission is to allow seamless UX and provide users with new bridging and swapping routes as fast as possible. This comes at the cost of having some degree of centralization. We choose the Dimond standard to be able to constantly add new bridges and update the existing ones as they progress as well.

We agree with the increased safety a DAO/Multisign mechanism and will provide them in the future. Timelocks are currently not planned, as we want to be able to react fast if we have to disable bridges for security reasons (e.g. if the underlying bridge is being exploited)

#1 - H3xept

2022-04-12T09:33:17Z

Duplicate of #65

#2 - gzeoneth

2022-04-16T17:20:14Z

Valid submission as user will approve fund to the contract.

Awards

1110.8314 USDC - $1,110.83

Labels

bug
resolved
QA (Quality Assurance)

External Links

LOW

Low#1: initNXTP can be initialized multiple times.

L33-37

function initNXTP(ITransactionManager _txMgrAddr) external { Storage storage s = getStorage(); LibDiamond.enforceIsContractOwner(); s.nxtpTxManager = _txMgrAddr; }

The function initNXTP has init in its name, suggesting that it should only be called once to intiliaze the nxtpTxManager. However, it can be called multiple times to overwrite the address.

Recommend setting the address in a constructor or reverting if address is already set. Third option would be changing the name from initNXTP to something like setNXTP to better align function names with their functionality.

Note: Same issue is present in initHop and initCbridge.

Low#2: No zero value checks for both _nxtpData.amount and msg.value in startBridgeTokensViaNXTP.

L46-60

function startBridgeTokensViaNXTP(LiFiData memory _lifiData, ITransactionManager.PrepareArgs memory _nxtpData) public payable { // Ensure sender has enough to complete the bridge transaction address sendingAssetId = _nxtpData.invariantData.sendingAssetId; if (sendingAssetId == address(0)) require(msg.value == _nxtpData.amount, "ERR_INVALID_AMOUNT"); else { uint256 _sendingAssetIdBalance = LibAsset.getOwnBalance(sendingAssetId); LibAsset.transferFromERC20(sendingAssetId, msg.sender, address(this), _nxtpData.amount); require( LibAsset.getOwnBalance(sendingAssetId) - _sendingAssetIdBalance == _nxtpData.amount, "ERR_INVALID_AMOUNT" ); }

Lack of zero value check on both _nxtpData.amount and msg.value allow bridge to be wastefully started.

Recommend implementing a require function such as(line 50):

require(msg.value > 0 || _nxtpData.amount > 0, "Amount must be greater than zero!");

Low#3: No events when approving/blocking a DEX contract.

DexManagerFacet.sol

Implementing events here would increase transparency and trust.

Low#4: No zero address check for adding DEX contract in addDex, batchAddDex, removeDex and batchRemoveDex.

addDex

Zero address check would prevent adding harmful DEX contracts by mistake. If zero address DEXs are whitelisted, users could burn their tokens on accident.

Low#5: Unchecked transfer in WithdrawFacet.sol

WithdrawFacet.sol

Boolean return value for transfer is not checked.

assert(_amount <= self.balance); payable(sendTo).transfer(_amount); } else {

I recommend implementing call instead:

(bool success, ) = sendTo.call.value(_amount)(""); require(success, "Transfer failed.");

Low#6: Function swapTokensGeneric incorrect as to spec.

swapTokensGeneric

* @param _lifiData data used purely for tracking and analytics

The above statement in the code snippet is incorrect because _lifiData is used to calculate the amount to be transfered back to user after swap.

uint256 postSwapBalance = LibAsset.getOwnBalance(_lifiData.receivingAssetId) - receivingAssetIdBalance; LibAsset.transferAsset(_lifiData.receivingAssetId, payable(msg.sender), postSwapBalance);

I recommend either exchanging the use of _lifiData for _swapData or changing the comments.

NON-CRITICAL

Non-crit#1: initNXTP and initHop emit no event.

NXTPFacet.sol: L33-37 HopFacet.sol: L40-52 Emitting an event with the initialization of nxtpTxManager and initHop can increase the protocols transparency and trust. CbridgeFacet.initCbridge emits an event, so keeping it consistent is also good practice.

Non-crit#2: Minor typo in comment in line 31. Conatains - Contains.

L31

Fix typo.

Non-crit#3: Implementation of startBridgeTokensViaCBridge has same functionality but deviates from conventions set in startBridgeTokensViaHop and startBridgeTokensViaNXT

L57-84 L61-72

By comparing startBridgeTokensViaCBridge to startBridgeTokensViaHop and startBridgeTokensViaNXT we can see that the first deviates from the other two, despite containing the same functionality. This hurts readbility. Please implement startBridgeTokensViaCBridge in the same manner the other startBridge functions have been implemented. Differences can be seen below.

startBridgeTokensViaCBridge:

function startBridgeTokensViaCBridge(LiFiData memory _lifiData, CBridgeData calldata _cBridgeData) public payable { if (_cBridgeData.token != address(0)) { uint256 _fromTokenBalance = LibAsset.getOwnBalance(_cBridgeData.token); LibAsset.transferFromERC20(_cBridgeData.token, msg.sender, address(this), _cBridgeData.amount); require( LibAsset.getOwnBalance(_cBridgeData.token) - _fromTokenBalance == _cBridgeData.amount, "ERR_INVALID_AMOUNT" ); } else { require(msg.value >= _cBridgeData.amount, "ERR_INVALID_AMOUNT"); }

startBridgeTokensViaHop:

function startBridgeTokensViaHop(LiFiData memory _lifiData, HopData calldata _hopData) public payable { address sendingAssetId = _bridge(_hopData.asset).token; if (sendingAssetId == address(0)) require(msg.value == _hopData.amount, "ERR_INVALID_AMOUNT"); else { uint256 _sendingAssetIdBalance = LibAsset.getOwnBalance(sendingAssetId); LibAsset.transferFromERC20(sendingAssetId, msg.sender, address(this), _hopData.amount); require( LibAsset.getOwnBalance(sendingAssetId) - _sendingAssetIdBalance == _hopData.amount, "ERR_INVALID_AMOUNT" ); }

Non-crit#4: Commented code in DiamondLoupeFacet.sol.

DiamondLoupeFacet.sol

Please delete code snippet below as it serves no purpose.

// Diamond Loupe Functions //////////////////////////////////////////////////////////////////// /// These functions are expected to be called frequently by tools. // // struct Facet { // address facetAddress; // bytes4[] functionSelectors; // }

Non-crit#5: Incosistent return type within DiamondLoupeFacet.sol contract.

supportsInterface

The function supportsInterface uses an unamed return, while the rest of the contract uses named returns. Please keep it consistent within contracts and accross the project if possible to improve readibility.

function supportsInterface(bytes4 _interfaceId) external view override returns (bool) { LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); return ds.supportedInterfaces[_interfaceId]; }

#0 - H3xept

2022-04-08T07:59:39Z

  1. All init functions are secure as they are marked as onlyOwner; the contract owner might still find it relevant to re-initialise the facet.

#1 - H3xept

2022-04-08T08:03:21Z

  1. Fixed in lifinance/lifi-contracts@daa93cb66d510040966a7e226d97da155139dd0d

#2 - H3xept

2022-04-08T08:07:16Z

  1. Fixed in lifinance/lifi-contracts@9daa1a2bb3a2cf5be938a75746c46fa272b1010a

#3 - H3xept

2022-04-08T15:29:36Z

Re deprecated transfer()

Duplicate of #14

#4 - H3xept

2022-04-11T12:13:28Z

Re init can be called multiple times

Duplicate of #174

#5 - liveactionllama

2022-04-28T06:11:26Z

Per discussion with judge ( @gzeoneth ), Low#3 -> Non Critical

Downgraded issues from this warden that were also considered with their QA Report: #61 #64 #67 -> Low Risk #62 -> Non Critical

#6 - gzeoneth

2022-05-11T17:17:16Z

Low#6: Function swapTokensGeneric incorrect as to spec. related to #67 submitted by the same warden and duplicate of #75

Awards

62.4172 USDC - $62.42

Labels

bug
G (Gas Optimization)
resolved

External Links

Gas#1: Unecessary read from storage prior to a possible revert in initNXTP.

L33-37

        Storage storage s = getStorage();
        LibDiamond.enforceIsContractOwner();
        s.nxtpTxManager = _txMgrAddr;
    }

Storage storage s = getStorage(); reads from storage even though the function can revert at LibDiamond.enforceIsContractOwner(); causing unnecessary usage of gas.

Recommend implementing LibDiamond.enforceIsContractOwner(); prior to Storage storage s = getStorage();

Example:

        LibDiamond.enforceIsContractOwner();
        Storage storage s = getStorage();
        s.nxtpTxManager = _txMgrAddr;
    }

Gas#2: Unnecessary else statement in swapAndStartBridgeTokensViaCBridge

L92-119

The use of the else statement below is not necessary and can be avoided by implementing swapAndStartBridgeTokensViaCBridge in the same manner swapAndStartBridgeTokensViaHop or swapAndStartBridgeTokensViaNXTP have been implemented. Reference for swapAndStartBridgeTokensViaHop below.

swapAndStartBridgeTokensViaCBridge:

function swapAndStartBridgeTokensViaCBridge( LiFiData memory _lifiData, LibSwap.SwapData[] calldata _swapData, CBridgeData memory _cBridgeData ) public payable { if (_cBridgeData.token != address(0)) { uint256 _fromTokenBalance = LibAsset.getOwnBalance(_cBridgeData.token); // Swap _executeSwaps(_lifiData, _swapData); uint256 _postSwapBalance = LibAsset.getOwnBalance(_cBridgeData.token) - _fromTokenBalance; require(_postSwapBalance > 0, "ERR_INVALID_AMOUNT"); _cBridgeData.amount = _postSwapBalance; } else { uint256 _fromBalance = address(this).balance; // Swap _executeSwaps(_lifiData, _swapData); uint256 _postSwapBalance = address(this).balance - _fromBalance; require(_postSwapBalance > 0, "ERR_INVALID_AMOUNT"); _cBridgeData.amount = _postSwapBalance; }

swapAndStartBridgeTokensViaHop swapAndStartBridgeTokensViaHop:

function swapAndStartBridgeTokensViaHop( LiFiData memory _lifiData, LibSwap.SwapData[] calldata _swapData, HopData memory _hopData ) public payable { address sendingAssetId = _bridge(_hopData.asset).token; uint256 _sendingAssetIdBalance = LibAsset.getOwnBalance(sendingAssetId); // Swap _executeSwaps(_lifiData, _swapData); uint256 _postSwapBalance = LibAsset.getOwnBalance(sendingAssetId) - _sendingAssetIdBalance; require(_postSwapBalance > 0, "ERR_INVALID_AMOUNT"); _hopData.amount = _postSwapBalance;

Note: Same issue present in AnyswapFacet.swapAndStartBridgeTokensViaAnyswapL74-108

#0 - H3xept

2022-04-04T08:01:13Z

  1. Fixed in lifinance/lifi-contracts@36039dd114fc23acebb60cd195e1175a076e71b8

#1 - H3xept

2022-04-04T08:02:18Z

  1. Refactored in lifinance/lifi-contracts@87a27cee2fbde337c4ab873971f37573d2240994
AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter