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
Rank: 12/59
Findings: 6
Award: $1,962.85
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: hake
Also found by: Kenshin, Ruhum, VAD37, WatchPug, csanuragjain, hickuphh3, hyh, kirk-baird, obront, pmerkleplant, rayn, shw, tintin, wuwe1
77.3842 USDC - $77.38
https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Libraries/LibSwap.sol#L33-L35
There is a WithdrawFacet
such that only the owner/admin can recover the lost funds in the contract. However, any user can retrieve the funds by using the swapTokensGeneric
function, which might be unexpected behavior.
swapTokensGeneric
function with corresponding parameters to swap 1000 USDC to DAI via the Uniswap V2 router.swapTokensGeneric
function calls _executeSwaps
, which calls LibSwap.swap
. In this function, the contract checks whether LibAsset.getOwnBalance(fromAssetId) < fromAmount
or not. If it has enough balance, it does not call LibAsset.transferFromERC20
to request the tokens from the caller.GenericSwapFacet.sol#L22-L44 Swapper.sol#L12-L23 LibSwap.sol#L33-L35
If recovering funds by anyone is not the desired behavior, consider always transferring the funds from users before the first swap.
#0 - H3xept
2022-04-12T08:39:28Z
Duplicate of #66
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).
https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/CBridgeFacet.sol#L150-L156
The _startBridge
function in CBridgeFacet
is to bridge the tokens to CBridge
by calling the sendNative
or send
function on the bridge contract. However, when calling the sendNative
function, no native token is sent to the bridge. The sendNative
call always fails because the CBridge
side checks whether the exact _cBridgeData.amount
of native tokens are provided. If not, it reverts the transaction.
Take the Ethereum CBridge V2 as an example. The sendNative
function ensures the exact amount of native tokens are provided at line 64.
function sendNative( address _receiver, uint256 _amount, uint64 _dstChainId, uint64 _nonce, uint32 _maxSlippage ) external payable nonReentrant whenNotPaused { require(msg.value == _amount, "Amount mismatch"); require(nativeWrap != address(0), "Native wrap not set"); bytes32 transferId = _send(_receiver, nativeWrap, _amount, _dstChainId, _nonce, _maxSlippage); IWETH(nativeWrap).deposit{value: _amount}(); emit Send(transferId, msg.sender, _receiver, nativeWrap, _amount, _dstChainId, _nonce, _maxSlippage); }
Consider sending _cBridgeData.amount
amount of native tokens when calling the sendNative
function, for example:
ICBridge(bridge).sendNative{ value: _cBridgeData.amount }( _cBridgeData.receiver, _cBridgeData.amount, _cBridgeData.dstChainId, _cBridgeData.nonce, _cBridgeData.maxSlippage );
Also, add a payable
keyword to the sendNative
function in the ICBridge
interface.
#0 - H3xept
2022-04-11T11:00:20Z
Duplicate of #35
#1 - gzeoneth
2022-04-16T16:25:43Z
Changing to Med Risk as no fund is lost.
https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/DexManagerFacet.sol#L73
The batchRemoveDex
function does not work as expected. It should remove all the given DEX addresses from the dexWhitelist
. However, it only removes the first successfully found DEX address and then stops removing the rest. The functionality is broken, and therefore the admin can only remove one DEX address at a time.
There is a return
statement at line 73, so after the first DEX address is found in the array and is removed, the function directly returns.
Consider removing line 73 (the return
in the function).
#0 - H3xept
2022-04-11T10:41:40Z
Duplicate of #34
#1 - gzeoneth
2022-04-16T16:34:28Z
Changing to Med Risk to align with #34
🌟 Selected for report: shw
Also found by: Picodes, hickuphh3, hyh, pmerkleplant
https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/CBridgeFacet.sol#L68
When a user bridges a native token via the startBridgeTokensViaCBridge
function of CBridgeFacet
, the contract checks whether msg.value >= _cBridgeData.amount
holds. In other words, if a user accidentally sends more native tokens than he has to, the contract accepts it but only bridges the _cBridgeData.amount
amount of tokens. The rest of the tokens are left in the contract and can be recovered by anyone (see another submission for details).
Notice that in the similar functions of other facets (e.g., AnyswapFacet
, HopFacet
), the provided native token is ensured to be the exact bridged amount, which effectively prevents the above scenario of loss of funds.
Consider changing >=
to ==
at line 68.
#0 - H3xept
2022-04-01T09:47:01Z
Fixed by lifinance/lifi-contracts@bb21af9a30ea73393101fc80efaa3a1f7cf25bd1
#1 - gzeoneth
2022-04-16T17:42:08Z
Sponsor confirmed with fix.
🌟 Selected for report: hyh
Also found by: Dravee, JMukesh, Jujic, peritoflores, shw, sorrynotsorry, wuwe1
https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/WithdrawFacet.sol#L31
The withdraw
function in WithdrawFacet
uses the native transfer
keyword to send ETH, which is considered unsafe because of the fixed gas budget, and its functionality could be broken in some circumstances:
Consider using a low-level call to send ETH, for example, the LibAsset.transferNativeAsset
function.
#0 - H3xept
2022-04-08T15:28:25Z
Duplicate of #14
🌟 Selected for report: hake
Also found by: 0v3rf10w, 0xDjango, 0xkatana, BouSalman, CertoraInc, Dravee, Hawkeye, IllIllI, JMukesh, Jujic, Kenshin, PPrieditis, Picodes, PranavG, Ruhum, SolidityScan, VAD37, WatchPug, aga7hokakological, catchup, csanuragjain, cthulhu_cult, defsec, dimitri, hickuphh3, hubble, hyh, kenta, kirk-baird, obront, peritoflores, rayn, robee, saian, samruna, shenwilly, shw, sorrynotsorry, tchkvsky, teryanarmen, ych18
113.5781 USDC - $113.58
https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/NXTPFacet.sol#L54-L59 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L46 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/CBridgeFacet.sol#L64 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/HopFacet.sol#L69
The startBridgeTokensVia...
functions do not handle fee-on-transfer tokens (e.g, USDT) correctly. If some fee is deducted during the token transfer (LibAsset.transferFromERC20
), the transaction will revert because of the following require
check. As a result, users cannot bridge fee-on-transfer tokens via the startBridgeTokensVia...
functions.
Take NXTPFacet
as an example. At line 55, _nxtpData.amount
is provided as the parameter of LibAsset.transferFromERC20
. In cases where a fee-on-transfer token is transferred, the received amount could be less than _nxtpData.amount
.
Then, at line 56, the contract ensures whether its balance is increased by exactly _nxtpData.amount
, which is false in this case, causing the tx to be reverted.
Consider calculating the received amount of tokens after the transfer, and modify _nxtpData.amount
accordingly, for example:
LibAsset.transferFromERC20(sendingAssetId, msg.sender, address(this), _nxtpData.amount); _nxtpData.amount = LibAsset.getOwnBalance(sendingAssetId) - _sendingAssetIdBalance;
#0 - maxklenk
2022-04-01T07:38:14Z
Thanks for pointing out that Fee-on-transfer tokens are not supported. We are aware of that and will document it accordingly in our documentation. We will not be able to support those tokens as long as the exchanges and bridges do not support them as well. The balance check prevents continuing the transaction with such a token.
#1 - gzeoneth
2022-04-16T18:03:51Z
Downgrading to Low/QA. Treating as warden's QA Report.
#2 - JeeberC4
2022-04-17T04:25:32Z
Preserving original title: Fee-on-transfer tokens are not handled correctly