LI.FI contest - rayn'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: 4/59

Findings: 9

Award: $5,952.43

🌟 Selected for report: 0

πŸš€ 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

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

Vulnerability details

Impact

https://github.com/code-423n4/2022-03-lifinance/blob/main/docs/GenericSwapFacet.md stated that _lifiData is strictly for analytics purposes. But _lifiData is used to set receivingAsset.

Proof of Concept

In GenericSwapFacet.swapTokensGeneric, _lifiData.receivingAssetId is used in LibAsset.getOwnBalance and LibAsset.transferAsset.

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

function swapTokensGeneric(LiFiData memory _lifiData, LibSwap.SwapData[] calldata _swapData) public payable { uint256 receivingAssetIdBalance = LibAsset.getOwnBalance(_lifiData.receivingAssetId); // Swap _executeSwaps(_lifiData, _swapData); uint256 postSwapBalance = LibAsset.getOwnBalance(_lifiData.receivingAssetId) - receivingAssetIdBalance; LibAsset.transferAsset(_lifiData.receivingAssetId, payable(msg.sender), postSwapBalance); ... }

Tools Used

Manual code review.

In order to follow the policy, there should be a new parameter GenericSwapData.

function swapTokensGeneric(LiFiData memory _lifiData, LibSwap.SwapData[] calldata _swapData, GenericSwapData _genericSwapData) public payable { uint256 receivingAssetIdBalance = LibAsset.getOwnBalance(_genericSwapData.receivingAssetId); // Swap _executeSwaps(_lifiData, _swapData); uint256 postSwapBalance = LibAsset.getOwnBalance(_genericSwapData.receivingAssetId) - receivingAssetIdBalance; LibAsset.transferAsset(_genericSwapData.receivingAssetId, payable(msg.sender), postSwapBalance); ... }

#0 - H3xept

2022-04-12T10:41:03Z

Duplicate of #62

#1 - gzeoneth

2022-04-16T17:10:32Z

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

#2 - gzeoneth

2022-04-16T18:58:43Z

Consider with warden's QA Report #139

#3 - gzeoneth

2022-05-11T17:15:57Z

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: kirk-baird

Also found by: cccz, dirk_y, hickuphh3, rayn

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/main/src/Facets/AnyswapFacet.sol#L35 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L37 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L74 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L80 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L131 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L136 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L137 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L149 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L151 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L159 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L161

Vulnerability details

Impact

AnyswapFacet does not check whether the passed _anyswapData.router is benign. This allows attackers to provide malicious routers to "exchange" tokens and native asset in an extremely favorable rate from Diamond, or potentially stealing all remaining tokens when chained with the LibAsset.approveERC20 bug discussed in a separate report.

Proof of Concept

Opposed to other contracts, AnyswapFacet does not have a fixed router contract explicitly assigned by Diamond owner. Instead, it allows users to pass the desired router along with other function arguments. This opens up a chance for attackers to craft a fake _anyswapData.router.

To demonstrate the exploit, we will focus on the startBridgeTokensViaAnyswap function and discuss the case of profiting by exchanging USDT to ether.

Below is the code covered in our attack. Notice while we focus our attack on startBridgeTokensViaAnyswap here, swapAndStartBridgeTokensViaAnyswap is also suscepetible to the same attack logic.

function startBridgeTokensViaAnyswap(LiFiData memory _lifiData, AnyswapData calldata _anyswapData) public payable { address underlyingToken = IAnyswapToken(_anyswapData.token).underlying(); if (_anyswapData.token != address(0) && underlyingToken != IAnyswapRouter(_anyswapData.router).wNATIVE()) { if (underlyingToken == address(0)) { underlyingToken = _anyswapData.token; } uint256 _fromTokenBalance = LibAsset.getOwnBalance(underlyingToken); LibAsset.transferFromERC20(underlyingToken, msg.sender, address(this), _anyswapData.amount); require( LibAsset.getOwnBalance(underlyingToken) - _fromTokenBalance == _anyswapData.amount, "ERR_INVALID_AMOUNT" ); } else { require(msg.value == _anyswapData.amount, "ERR_INVALID_AMOUNT"); } _startBridge(_anyswapData); ... } function _startBridge(AnyswapData memory _anyswapData) internal { // Check chain id require(block.chainid != _anyswapData.toChainId, "Cannot bridge to the same network."); address underlyingToken = IAnyswapToken(_anyswapData.token).underlying(); if (underlyingToken == IAnyswapRouter(_anyswapData.router).wNATIVE()) { IAnyswapRouter(_anyswapData.router).anySwapOutNative{ value: _anyswapData.amount }( _anyswapData.token, _anyswapData.recipient, _anyswapData.toChainId ); return; } if (_anyswapData.token != address(0)) { // Has underlying token? if (underlyingToken != address(0)) { // Give Anyswap approval to bridge tokens LibAsset.approveERC20(IERC20(underlyingToken), _anyswapData.router, _anyswapData.amount); IAnyswapRouter(_anyswapData.router).anySwapOutUnderlying( _anyswapData.token, _anyswapData.recipient, _anyswapData.amount, _anyswapData.toChainId ); } else { // Give Anyswap approval to bridge tokens LibAsset.approveERC20(IERC20(_anyswapData.token), _anyswapData.router, _anyswapData.amount); IAnyswapRouter(_anyswapData.router).anySwapOut( _anyswapData.token, _anyswapData.recipient, _anyswapData.amount, _anyswapData.toChainId ); } } }

Imagine a scenario where there are currently some ether in the Diamond contract (or we can trigger come bridges to send ether to the contract either due to timeout/expiry or by simply fulfilling a swap). Our target is to siphon those ether by depositing USDT on a 1:1 basis.

Our argument setup is approximately like this, we choose a _anyswapData.token that has USDT as IAnyswapToken(_anyswapData.token).underlying(), then design a malicious _anyswapData.router that has IAnyswapRouter(_anyswapData.router).wNATIVE() return address(0) on first call and address(USDT) on second call. We also set _anyswapData.amount to be equal to current address(Diamond).balance.

Now we can trace what will happen throughout the process.

  1. underlyingToken will be set to USDT
  2. since underlyingToken != wNATIVE (address(USDT) != address(0)), _anyswapData.amount of USDT will be transferred from caller(attacker/us) to Diamond
  3. pass checks and proceed to enter _startBridge
  4. The second call of wNATIVE returns address(USDT), so now underlyingToken == wNATIVE
  5. IAnyswapRouter(_anyswapData.router).anySwapOutNative{ value: _anyswapData.amount }(...) is called, we now can walk away with _anyswapData.amount of ether

The exploit is pretty simple, but we still need to deal with how to have the fake router return different wNATIVE for the first and second call. If we assume that the call is done with call opcode, it is pretty easy to just keep an internal storage in the fake router contract to decide on what value to return. However, if staticcall is used, we can no longer change storage value within the fake router. Thankfully under this case, it is still possible to utilize remaining gas to decide when to switch from returning address(0) to address(USDT).

The attack can also be adapted to stealing tokens by depositing ether, under cases where valuable tokens such as WBTC are held by the Diamond contract. In fact, when used together with the LibAsset.approveERC20 function which approves UINT256_MAX (instead of amount specified in argument) to the spender, it is possible specify a 0 _anyswapData.amount and proceed to transfer all tokens from Diamond to attacker.

Overall, this lack of check on router fully compromises any asset stored in the Diamond contract.

Tools Used

vim, ganache-cli

While one of the strengths of anySwap is its flexibility, we have shown in this context that flexibility equates vulnerability. The easiest way to fix this is to adopt a whitelist where Diamond contract owners may add/remove trusted routers.

#0 - H3xept

2022-04-11T08:17:33Z

Duplicate of #12

#1 - gzeoneth

2022-04-16T17:53:59Z

Duplicate of #117

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
duplicate
2 (Med Risk)
disagree with severity
sponsor acknowledged

Awards

77.3842 USDC - $77.38

External Links

Lines of code

https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/Swapper.sol#L12 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Libraries/LibSwap.sol#L33

Vulnerability details

Impact

LibSwap.swap only transfers non-native tokens from msg.sender when balance of said token is lesser than requested fromAmont, thus if there are any remaining tokens in the contract, attacker can easily utilize those tokens for swapping and in turn claim ownership of those

Proof of Concept

The following exploit is demonstrated using GenericSwapFacet. But the vulnerability can be triggered from almost every faucet that uses _executeSwaps.

  • Assume that the WETH balance of the contract is 100 WETH
  • Alice call GenericSwapFacet.swapTokensGeneric and _lifiData.receivingAssetId is set to native asset (can be set to any asset except WETH)

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

function swapTokensGeneric(LiFiData memory _lifiData, LibSwap.SwapData[] calldata _swapData) public payable { uint256 receivingAssetIdBalance = LibAsset.getOwnBalance(_lifiData.receivingAssetId); // Swap _executeSwaps(_lifiData, _swapData); uint256 postSwapBalance = LibAsset.getOwnBalance(_lifiData.receivingAssetId) - receivingAssetIdBalance; LibAsset.transferAsset(_lifiData.receivingAssetId, payable(msg.sender), postSwapBalance); ... }
  • Alice uses WETH as sendingAsset. And the fromAmount is 100. Since LibAsset.getOwnBalance(fromAssetId) < fromAmount, no WETH will be collected from Alice
  • The contract swap out 100 WETH to DEX, and swap in some native assets. Then it will send the native assets received DEX to Alice.
  • Obviously, Alice paid nothing(or only gas) to get the assets.

https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Libraries/LibSwap.sol#L42

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

Tools Used

Manual code review.

Before doing _executeSwaps. the contract should track the preBalance.

And use

prebalance - LibAsset.getOwnBalance(fromAssetId) < fromAmount

instead of

LibAsset.getOwnBalance(fromAssetId) < fromAmount

#0 - H3xept

2022-04-12T08:38:51Z

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

Findings Information

🌟 Selected for report: kirk-baird

Also found by: TomFrenchBlockchain, VAD37, WatchPug, hyh, rayn, wuwe1

Labels

bug
duplicate
2 (Med Risk)

Awards

385.2169 USDC - $385.22

External Links

Lines of code

https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/Swapper.sol#L12 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Libraries/LibSwap.sol#L42

Vulnerability details

Impact

_executeSwaps is always wrapped in a loop, however, it uses msg.value to decide the amount of native tokens to use without any additional checks. Attackers can easily drain the existing funds by repeatedly swapping native asset.

Proof of Concept

The following exploit is demonstrated using GenericSwapFacet. But the vulnerability can be triggered from every faucet that uses _executeSwaps.

  • Assume that the native asset balance of the contract is 100eth
  • Alice calls GenericSwapFacet.swapTokensGeneric with msg.value = 100eth

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

function swapTokensGeneric(LiFiData memory _lifiData, LibSwap.SwapData[] calldata _swapData) public payable { uint256 receivingAssetIdBalance = LibAsset.getOwnBalance(_lifiData.receivingAssetId); // Swap _executeSwaps(_lifiData, _swapData); uint256 postSwapBalance = LibAsset.getOwnBalance(_lifiData.receivingAssetId) - receivingAssetIdBalance; LibAsset.transferAsset(_lifiData.receivingAssetId, payable(msg.sender), postSwapBalance); ... }
  • The _swapData array sent by Alice has two SwapData. So it calls LibSwap.swap twice.

https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/Swapper.sol#L12

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]); } }
  • Alice uses native asset as sendingAsset in both swap data. So LibSwap.swap won't transfer any more native asset from Alice.
  • _swapData.callTo.call{ value: msg.value }(_swapData.callData) is executed twice. The contract actually send 200 eth to DEX. But only receive 100 eth from Alice.

https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Libraries/LibSwap.sol#L42

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

Tools Used

Manual code review.

When sending native asset to DEX in LibSwap.swap, the contract should do bookkeeping on the actual used native asset and received native asset. A common way to do this is to extract msg.value into a local variable, and subtract from the said variable every time when native asset is used. Checks should also be enforced against this local variable instead of the raw msg.value.

#0 - H3xept

2022-04-11T09:21:05Z

Duplicate of #86

Findings Information

🌟 Selected for report: hickuphh3

Also found by: WatchPug, hyh, rayn, shw, wuwe1

Labels

bug
duplicate
2 (Med Risk)

Awards

499.3553 USDC - $499.36

External Links

Lines of code

https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/CBridgeFacet.sol#L150

Vulnerability details

Impact

When sendNative() is called, cbridge expects _amount of native tokens to be sent to it. However, CBridgeFacet does not send any native asset, resulting in a certain revert.

Proof of Concept

In CBridgeFacet._startBridge, ICBridge(bridge).sendNative() doesn't have value set. Thus no native asset will be sent to the bridge.

https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/CBridgeFacet.sol#L150

function _startBridge(CBridgeData memory _cBridgeData) internal { Storage storage s = getStorage(); address bridge = _bridge(); // Do CBridge stuff require(s.cBridgeChainId != _cBridgeData.dstChainId, "Cannot bridge to the same network."); if (LibAsset.isNativeAsset(_cBridgeData.token)) { ICBridge(bridge).sendNative( _cBridgeData.receiver, _cBridgeData.amount, _cBridgeData.dstChainId, _cBridgeData.nonce, _cBridgeData.maxSlippage ); } else { // Give CBridge approval to bridge tokens LibAsset.approveERC20(IERC20(_cBridgeData.token), bridge, _cBridgeData.amount); // solhint-disable check-send-result ICBridge(bridge).send( _cBridgeData.receiver, _cBridgeData.token, _cBridgeData.amount, _cBridgeData.dstChainId, _cBridgeData.nonce, _cBridgeData.maxSlippage ); } }

Tools Used

Manual code review.

Set value

ICBridge(bridge).sendNative{ value: msg.value }( _cBridgeData.receiver, _cBridgeData.amount, _cBridgeData.dstChainId, _cBridgeData.nonce, _cBridgeData.maxSlippage );

#0 - H3xept

2022-04-11T11:00:10Z

Duplicate of #35

#1 - gzeoneth

2022-04-16T16:25:32Z

Changing to Med Risk as no fund is lost.

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

https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/CBridgeFacet.sol#L57 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/CBridgeFacet.sol#L92 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/CBridgeFacet.sol#L159

Vulnerability details

Impact

A malicious _cBridgeData.token can be crafted to re-enter facet. While no immediate attack vectors are found, preventive measures are suggested to fully close off such attack chances.

Proof of Concept

In _startBridge, LibAsset.approveERC20(IERC20(_cBridgeData.token), ...) is called on potentially malicious _cBridgeData.token. Since approve will be called with call opcode, it is possible to utilize this chance to re-enter facet.

We haven't identified any potential attack vectors except withdrawing previously transferred _cBridgeData.token, which is pretty much next to useless considering we already have full control over _cBridgeData.token. But generally, reducing the attack surface, especially for something as dangerous as reentrancy is a good thing to do.

Tools Used

vim, ganache-cli

Validate _cBridgeData.token if possible. If this is not desired due to reduced flexibility of facet, add nonReentrant modifier to public/external functions instead.

#0 - H3xept

2022-04-11T12:26:11Z

Duplicate of #109

Findings Information

🌟 Selected for report: WatchPug

Also found by: VAD37, catchup, csanuragjain, rayn

Labels

bug
duplicate
2 (Med Risk)
disagree with severity
sponsor acknowledged

Awards

665.807 USDC - $665.81

External Links

Lines of code

https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Libraries/LibSwap.sol#L38 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Libraries/LibAsset.sol#L68

Vulnerability details

Impact

In LibAsset.approveERC20, It approves MAX_INT instead of amount. It allows any potentially malicious/exploited bridge/dex to wreck havock and steal all said token instead of limiting attacks to only the explicitly approved amount. Attacks due to inappropriate approval have happened before, and there is no reason to believe that it is impossible to happen in the future.

Proof of Concept

  • In LibSwap.swap, it seems like LibAsset.approveERC20(IERC20(fromAssetId), _swapData.approveTo, fromAmount) should approve fromAmount.

https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Libraries/LibSwap.sol#L38

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); } ... }
  • However, LibAsset.approveERC20 approves MAX_INT.
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); } }
  • fromAmount is not used in _swapData.callTo.call and the contract approves MAX_INT. Thus it can transfer more assets than fromAmount.
function swap(bytes32 transactionId, SwapData calldata _swapData) internal { ... if (!LibAsset.isNativeAsset(fromAssetId)) { LibAsset.approveERC20(IERC20(fromAssetId), _swapData.approveTo, fromAmount); } ... (bool success, bytes memory res) = _swapData.callTo.call{ value: msg.value }(_swapData.callData); ... }

Tools Used

Manual code review.

Use

SafeERC20.safeApprove(IERC20(assetId), spender, amount);

instead of

SafeERC20.safeApprove(IERC20(assetId), spender, MAX_INT);

#0 - H3xept

2022-04-12T09:56:17Z

We are aware of unlimited approval given to swaps and bridges. This has been done on purpose to avoid extra fees to be paid by users for each transaction. This does not constitute an asset loss problem 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 - gzeoneth

2022-04-16T17:01:38Z

Duplicate of #160

Summary

We list 5 low-critical findings and 1 non-critical finding here:

  • (LOW) AnyswapFacet implicitly rejects address(0) as _anyswapData.token and reverts without warning
  • (LOW) CBridgeFacet allows more than _cBridgeData.amount of native assets to be sent to it
  • (LOW) Anyswap router version confusion
  • (LOW) NXTPFacet does not check whether _nxtpData.invariantData.sendingChainId equals current chainId
  • (LOW) NXTPFacet does not check _nxtpData.invariantData.callTo and _nxtpData.invariantData.callDataHash equals current chainid
  • (Non-Critical) initializeDiamondCut implementation disagrees with EIP-2535

In summary of recommended security practices, it's better to check native assets, chainId and 3rd party bridge versions to avoid abuse. Also, it’s better to follow EIP-2535 proposal and check the exact amount for better practices.

(LOW) AnyswapFacet implicitly rejects address(0) as _anyswapData.token and reverts without warning

Impact

AnyswapFacet has a different interface for accepting native assets compared to other facets. This might lead to confusion and misuse of the facet, and result in loss of gas for contract users.

The logic of deciding which token to use in Anyswap is more complex than other facets. Overall, the decision is done twice, first for checking (and transferring funds) from the user, and then for deciding which Anyswap router function to call.

logic for startBridgeTokensViaAnyswap

  1. if _anyswapData.token == address(0), native asset is used
  2. if _anyswapData.token.underlying() == _anyswapData.router.wNATIVE(), native asset is used
  3. if _anyswapData.token.underlying() == address(0), _anyswapData.token is used
  4. if _anyswapData.token.underlying() is used

logic for _startBridge

  1. if _anyswap.token.underlying() == _anyswapData.router.wNATIVE(), call anySwapOutNative()
  2. if _anyswapData.token == address(0), do nothing (note that this path would revert upon calling IAnyswapToken(_anyswapData.token).underlying() in _startBridge
  3. if _anyswapData.token.underlying() == address(0), call anySwapOut()
  4. call anySwapOutUnderlying()

The main mismatch rises when _anyswapData.token == address(0), startBridgeTokensViaAnyswap accepts those cases and uses native asset, while _startBridge does not handle this and implicitly reverts.

Since other facets usually use address(0) as native asset, we judged that users might be confused by the behaviour of AnyswapFacet, especially since it does not reject address(0) early in call, and only reverts at a relatively late stage.

Proof of Concept

https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L35 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L37 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L49 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L74 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L80 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L95 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L131 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L136 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L145

It would be more appropriate to follow the same pattern as other facets, and add code to handle address(0) in _startBridge. Or at the very least, disallow address(0) early in startBridgeTokensViaAnyswap and swapAndStartBridgeTokensViaAnyswap and make it explicit that address(0) as token is not allowed.

(LOW) CBridgeFacet allows more than _cBridgeData.amount of native assets to be sent to it

Impact

Allowing excessive native assets to be sent is unnecessary, and would potentially lead to loss of the caller's asset.

Proof of Concept

https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/CBridgeFacet.sol#L68

Use == rather than >=

require(msg.value == _cBridgeData.amount, "ERR_INVALID_AMOUNT");

(LOW) Anyswap router version confusion

Impact

Anyswap router used in deployment/test is not compatible with AnyswapFacet function calls.

The provided Anyswap router address in test/facet/AnyswapFacet.test.ts and config/anyswap.ts are both implementing AnyswapV4Router. However, anySwapOutNative is only implemented in later versions of Anyswap router (e.g. AnyswapV6Router). While this is not strictly included in audit scope, we deem it appropriate to bring attention to the mismatch, in case of incorrect deployment in the future.

Proof of Concept

https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/AnyswapFacet.sol#L137 https://github.com/code-423n4/2022-03-lifinance/blob/main/config/anyswap.ts#L14 https://github.com/code-423n4/2022-03-lifinance/blob/main/test/facets/AnyswapFacet.test.ts#L14

Use a newer Anyswap router address in deployment and test script instead.

(LOW) NXTPFacet does not check whether _nxtpData.invariantData.sendingChainId equals current chainId

Impact

Diamond might be abused to serve as router and call prepare on the receiving chain.

In the nxtp protocol, prepare is called by the sender in the sending chain and by router in the receiving chain. Diamond only intended to serve as the sender, but since it does not check _nxtpData.invariantData.sendingChainId against the current chainId, it is possible to abuse the field and call prepare as a router instead.

While the above abuse is theoretically possible, there are several checks in the nxtp protocol that must be satisfied in order to perform such actions. Including getting Diamond approved as a valid router, making bids in place of Diamond and so on. Overall, the trick is quite difficult to pull off.

Proof of Concept

https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/NXTPFacet.sol#L46 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/NXTPFacet.sol#L85 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/NXTPFacet.sol#L185

Validate _nxtpData.invariantData.sendingChainId against chainId.

(LOW) NXTPFacet does not check _nxtpData.invariantData.callTo and _nxtpData.invariantData.callDataHash equals current chainid

Impact

Intended fulfill call back can be skipped or replaced with other functions.

The design of the NXTPFacet is that the two fulfill functions will be called on the receiving chain. However, there are no checks against whether the two helper functions completeBridgeTokensViaNXTP and swapAndCompleteBridgeTokensViaNXTP are passed into as calldata, and whether Diamond is even assigned as callTo. This allows users to assign arbitrary callbacks on the receiving chain.

While this does not pose any direct threats, it would be appropriate to check the two calling fields and conform to contract usage design.

Proof of Concept

https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/NXTPFacet.sol#L46 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/NXTPFacet.sol#L85 https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Facets/NXTPFacet.sol#L185

Validate _nxtpData.invariantData.callTo and _nxtpData.invariantData.callDataHash against the intended callback functions.

(Non-Critical) initializeDiamondCut implementation disagrees with EIP-2535

Impact

The original proposal of Diamond specifies that _calldata is allowed to carry additional information even when _init is address(0). Current implementation disagrees with the proposal.

Proof of Concept

https://github.com/code-423n4/2022-03-lifinance/blob/main/src/Libraries/LibDiamond.sol#L185

Refer to EIP-2535: https://eips.ethereum.org/EIPS/eip-2535

If the _init value is address(0) then _calldata execution is skipped. In this case _calldata can contain 0 bytes or custom information.

#0 - H3xept

2022-04-07T13:30:39Z

Re anyswap check for 0x0

Fixed in lifinance/lifi-contracts@f35ed79a266a69b363d72332b7861d15d18b98cb

#1 - H3xept

2022-04-07T13:32:33Z

CBridgeFacet amount

Fixed in lifinance/lifi-contracts@a8d6336c2ded97bdbca65b64157596b33f18f70d

#2 - H3xept

2022-04-07T13:52:21Z

Re Anyswap router mismatch

I'm reasonably certain AnyswapRouterV4 does implement anySwapNative as it inherits from V3 and the function is present there (0x332730a4F6E03D9C55829435f10360E13cfA41Ff)

Awards

61.5429 USDC - $61.54

Labels

bug
G (Gas Optimization)
resolved

External Links

#0 - H3xept

2022-04-07T14:58:25Z

We internally decided to avoid unchecked statements for now.

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