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: 9/59
Findings: 3
Award: $2,593.92
π Selected for report: 0
π Solo Findings: 0
π Selected for report: GeekyLumberjack
Also found by: CertoraInc
https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Libraries/LibSwap.sol#L42-L46 https://github.com/code-423n4/2022-03-lifinance/blob/699c2305fcfb6fe8862b75b26d1d8a2f46a551e6/src/Libraries/LibAsset.sol#L49-L50
Low level calls (call, delegate call and static call) return success if the called contract doesnβt exist (not deployed or destructed).
That means that in LibSwap
, if _swapData.callTo
doesn't exist the call will fail but success will be set to true, which will act like the call was successful.
This bug can also be seen in the transferNativeAsset
function of the LibAsset
library:
function transferNativeAsset(address payable recipient, uint256 amount) internal { // solhint-disable-next-line avoid-low-level-calls (bool success, ) = recipient.call{ value: amount }(""); require(success, "#TNA:028"); }
Remix & VS Code
Check address existence before the low level call
#0 - H3xept
2022-04-12T10:53:10Z
Duplicate of #101
π 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
224.2059 USDC - $224.21
The ITransactionManager
intraface's solidity version is pragma solidity 0.8.7;
, and the rest of the contracts are pragma solidity ^0.8.7;
- this should be consistent.
Missing space in error message in the initializeDiamondCut
function of LibDiamond
library:
require(_calldata.length == 0, "LibDiamondCut: _init is address(0) but_calldata is not empty");// should be "... but _calldata ..."
Pragmas should be locked to a specific compiler version (instead of ^0.8.7
for example), to avoid contracts getting deployed using a different version, which may have a greater risk of undiscovered bugs.
Unsafe transferOwnership - it will be more safe to use a pattern for transferring the ownership, for example making the new pending owner to accept the ownership. That will make sure that the new owner is an actual existing address and not a self-distructed address or a non-existing address.
Check that the array's lengths given to the initHop
function as parameters are equal (can cause errors)
The fallback function of the LiFiDiamond is re-enterable, so it will be a good thing to add a reentrency guard to it (one reentrency guard will lock all the functions of the facets).
In the CBridgeFacet
contract you make sure that msg.value >= _cBridgeData.amount, "ERR_INVALID_AMOUNT"
, but in the other facets you make sure that they are equal. This difference might not be wanted, and you should pay attention to it.
In the comment before the _bridge
function it says that the function is public, but it is clearly declared as internal.
/* * @dev Public view function for the CBridge router address * @returns the router address */ function _bridge() internal view returns (address) { Storage storage s = getStorage(); return s.cBridge; }
#0 - H3xept
2022-04-07T07:37:04Z
Re Solidity version pragma: We internally decided to tackle the solc pragma issue after the audit resolves.
#1 - H3xept
2022-04-07T07:40:08Z
Fixed in lifinance/lifi-contracts@221fe883f705635feebb2af64a028f30d05afbf8
#2 - H3xept
2022-04-07T09:25:41Z
Fixed in lifinance/lifi-contracts@77afb9a8839efbee2a9b8367fa81fecfec7ce647
#3 - H3xept
2022-04-07T09:35:52Z
Fixed in lifinance/lifi-contracts@87a27cee2fbde337c4ab873971f37573d2240994
#4 - H3xept
2022-04-11T11:15:24Z
Duplicate of #71
#5 - H3xept
2022-04-11T12:22:04Z
Duplicate of #143
π Selected for report: Dravee
Also found by: 0v3rf10w, 0xDjango, 0xNazgul, 0xkatana, ACai, CertoraInc, FSchmoede, Funen, Hawkeye, IllIllI, Jujic, Kenshin, PPrieditis, Picodes, SolidityScan, TerrierLover, Tomio, WatchPug, catchup, csanuragjain, defsec, dimitri, hake, hickuphh3, kenta, minhquanym, obront, peritoflores, rayn, rfa, robee, saian, samruna, tchkvsky, teryanarmen, ych18
86.4214 USDC - $86.42
In the _startBridge
function of the CBridgeFacet
contract, use s.cBridge
instead of calling the _bridge()
function
Storage storage s = getStorage(); address bridge = _bridge();
This is the implementation of the function:
/* * @dev Public view function for the CBridge router address * @returns the router address */ function _bridge() internal view returns (address) { Storage storage s = getStorage(); return s.cBridge; }
There is no need to define two variables in the swapAndStartBridgeTokensViaCBridge
function of the CBridgeFacet
contract, you can simply use one variable for this calculation.
Code before:
uint256 _fromTokenBalance = LibAsset.getOwnBalance(_cBridgeData.token); // Swap _executeSwaps(_lifiData, _swapData); uint256 _postSwapBalance = LibAsset.getOwnBalance(_cBridgeData.token) - _fromTokenBalance;
Code after:
uint256 _balanceDiff = LibAsset.getOwnBalance(_cBridgeData.token); // Swap _executeSwaps(_lifiData, _swapData); _balanceDiff = LibAsset.getOwnBalance(_cBridgeData.token) - _balanceDiff;
Using >
costs more gas than using <
, so you can replace all the >
with <
and switch the expressions, for example in the CBridgeFacet
contract, use require(0 < _postSwapBalance, "ERR_INVALID_AMOUNT");
instead of require(_postSwapBalance > 0, "ERR_INVALID_AMOUNT");
. This can be done in many places in the code, simply search >
and switch the expressions to replace it with <
Loop optimizations - there are multiple optimizations that can be done to your loops. Let's look at the loop in the batchAddDex
function of the DexManagerFacet
contract for example:
++i
instead of i++
_dexs[i]
in a local variable instead of accessing it 3 times in every iterationfor (uint256 i; i < _dexs.length; i++) { if (s.dexWhitelist[_dexs[i]] == true) { continue; } s.dexWhitelist[_dexs[i]] = true; s.dexs.push(_dexs[i]); }
These kind of optimizations can be done to more loops in your code. Now we'll take a look at the loop in the batchRemoveDex
function of the DexManagerFacet
contract. Here we can do multiple things, similar to the previous loop:
++i
and ++j
instead of i++
and j++
_dexs[i]
in a local variable instead of accessing it many times in every iteration (here it is being accessed in every iteration of the inner loop, plus 2 times outside the loop).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; } } }
More loops in the code:
_executeSwaps
function of the Swapper
contractinitHop
function of the HopFacet
contractfacets
function of the DiamondLoupeFacet
contractremoveDex
function of the DexManagerFacet
contractCalculate the hash before deployment, and just use the hash result. This can be done for example in the CBridgeFacet
contract where you define bytes32 internal constant NAMESPACE = keccak256("com.lifi.facets.cbridge2");
. This can also be done in the HopFacet
, NXTPFacet
and LibDiamond
contracts.
Use defined values instead of calculate the maximum value of uint256 - use type(uint256.max
instead of 2**256 - 1
(this will save the gas spent on the calculation). This can be done in the LibAsset
and LibSwap
libraries.
use boolean value instead of comparing to true
or false
. For example in the _executeSwaps
function of the Swapper
contract, the code in the require can be written as:
function _executeSwaps(LiFiData memory _lifiData, LibSwap.SwapData[] calldata _swapData) internal { // Swap for (uint8 i; i < _swapData.length; i++) { require( ls.dexWhitelist[_swapData[i].approveTo] && ls.dexWhitelist[_swapData[i].callTo], "Contract call not allowed!" ); LibSwap.swap(_lifiData.transactionId, _swapData[i]); } }
instead of:
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]); } }
Use unchecked in the swapAndCompleteBridgeTokensViaNXTP
of the NXTPFacet
contract, this calculation won't underflow because we know that postSwapBalance > startingBalance
.
if (postSwapBalance > startingBalance) { finalBalance = postSwapBalance - startingBalance; // use unchecked on this calculation LibAsset.transferAsset(finalAssetId, payable(receiver), finalBalance); }
Simplify the if statements in the swap
function of LibSwap
library - this will save the gas spent on checking the same condition and calling the same function twice.
old code:
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); }
new 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); }
call the LibDiamond.enforceIsContractOwner
function before the getStorage
function in the initNXTP
function of the NXTPFacet
contract and in the initCbridge
function of the CBridgeFacet
contract, in order to save gas in case that the LibDiamond.enforceIsContractOwner
function will revert
function initNXTP(ITransactionManager _txMgrAddr) external { Storage storage s = getStorage(); LibDiamond.enforceIsContractOwner(); s.nxtpTxManager = _txMgrAddr; }
function initCbridge(address _cBridge, uint64 _chainId) external { Storage storage s = getStorage(); LibDiamond.enforceIsContractOwner(); s.cBridge = _cBridge; s.cBridgeChainId = _chainId; emit Inited(s.cBridge, s.cBridgeChainId); }
#0 - H3xept
2022-03-31T14:03:23Z
Re: >
is more expensive than <
I could not find any evidence to back this up online. I compiled two solidity functions to test it out and it seems that the assembly produced is basically the same. Unless there's a big asymmetry in the VM implementation of GT compared to LT, the gas usage should be the same. Arguably, require(a > 0, ...)
is much more readable than require(0 < a, ...)
.
a > b:
DUP2 b DUP4 a GT a > b SWAP1 return a > b POP return a > b
a < b:
DUP2 b DUP4 a LT a < b SWAP1 return a < b POP return a < b
#1 - H3xept
2022-03-31T14:15:01Z
Re: Rewriting MAX_INT According to this source, SOLC will evaluate this only once at compile time.
#2 - H3xept
2022-04-08T15:21:07Z
Duplicate of #39
#3 - H3xept
2022-04-08T15:24:21Z
Duplicate of #39
#4 - H3xept
2022-04-11T11:49:43Z
Duplicate of #39
#5 - H3xept
2022-04-11T11:56:53Z
We internally decided to avoid unchecked operations for now.
#6 - H3xept
2022-04-11T12:06:46Z
We internally decided to avoid previx increments for now.
#7 - H3xept
2022-04-11T12:53:20Z
Duplicate of #182