Trader Joe v2 contest - Lambda's results

One-stop-shop decentralized trading on Avalanche.

General Information

Platform: Code4rena

Start Date: 14/10/2022

Pot Size: $100,000 USDC

Total HM: 12

Participants: 75

Period: 9 days

Judge: GalloDaSballo

Total Solo HM: 1

Id: 171

League: ETH

Trader Joe

Findings Distribution

Researcher Performance

Rank: 2/75

Findings: 3

Award: $13,556.88

🌟 Selected for report: 2

🚀 Solo Findings: 1

Findings Information

Awards

0.3268 USDC - $0.33

Labels

bug
3 (High Risk)
satisfactory
duplicate-299

External Links

Lines of code

https://github.com/code-423n4/2022-10-traderjoe/blob/37258d595d596c195507234f795fa34e319b0a68/src/LBToken.sol#L191

Vulnerability details

Impact

LBToken._transfer caches the _fromBalance and _toBalance and then updates the _balances mapping based on these cached values. This does not work correctly when _from == _to. In this case, we have (for _from == _to == address(A):

_balances[_id][address(A)] = _fromBalance - _amount;
_balances[_id][address(A)] = _toBalance + _amount;

The first decrement of the balance is therefore simply discarded and the balance is only incremented. An attacker can double his own balance like this.

Proof Of Concept

The following diff shows a scenario where an attacker doubles his own balance by a self-transfer:

--- a/test/LBToken.t.sol
+++ b/test/LBToken.t.sol
@@ -91,6 +91,9 @@ contract LiquidityBinTokenTest is TestHelper {
         assertEq(pair.balanceOf(DEV, _ids[0]), 0);
         assertEq(pair.balanceOf(ALICE, _ids[0]), 0);
         assertEq(pair.balanceOf(BOB, _ids[0]), amountIn / 3);
+        vm.prank(BOB);
+        pair.safeTransferFrom(BOB, BOB, _ids[0], amountIn / 3);
+        assertEq(pair.balanceOf(BOB, _ids[0]), amountIn / 3 * 2);
     }
 
     function testSafeBatchTransferNotApprovedReverts() public {

Do not allow self transfers.

#0 - trust1995

2022-10-23T21:56:53Z

Dup of #422

#1 - GalloDaSballo

2022-10-26T16:35:49Z

#2 - c4-judge

2022-11-23T18:28:33Z

GalloDaSballo marked the issue as not a duplicate

#3 - c4-judge

2022-11-23T18:29:08Z

GalloDaSballo marked the issue as duplicate of #299

#4 - Simon-Busch

2022-12-05T06:39:53Z

Marked this issue as Satisfactory as requested by @GalloDaSballo

Findings Information

🌟 Selected for report: Lambda

Labels

bug
2 (Med Risk)
sponsor confirmed
selected for report
M-01

Awards

13555.2857 USDC - $13,555.29

External Links

Lines of code

https://github.com/code-423n4/2022-10-traderjoe/blob/e81b78ddb7cc17f0ece921fbaef2c2521727094b/src/LBRouter.sol#L291

Vulnerability details

Impact

LBRouter.removeLiquidity reorders tokens when the user did not pass them in the pair order (ascending order):

if (_tokenX != _LBPair.tokenX()) {
            (_tokenX, _tokenY) = (_tokenY, _tokenX);
            (_amountXMin, _amountYMin) = (_amountYMin, _amountXMin);
}

However, when returning amountX and amountY, it is ignored if the order was changed:

(amountX, amountY) = _removeLiquidity(_LBPair, _amountXMin, _amountYMin, _ids, _amounts, _to);

Therefore, when the order of the tokens is swapped by the function, the return value amountX ("Amount of token X returned") in reality is the amount of the user-provided token Y that is returned and vice versa.

Because this is an exposed function that third-party protocols / contracts will use, this can cause them to malfunction. For instance, when integrating with Trader Joe, something natural to do is:

(uint256 amountAReceived, uint256 amountBReceived) = LBRouter.removeLiquidity(address(tokenA), address(tokenB), ...); contractBalanceA += amountAReceived; contractBalanceB += amountBReceived;

This snippet will only be correct when the token addresses are passed in the right order, which should not be the case. When they are not passed in the right order, the accounting of third-party contracts will be messed up, leading to vulnerabilities / lost funds there.

Proof Of Concept

First consider the following diff, which shows a scenario when LBRouter does not switch tokenX and tokenY, resulting in correct return values:

--- a/test/LBRouter.Liquidity.t.sol
+++ b/test/LBRouter.Liquidity.t.sol
@@ -57,7 +57,9 @@ contract LiquidityBinRouterTest is TestHelper {
 
         pair.setApprovalForAll(address(router), true);
 
-        router.removeLiquidity(
+        uint256 token6BalBef = token6D.balanceOf(DEV);
+        uint256 token18BalBef = token18D.balanceOf(DEV);
+        (uint256 amountFirstRet, uint256 amountSecondRet) = router.removeLiquidity(
             token6D,
             token18D,
             DEFAULT_BIN_STEP,
@@ -70,7 +72,9 @@ contract LiquidityBinRouterTest is TestHelper {
         );
 
         assertEq(token6D.balanceOf(DEV), amountXIn);
+        assertEq(amountXIn, token6BalBef + amountFirstRet);
         assertEq(token18D.balanceOf(DEV), _amountYIn);
+        assertEq(_amountYIn, token18BalBef + amountSecondRet);
     }
 
     function testRemoveLiquidityReverseOrder() public {

This test passes (as it should). Now, consider the following diff, where LBRouter switches tokenX and tokenY:

--- a/test/LBRouter.Liquidity.t.sol
+++ b/test/LBRouter.Liquidity.t.sol
@@ -57,12 +57,14 @@ contract LiquidityBinRouterTest is TestHelper {
 
         pair.setApprovalForAll(address(router), true);
 
-        router.removeLiquidity(
-            token6D,
+        uint256 token6BalBef = token6D.balanceOf(DEV);
+        uint256 token18BalBef = token18D.balanceOf(DEV);
+        (uint256 amountFirstRet, uint256 amountSecondRet) = router.removeLiquidity(
             token18D,
+            token6D,
             DEFAULT_BIN_STEP,
-            totalXbalance,
             totalYBalance,
+            totalXbalance,
             ids,
             amounts,
             DEV,
@@ -70,7 +72,9 @@ contract LiquidityBinRouterTest is TestHelper {
         );
 
         assertEq(token6D.balanceOf(DEV), amountXIn);
+        assertEq(amountXIn, token6BalBef + amountSecondRet);
         assertEq(token18D.balanceOf(DEV), _amountYIn);
+        assertEq(_amountYIn, token18BalBef + amountFirstRet);
     }
 
     function testRemoveLiquidityReverseOrder() public {

This test should also pass (the order of the tokens was only switched), but it does not because the return values are mixed up.

Add the following statement in the end:

if (_tokenX != _LBPair.tokenX()) {
	return (amountY, amountX);
}

#0 - GalloDaSballo

2022-11-14T14:18:33Z

The Warden has shown how, due to an inconsistent re-ordering, the removeLiqudity function can return incorrect (swapped) amounts.

While invariants are not broken, this is an example of an incorrect function behaviour.

For this reason, despite no loss of value, I believe Medium Severity to be appropriate as the potential impact warrants an increased severity.

#1 - c4-judge

2022-11-14T14:18:40Z

GalloDaSballo marked the issue as selected for report

Findings Information

Labels

bug
2 (Med Risk)
primary issue
sponsor confirmed
selected for report
M-02

Awards

1.2646 USDC - $1.26

External Links

Lines of code

https://github.com/code-423n4/2022-10-traderjoe/blob/37258d595d596c195507234f795fa34e319b0a68/src/LBToken.sol#L237

Vulnerability details

Impact

In LBToken._burn, the _beforeTokenTransfer hook is called with from = address(0) and to = _account:

_beforeTokenTransfer(address(0), _account, _id, _amount);

Through a lucky coincidence, it turns out that this in the current setup does not cause a high severity issue. _burn is always called with _account = address(this), which means that LBPair._beforeTokenTransfer is a NOP. However, this wrong call is very dangerous for future extensions or protocol that built on top of the protocol / fork it.

Proof Of Concept

Let's say the protocol is extended with some logic that needs to track mints / burns. The canonical way to do this would be:

function _beforeTokenTransfer(
        address _from,
        address _to,
        uint256 _id,
        uint256 _amount
    ) internal override(LBToken) {
	if (_from == address(0)) {
		// Mint Logic
	} else if (_to == address(0)) {
		// Burn Logic
	}
}

Such an extension would break, which could lead to loss of funds or a bricked system.

Call the hook correctly:

_beforeTokenTransfer(_account, address(0), _id, _amount);

#0 - GalloDaSballo

2022-11-11T21:21:28Z

The warden has shown how, due to a typo / programming mistake, the hook for burning tokens is called with incorrect parameters.

Because of the caller being the pair, this ends up having reduced impact.

#1 - c4-judge

2022-11-11T21:21:38Z

GalloDaSballo marked the issue as primary issue

#2 - c4-judge

2022-11-11T21:21:45Z

GalloDaSballo marked the issue as selected for report

#3 - Simon-Busch

2022-12-05T06:26:54Z

Marked this issue as Satisfactory as requested by @GalloDaSballo

#4 - Simon-Busch

2022-12-05T06:34:56Z

Revert, wrong action

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