Trader Joe v2 contest - rbserver'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: 20/75

Findings: 2

Award: $281.07

QA:
grade-b

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: rbserver

Also found by: 8olidity, ElKu, Rahoz, TomJ, Trust, cccz, d3e4, hyh, lukris02, m_Rassska, neumo, pashov, vv7

Labels

bug
2 (Med Risk)
disagree with severity
primary issue
sponsor confirmed
selected for report
M-06

Awards

1.2646 USDC - $1.26

External Links

Lines of code

https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBRouter.sol#L485-L521

Vulnerability details

Impact

When calling the swapAVAXForExactTokens function, if (msg.value > amountsIn[0]) _safeTransferAVAX(_to, amountsIn[0] - msg.value) is executed, which is for refunding any excess amount sent in; this is confirmed by this function's comment as well. However, executing amountsIn[0] - msg.value will always revert when msg.value > amountsIn[0] is true. Developers who has the design of the swapAVAXForExactTokens function in mind could develop front-ends and contracts that will send excess amount when calling the swapAVAXForExactTokens function. Hence, the users, who rely on these front-ends and contracts for interacting with the swapAVAXForExactTokens function will always find such interactions being failed since calling this function with the excess amount will always revert. As a result, the user experience becomes degraded, and the usability of the protocol becomes limited.

https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBRouter.sol#L485-L521

    /// @notice Swaps AVAX for exact tokens while performing safety checks
    /// @dev will refund any excess sent
    ...
    function swapAVAXForExactTokens(
        uint256 _amountOut,
        uint256[] memory _pairBinSteps,
        IERC20[] memory _tokenPath,
        address _to,
        uint256 _deadline
    )
        external
        payable
        override
        ensure(_deadline)
        verifyInputs(_pairBinSteps, _tokenPath)
        returns (uint256[] memory amountsIn)
    {
        ...

        if (msg.value > amountsIn[0]) _safeTransferAVAX(_to, amountsIn[0] - msg.value);
    }

Proof of Concept

Please add the following test in test\LBRouter.Swaps.t.sol. This test will pass to demonstrate the described scenario.

    function testSwapAVAXForExactTokensIsUnableToRefund() public {
        uint256 amountOut = 1e18;

        (uint256 amountIn, ) = router.getSwapIn(pairWavax, amountOut, false);

        IERC20[] memory tokenList = new IERC20[](2);
        tokenList[0] = wavax;
        tokenList[1] = token6D;
        uint256[] memory pairVersions = new uint256[](1);
        pairVersions[0] = DEFAULT_BIN_STEP;

        vm.deal(DEV, amountIn + 500);

        // Although the swapAVAXForExactTokens function supposes to refund any excess sent,
        //   calling it reverts when sending more than amountIn
        //   because executing _safeTransferAVAX(_to, amountsIn[0] - msg.value) results in arithmetic underflow
        vm.expectRevert(stdError.arithmeticError);
        router.swapAVAXForExactTokens{value: amountIn + 1}(amountOut, pairVersions, tokenList, DEV, block.timestamp);
    }

Tools Used

VSCode

https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBRouter.sol#L520 can be updated to the following code.

        if (msg.value > amountsIn[0]) _safeTransferAVAX(_to, msg.value - amountsIn[0]);

#0 - Shungy

2022-10-24T06:00:56Z

I believe this finding to be valid.

The issue exists in this line: https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBRouter.sol#L520

Also “Sending excess amount” would be happening regardless of the interface, due to changing price. So this issue effects anyone using this router.

Since this would only cause revert issues in a contract that can be easily replaced, I believe a severity low or medium to be appropriate. For example this was submitted as Low by @trust1995 : https://github.com/code-423n4/2022-10-traderjoe-findings/issues/418

#1 - GalloDaSballo

2022-10-26T18:26:21Z

Best submission because has coded POC, and is brief and clear

#2 - c4-judge

2022-11-10T15:41:10Z

GalloDaSballo marked the issue as primary issue

#3 - GalloDaSballo

2022-11-13T19:53:29Z

The finding is valid, sending more than necessary will revert. No loss to end-users was shown, and the revert will prevent the tx from finishing.

Because of that, considering the fact that a accurate measure could be calculated, meaning that functionality can be side-stepped, despite recommending fixing, I think the finding is QA - Low Severity as the revert is self-inflicted

#4 - GalloDaSballo

2022-11-13T19:53:31Z

L

#5 - c4-judge

2022-11-13T19:53:38Z

#6 - GalloDaSballo

2022-11-20T22:28:56Z

Per discussion from backstage I've re-reviewed the finding.

I have poked around with the test provided and tweaked it a little, this is the last one I have

function testSwapAVAXForExactTokensIsUnableToRefund() public {
        uint256 amountOut = 1e18;

        (uint256 amountIn, ) = router.getSwapIn(pairWavax, amountOut, false);

        IERC20[] memory tokenList = new IERC20[](2);
        tokenList[0] = wavax;
        tokenList[1] = token6D;
        uint256[] memory pairVersions = new uint256[](1);
        pairVersions[0] = DEFAULT_BIN_STEP;

        vm.deal(DEV, 50 * amountIn + 500);


        // Although the swapAVAXForExactTokens function supposes to refund any excess sent,
        //   calling it reverts when sending more than amountIn
        //   because executing _safeTransferAVAX(_to, amountsIn[0] - msg.value) results in arithmetic underflow
        vm.expectRevert(stdError.arithmeticError);
        router.swapAVAXForExactTokens{value: amountIn + 1}(amountOut, pairVersions, tokenList, DEV, block.timestamp);

        router.swapAVAXForExactTokens{value: amountIn}(amountOut, pairVersions, tokenList, DEV, block.timestamp);

        vm.expectRevert(stdError.arithmeticError);
        router.swapAVAXForExactTokens{value: amountIn + 100000}(amountOut, pairVersions, tokenList, DEV, block.timestamp);


    }

Ultimately we can conclude that any amount below amountIn will result in a revert due to insufficient amountOut, and any amount above amountIn will revert due to the highlighted overflow.

The function is a routing function, which is meant to allow some degree of slippage.

My original downgrade was based on the idea that the function can run, however, after it being flagged, I must agree that the availability of the function is impaired in the majority of ordinary cases (a .10% / .5% slippage is expected under most operations / FEs)

For this reason, I agree with upgrading back to Medium Severity.

I'd like to thank @shung and hyh for their feedback

#7 - Simon-Busch

2022-11-21T06:10:12Z

Reverted to M as requested by @GalloDaSballo

#9 - c4-judge

2022-11-23T21:05:02Z

GalloDaSballo marked the issue as selected for report

#10 - Simon-Busch

2022-12-05T06:27:27Z

Marked this issue as Satisfactory as requested by @GalloDaSballo

#11 - Simon-Busch

2022-12-05T06:41:32Z

revert, wrong action

Findings Information

🌟 Selected for report: zzykxx

Also found by: 0x1f8b, 0xSmartContract, IllIllI, KingNFT, Rolezn, adriro, brgltd, hansfriese, pashov, rbserver

Labels

bug
downgraded by judge
QA (Quality Assurance)
grade-b
Q-10

Awards

279.8109 USDC - $279.81

External Links

Lines of code

https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBPair.sol#L304-L413 https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/libraries/TokenHelper.sol#L40-L52 https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/libraries/TokenHelper.sol#L74-L81

Vulnerability details

Impact

When calling the swap function, tokenY.safeTransfer(_to, _amountOut) or tokenX.safeTransfer(_to, _amountOut) is executed, where the TokenHelper library's safeTransfer function is used. According to the implementation of the TokenHelper library's safeTransfer function, tokenY and tokenX's contract existences are not checked. It is possible that a token becomes non-existent in the future; for instance, if some bugs are found for the output token, it is possible that the output token will be destroyed through calling its selfdestruct function after migrating its data to another contract for fixing these bugs. When this happens, such output token becomes non-existent but calling the swap function to swap for it does not revert. As a result, the user who swaps for a non-existent output token will lose the input token amount while receiving no output token amount.

https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBPair.sol#L304-L413

    function swap(bool _swapForY, address _to)
        external
        override
        nonReentrant
        returns (uint256 amountXOut, uint256 amountYOut)
    {
        ...

        if (_swapForY) {
            amountYOut = _amountOut;
            tokenY.safeTransfer(_to, _amountOut);
        } else {
            amountXOut = _amountOut;
            tokenX.safeTransfer(_to, _amountOut);
        }
    }

https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/libraries/TokenHelper.sol#L40-L52

    function safeTransfer(
        IERC20 token,
        address recipient,
        uint256 amount
    ) internal {
        if (amount != 0) {
            (bool success, bytes memory result) = address(token).call(
                abi.encodeWithSelector(token.transfer.selector, recipient, amount)
            );

            _catchTransferError(success, result);
        }
    }

https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/libraries/TokenHelper.sol#L74-L81

    function _catchTransferError(bool success, bytes memory result) private pure {
        // Look for revert reason and bubble it up if present
        if (!(success && (result.length == 0 || abi.decode(result, (bool))))) {
            assembly {
                revert(add(32, result), mload(result))
            }
        }
    }

Proof of Concept

First, in test\mocks\, please add the following mock token contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import './ERC20.sol';

contract ERC20MockWithSelfdestruct is ERC20Mock {
  constructor() ERC20Mock("Test", "T", 18) {}

  function kill() external {
    selfdestruct(payable(msg.sender));
  }
}

Then, please add the following code in test\LBPair.Swaps.t.sol.

  1. Import ERC20MockWithSelfdestruct as follows.
import "./mocks/ERC20MockWithSelfdestruct.sol";
  1. Add the following state variables in the LiquidityBinPairSwapsTest contract.
    LBPair pair_;
    ERC20MockWithSelfdestruct erc20MockWithSelfdestruct;
    uint256 amountXIn_;
  1. Add the following code for POC purpose in the setUp function. Note this will break other tests so please only use this setUp code for this test.
    function setUp() public {
        token6D = new ERC20MockDecimals(6);
        token18D = new ERC20MockDecimals(18);

        factory = new LBFactory(DEV, 8e14);
        ILBPair _LBPairImplementation = new LBPair(factory);
        factory.setLBPairImplementation(address(_LBPairImplementation));
        setDefaultFactoryPresets(DEFAULT_BIN_STEP);
        addAllAssetsToQuoteWhitelist(factory);
        router = new LBRouter(ILBFactory(DEV), IJoeFactory(DEV), IWAVAX(DEV));

        pair = createLBPairDefaultFees(token6D, token18D);

        /** add the following code for POC purpose */

        // deploy the ERC20 mock token, which has the selfdestruct functionality
        erc20MockWithSelfdestruct = new ERC20MockWithSelfdestruct();

        factory.addQuoteAsset(erc20MockWithSelfdestruct);

        // create a pair with token6D and erc20MockWithSelfdestruct
        pair_ = createLBPairDefaultFees(token6D, erc20MockWithSelfdestruct);

        uint256 tokenAmount = 100e18;
        erc20MockWithSelfdestruct.mint(address(pair_), tokenAmount);

        uint256[] memory _ids = new uint256[](1);
        _ids[0] = ID_ONE;

        uint256[] memory _liquidities = new uint256[](1);
        _liquidities[0] = Constants.PRECISION;

        pair_.mint(_ids, new uint256[](1), _liquidities, DEV);

        amountXIn_ = 10e18;

        (uint256 amountYOut_, ) = router.getSwapOut(pair_, amountXIn_, true);

        // ALICE has amountXIn_*2 of token6D at this moment
        token6D.mint(ALICE, amountXIn_ * 2);

        // ALICE swaps amountXIn_ of token6D for erc20MockWithSelfdestruct
        vm.startPrank(ALICE);
        token6D.transfer(address(pair_), amountXIn_);
        pair_.swap(true, ALICE);
        vm.stopPrank();

        // after this swap, ALICE has amountXIn_ of token6D left and has received amountYOut_ of erc20MockWithSelfdestruct
        assertEq(token6D.balanceOf(ALICE), amountXIn_);
        assertEq(erc20MockWithSelfdestruct.balanceOf(ALICE), amountYOut_);
        
        // simulate a situation where mockERC20WithSelfdestruct is destroyed after its data is migrated to another contract for fixing some bugs
        erc20MockWithSelfdestruct.kill();

        /** */
    }
  1. Add the following test. This test will pass to demonstrate the described scenario.
    function testLoseInputTokenAndNotReceiveOutputTokenWhenOutputTokenBecomesNonExistent() public {
        // ALICE swaps amountXIn_ of token6D for erc20MockWithSelfdestruct again
        vm.startPrank(ALICE);
        token6D.transfer(address(pair_), amountXIn_);
        pair_.swap(true, ALICE);
        vm.stopPrank();

        // after ALICE's second swap, ALICE has no token6D left
        assertEq(token6D.balanceOf(ALICE), 0);

        // however, ALICE has not received any erc20MockWithSelfdestruct in return this time since erc20MockWithSelfdestruct does not exist anymore
        vm.expectRevert(bytes(""));
        erc20MockWithSelfdestruct.balanceOf(ALICE);
    }

Tools Used

VSCode

In the TokenHelper library's safeTransfer function, the existence of the contract for token should be checked before performing the low-level call. If such contract does not exist, calling this function should revert.

#0 - Minh-Trng

2022-10-23T20:35:48Z

the swap function is supposed to be called from periphery contracts that performs safety checks (such as slippage). in this case it would revert

#1 - Shungy

2022-10-24T05:43:11Z

I also find this finding to be invalid.

Agreed with @Minh-Trng .

Additionally having a malicious token in the pair is not something that can be solved by LB. You cannot stop honeypots and such either.

#2 - GalloDaSballo

2022-10-27T21:33:20Z

Neat quality but making it dup of https://github.com/code-423n4/2022-10-traderjoe-findings/issues/376 as it's briefer

#3 - GalloDaSballo

2022-11-13T19:34:55Z

L

#4 - c4-judge

2022-11-13T19:35:05Z

GalloDaSballo changed the severity to QA (Quality Assurance)

#5 - c4-judge

2022-11-16T21:07:19Z

GalloDaSballo marked the issue as grade-c

#6 - c4-judge

2022-11-16T21:08:12Z

GalloDaSballo marked the issue as grade-b

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