LI.FI contest - 0xDjango'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: 1/59

Findings: 6

Award: $7,983.73

🌟 Selected for report: 2

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: 0xDjango

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

Labels

bug
3 (High Risk)

Awards

2219.3568 USDC - $2,219.36

External Links

Lines of code

https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Facets/GenericSwapFacet.sol#L23-L30

Vulnerability details

Impact

In the swapTokensGeneric() function, an arbitrary number of swaps can be performed from and to various tokens. However, the final balance that is sent to the user relies on _lifiData.receivingAssetId which has no use in the swapping functionality. LifiData is claimed to be used purely for analytical reasons per the comments to this function. If this value is input incorrectly, the swapped tokens will simply sit in the contract and be lost to the user.

Proof of Concept

Imagine a call to swapTokensGeneric() with the following parameters (excluding unnecessary parameters for this example):

  • LifiData.receivingAssetId = '0xUSDC_ADDRESS'

Single SwapData array:

  • LibSwap.SwapData.sendingAssetId = '0xWETH_ADDRESS'
  • LibSwap.SwapData.receivingAssetId = '0xDAI_ADDRESS'

Since the receivingAssetId from SwapData does not match the receivingAssetId from LifiData, the final funds will not be sent to the user after the swap is complete, based on the following lines of code:

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

Lines 1, 3, and 4 reference LifiData.receivingAssetId and handle the transfer of funds following the swaps. Line 2 performs the swap, referencing SwapData.receivingAssetId as can be seen in the executeSwaps() function definition:

function _executeSwaps(LiFiData memory _lifiData, LibSwap.SwapData[] calldata _swapData) internal { // Swap for (uint8 i; i < _swapData.length; i++) { require( ls.dexWhitelist[_swapData[i].approveTo] == true && ls.dexWhitelist[_swapData[i].callTo] == true, "Contract call not allowed!" ); LibSwap.swap(_lifiData.transactionId, _swapData[i]); } }

Tools Used

Manual Review.

I recommend adding a check that _lifiData.receivingAssetId equals the receivingAssetId of the last index of the SwapData array, or simply use the receivingAssetId of the last index of the SwapData array for sending the final tokens to the user.

#0 - H3xept

2022-04-05T15:02:46Z

Fixed in lifinance/lifi-contracts@52aa2b8ea3bc51de3e60784c00201e29103fe250

#1 - gzeoneth

2022-04-16T16:36:38Z

Sponsor confirmed with fix.

Findings Information

🌟 Selected for report: 0xDjango

Also found by: GeekyLumberjack, pmerkleplant

Labels

bug
3 (High Risk)
resolved

Awards

4566.5778 USDC - $4,566.58

External Links

Lines of code

https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Facets/GenericSwapFacet.sol#L23-L30 https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Libraries/LibSwap.sol#L48

Vulnerability details

Impact

Every function that stems from the GenericSwapFacet lacks checks to ensure that some tokens have been returned via the swaps. In LibSwap.sol in the swap() function, the swap call is sent to the target DEX. A return of success is required, otherwise the operation will revert.

Each "inner" swap via LibSwap.sol lacks output checks and also the "outer" swapTokensGeneric() via GenericSwapFacet.sol lacks a final check as well.

There is a possibility that the calldata is accidently populated with a function in the target router that is not actually performing any swapping functionality, getAmountsOut() for example. The function will return true, but no new returned tokens will be present in the contract. Meanwhile, the contract has already received the user's fromTokens directly.

Tools Used

Manual Review

This would be a potential use case of using function signature whitelists as opposed to contract address whitelists, as noted as a possibility by the LiFi team.

Otherwise, the following require statement in swapTokensGeneric() would ensure that at least a single token was received:

require(LibAsset.getOwnBalance(_swapData.receivingAssetId) - toAmount) > 0, "No tokens received")

#0 - H3xept

2022-04-11T11:47:42Z

Fixed in lifinance/lifi-contracts@3a42484dda8bafcfd122c8aa3b61d3766d545bf9

#1 - gzeoneth

2022-04-16T16:38:03Z

Sponsor confirmed with fix.

Findings Information

🌟 Selected for report: hickuphh3

Also found by: 0xDjango, JMukesh, Ruhum, cccz

Labels

bug
duplicate
2 (Med Risk)

Awards

665.807 USDC - $665.81

External Links

Lines of code

https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Libraries/LibSwap.sol#L29-L46

Vulnerability details

Impact

In the swap() function in LibSwap.sol, the SwapData attributes determine many of the initial logical steps, but do not have any bearing on the actual calldata that's passed via the call() function.

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);

The first several lines of this function transfer the _swapData.fromAssetId to the contract in the amount of _swapData.fromAmount, and subsequently approves _swapdata.approveTo for the same amount. The issue is that these values may not be what is being passed to the DEX contract via the _swapData.callData in the call() function.

The most severe issue as a result of a discrepancy that I can foresee is if swapData.fromAmount is larger than what is passed in via the calldata, the full swapData.fromAmount is transferred to the contract, then the swap is performed via the value in the calldata. The leftover amount will sit in the contract, available to whoever next swaps TO the original fromAssetId.

Tools Used

Manual review

You have mentioned the idea of whitelisting function signatures instead of DEX addresses. If you do so, if it's feasible to obtain the contracts' ABIs to decode the calldata input, you can verifiably ensure that the values used in the internal logic will match what is sent to the DEXs.

Otherwise, you can add a step to "sweep" the remainder balance (if any) following swaps to ensure that the user retains maximum token value.

#0 - H3xept

2022-04-12T10:35:33Z

Duplicate of #33

Findings Information

🌟 Selected for report: hickuphh3

Also found by: 0xDjango, WatchPug, catchup, csanuragjain, hyh, shw, ych18

Labels

bug
duplicate
2 (Med Risk)

Awards

303.3583 USDC - $303.36

External Links

Lines of code

https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Facets/DexManagerFacet.sol#L62-L77

Vulnerability details

Impact

Only one DEX removed while performing batchRemoveDEX() in DexManagerFacet.sol, potentially leading to many whitelisted DEXs previously thought to have been removed.

Proof of Concept

Given the following code to remove multiple DEXs from the whitelist:

function batchRemoveDex(address[] calldata _dexs) external { LibDiamond.enforceIsContractOwner(); for (uint256 i; i < _dexs.length; i++) { if (s.dexWhitelist[_dexs[i]] == false) { continue; } s.dexWhitelist[_dexs[i]] = false; for (uint256 j; j < s.dexs.length; j++) { if (s.dexs[j] == _dexs[i]) { _removeDex(j); return; } } } }

The batchRemoveDEX() function contains nested for loops. The inner for loop unfortunately uses return instead of break, causing the execution of the function to stop the moment it removes the first DEX. This is potentially serious as the team will believe they've removed multiple DEXs from being able to be called from the facets, meanwhile the DEXs can still receive said calls.

Tools Used

Manual review.

Change return to break.

#0 - H3xept

2022-04-11T10:40:12Z

Duplicate of #34

Awards

167.0761 USDC - $167.08

Labels

bug
resolved
QA (Quality Assurance)

External Links

Issue 1 (Low) - Avoid Floating Pragma

Details

Most or all contracts include a floating pragma ^0.8.7. It is recommended that the compiler version be locked to a single version to avoid contracts deployed with different versions that may contain various compiler bugs.

Issue 2 (Low) - No Transfer Ownership Pattern

https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Facets/OwnershipFacet.sol#L7-L11

Details

It is recommended to employ a transfer/accept ownership transfer pattern. This will avoid accidently transferring ownership to an incorrect address, as the accept function must be called by the pending owner for the transfer to be completed.

Issue 3 (Low) - LifiData can be set arbitrarily, dimenishing useability in analytics

https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Facets/GenericSwapFacet.sol#L22

Details

The comments specify:

_lifiData data used purely for tracking and analytics

The function uses the _lifiData attributes such as transactionId for logging, but this value can be arbitrary by the msg.sender.

Issue 4 (low) - Unnecessary transfer amount to LibSwap.sol

https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Libraries/LibSwap.sol#L33-L35

Details

The swap function contains logic that the fromAmount must be transferred to LibSwap.sol if the contract balance is less than the fromAmount. Given the possiblity that the contract already contains some number of tokens, the entire from amount will still be transferred to the contract, and subsequently swapped, leaving the same remainder tokens as before. Given the functionality that checks if the contract balance is equal to or larger than the from amount in the first place, it would make sense to only send the difference between the fromAmount and the contract balance.

Issue 5 (low) - Use of deprecated safeApprove() function

https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Libraries/LibAsset.sol#L67-L68

Details

OpenZeppelin has marked this function as deprecated and recommends using safeIncreaseAllowance. https://github.com/OpenZeppelin/openzeppelin-contracts/blob/566a774222707e424896c0c390a84dc3c13bdcb2/contracts/token/ERC20/utils/SafeERC20.sol#L38-L43

#0 - H3xept

2022-04-07T09:39:46Z

Re floating pragma

We internally decided to tackle this once the audit resolves

#1 - H3xept

2022-04-07T09:40:17Z

Re No Transfer Ownership Pattern

Fixed in lifinance/lifi-contracts@221fe883f705635feebb2af64a028f30d05afbf8

#2 - H3xept

2022-04-07T11:18:43Z

Re deprecated safeApprove

Fixed in lifinance/lifi-contracts@b4c3fab52f13194dc6c5e4d06819f8228b866035

#3 - H3xept

2022-04-07T11:24:01Z

Re Unnecessary transfer amount to LibSwap.sol

We now send tokens back to the user after swaps

#4 - ezynda3

2022-04-07T11:25:29Z

We now send tokens back to the user after swaps so we should not do this fix.

#5 - H3xept

2022-04-11T11:23:04Z

Re Use of deprecated safeApprove()

Duplicate of #82

#6 - H3xept

2022-04-11T12:22:18Z

Re No Transfer Ownership Pattern

Duplicate of #143

Awards

61.5429 USDC - $61.54

Labels

bug
G (Gas Optimization)
resolved

External Links

Repeated if conditions

Same conditions repeated in separate ifs, can be reduced to nested ifs, saving a repeated clause.

https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Libraries/LibSwap.sol#L33-L39

Update code:

if (!LibAsset.isNativeAsset(fromAssetId)) { if (LibAsset.getOwnBalance(fromAssetId) < fromAmount) { LibAsset.transferFromERC20(fromAssetId, msg.sender, address(this), fromAmount); } LibAsset.approveERC20(IERC20(fromAssetId), _swapData.approveTo, fromAmount); }

#0 - H3xept

2022-04-04T08:10:35Z

Tackled in previous commit.

#1 - H3xept

2022-04-08T15:21:17Z

Duplicate of #39

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