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: 16/59
Findings: 4
Award: $1,247.23
π Selected for report: 0
π 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/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Libraries/LibAsset.sol#L59-L70 https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Libraries/LibSwap.sol#L33-L35 https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Facets/GenericSwapFacet.sol#L28-L30
Using LibAsset.sol
approval exploit which LibSwap.sol
approve all whitelist address access to transfer all token hold by LiFi contract.
reference 1
reference 2
most facet function does not check input LibSwap.SwapData[]
before _executeSwaps(_lifiData, _swapData);
pass on to LibSwap.sol
. This allows anyone executes anything with whitelist DEX using Li.Fi as caller
Any malicious user can call LibSwap.sol
execute to swap all token balance hold by Li.Fi contract to another token.
Which can be used to transfer all token hold by Li.Fi contract to another address.
This only show the most simple, direct way to withdraw ERC20 token from contract. Consider Li.Fi already implement Whitelist function and token whitelist.
swapTokensGeneric()
function in GenericSwapFacet.sol
_lifiData.receivingAssetId
is USDC. This cache uint256 receivingAssetIdBalance
equal to current USDC token balance.LibSwap.SwapData[] _swapData
is custom compose to swap all token hold by Li.Fi contract to USDC through Uniswap.callTo
and approveTo
is Uniswap router address (whitelisted in dex.ts
)sendingAssetId
, receivingAssetId
and fromAmount
in SwapData
can be any valuebytes callData
in LibSwap.SwapData
is simply swapExactTokensForTokens
with UniswaptransferFrom
to get all Li.Fi token directly.Do this again with USDC and USDT to withdraw all USDC leftover.
Note: there are many more ways to exploit this using the same generic LibSwap.swap
function. As long as Li.Fi contract hold ERC-20 token, there is no safe or fair way to prevent attacker make unfair trade with token.
Manual
The best fix from my experience is to remove LibSwap.sol
and restart from scratch. Try different approach instead of generic arbitrary call to mitigate this. Spending time on securing LibSwap.sol
is simply not worth it.
From LibSwap documentation here, A library used to perform multiple swaps given an array of DEX addresses and calldata. This makes it very flexible but also dangerous in that it allows arbitrary calls to any contract
This is a wrong approach with diamond facet. Diamond facets are already flexible, no need for arbitrary call. Diamond allow adhoc implementation for each chain, different contracts which give flexibility to implement new features or new DEX. Sharing all swap logic into one single function and reuse it everywhere is not a good idea.
Current implementation, Swapper.sol
raw call in loop, is no different from a multicall
function to different DEX to swap and then cross bridge.
Might as well use a multicall
into facets function instead of facet use same lib function.
It is much simpler structure wise.
For flashloan swap, as long as Li.Fi not check correct amount of token being swapped, attacker can use other people token like token hold by LiFi contract.
#0 - H3xept
2022-04-12T08:38:45Z
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).
π Selected for report: kirk-baird
Also found by: TomFrenchBlockchain, VAD37, WatchPug, hyh, rayn, wuwe1
https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Facets/Swapper.sol#L14 https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Libraries/LibSwap.sol#L42 https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Facets/AnyswapFacet.sol#L85 https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Facets/CBridgeFacet.sol#L98 https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Facets/GenericSwapFacet.sol#L23 https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Facets/NXTPFacet.sol#L91
Swapper.sol
function _executeSwaps()
call in a loop that use the same msg.value
sent by user. This allows anyone to call _executeSwaps()
which can control Li.Fi contract ETH balance on raw call through function LibSwap.swap()
.
Note: Lines of code above include all facets swap function check wrong input token before swap. Which anyone can freely call whatever whitelist function on whitelist address without spend their token.
Attacker can withdraw main token (ETH) from LiFi contract. Dex Whitelist and Function whitelist cannot prevent this
Other User funds are not affected.
Attacker can exploit a function that does not check main token balance after _executeSwaps
like in AnySwapFacet.sol
.
These lines of code here only check input balance of AnyswapData memory _anyswapData
after execute and not balance of token inside LibSwap.SwapData[] calldata _swapData
.
(same go for GenericSwapFacet.sol
with swapTokensGeneric()
and every other facets)
Attacker can craft special msg to DODO RouteProxy
(whitelist in dex.ts
) and swap Li.Fi ETH to other token and transfer it to another address.
swapAndStartBridgeTokensViaAnyswap
with 1 ETH and another random token.msg.value
with 1 ETH and 2 swap with DODO router function.Swapper.sol
call in array loopLibSwap.sol
only take ERC20 from sender but not check or reduce amount from msg.value
_startBridge
function to end bridge token with no relation to Dodo swap at all.The simpler way is to just swap 1 ETH to USDC with swapTokensGeneric()
will just send back 2 ETH worth of USDC with same exploit above.
Note: This concept use other attack approach from different facets instead of attack through Uniswap like other report.
Due to some other bugs exist in LibSwap.sol
and Swapper.sol
that I already found. It is possible swap ETH directly for some fake token.
Manual
msg.value
in Swapper.sol
loop and check balance after each swap.LibSwap.SwapData[]
as one data fit all for facet input.Swapper.sol
and replace it with multicall function.Migrate this might require change all logic in facets.
I would highly recommend create adhoc data function and execution for each facet instead of 1 Swapper.sol
for all. This help reduces complexity and help reduce development time in long term. Also, 1 broken library does not require refactoring all facets code. Each facet should be modular and independent of each other.
Each facet should have its own unique SwapData
or inputData
.
LibSwap.SwapData[]
should be a subset data of this SwapData
and not a direct function input.
If possible, create new SwapData
inside function and not input send by user. Input should be used to create data to pass on LibSwap.sol
.
#0 - H3xept
2022-04-11T09:21:00Z
Duplicate of #86
π Selected for report: WatchPug
Also found by: VAD37, catchup, csanuragjain, rayn
https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Libraries/LibAsset.sol#L59-L70 https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Libraries/LibSwap.sol#L38
LibAsset.sol
approve max for spender instead of replace or add approval amount.
This is unintended behavior as swap suppose to use only amount user send to contract and not all token currently hold.
The docs inside LibAsset.sol
for approveERC20
is confusing. It says Amount to approve for spending
but it actually gives max allowance for spender. This might be error on Connext part since their implementation is different.
Technically, Li.Fi gives all whitelisted address inside config Dex.ts
access to transfer all token balance currently hold by Li.Fi.
Note: suppose to be low severity, but there are multiple exploits using this to withdraw all tokens hold by LiFi contract.
Also, if attacker have special angle attack in the future, attacker can force LibSwap.sol
call router like Uniswap to call transferFrom
directly with full approval by Li.Fi to withdraw all tokens.
Anyone can tell Li.Fi what to do with all token currently hold in contract without owner access.
Ex: attacker can force Li.Fi trade its own token for another token. Because LiFi give Uniswap router approval to spend any token.
Several angles of attack can happen because attacker can freely craft swapData
to LibSwap.sol
swap function without restriction
safeTransferERC20
from sender before swap useless, because attacker can swap using fund hold by LiFi contract too.Manual
approveERC20
here with increaseERC20Allowance
since most ERC20 token support this.#0 - H3xept
2022-04-12T10:12:27Z
Duplicate of #130
#1 - gzeoneth
2022-04-16T17:01:53Z
Duplicate of #160
π 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
118.82 USDC - $118.82
Low Severity
Planned improvement with function signature whitelist is not enough to prevent malicious call through LibSwap.sol
and Swapper.sol
.
Attacker can create custom token and force Li.Fi contract to call transfer
or approve
to this custom contract token. Code
Some coin take fee on safeTransferFrom
raise some expected behavior. Relevance issue I read, https://github.com/code-423n4/rulebook/issues/3.
Attacker can use LiFi contract as call function outside of whitelist function. But without caller msg.sender
as LiFi, the fund is safe.
Yet it is possible to create reentrancy attack through this contract token transfer or approval before it call to LibSwap.swap()
.
swapTokensGeneric
to swap fake ERC20 token through liquid pool using UniswapV2Router02 (which is whitelisted in Dex.ts
config)approve
function on malicious contract. At this point attacker can call anything they want.Without msg.sender
as Li.Fi, there is not much to do here except reentrancy back to Li.Fi.
Add token whitelist as well along with function whitelist.
It would be safe to consider reentrancy guard at the cost of more 40000 gas for each facet.
Rating: Low Severity
All Facets are implemented follow these swap token pattern.
function doSomething( LiFiData memory _lifiData, // for event LibSwap.SwapData[] calldata _swapData, // the swap data FacetData memory inputSpecialData ){ tokenFromInput = inputSpecialData.token; // balance token before swap uint256 _fromTokenBalance = LibAsset.getOwnBalance(tokenFromInput); // Swap. _executeSwaps(_lifiData, _swapData); // Check balance to see if we have new token that was swapped and return it to user. uint256 _postSwapBalance = LibAsset.getOwnBalance(tokenFromInput) - _fromTokenBalance; require(_postSwapBalance > 0, "ERR_INVALID_AMOUNT"); // give token back to user or bridge it over another chain // .... }
The expected behavior is user use UI and the input data would be the same as FacetData
made for each facet.
But what happen is, attacker can use this to tell swap token unrelated to current facet. Which the same as call anything they want with whitelist address
This allow attack angle if _executeSwaps
does not work correctly (it didn't check swap and balance correctly).
I assume the expected behavior is user only send token to contract and contract swap that token only. Hence checking tokenFromInput
, postswapbalance
.
Split this into atomic function to do one thing and use multicall into these function is much better approach to this.
Rating: NonCritical
This project break lots of software development practices and design principles which should translated well into security practice.
LibSwap
have no document to explain reasoning and it try to do both swap and get token from user.LibSwap
try to do everything in one function is bad idea all around.NativeAsset
is duplicated and checking several times during swap from internal LibSwap
to external facets functionSwapper.sol
send unnecessary information like entire LiFiData
struct data when it only need transactionId
to emit event#0 - H3xept
2022-04-11T11:29:55Z
Duplicate of #108
#1 - H3xept
2022-04-11T11:46:29Z
Duplicate of #76