sherlock-audit / 2024-02-jala-swap-judging

6 stars 4 forks source link

jasonxiale - user will receive less token if `swap path` contains more than one token whose decimal is 0 #212

Closed sherlock-admin closed 6 months ago

sherlock-admin commented 6 months ago

jasonxiale

medium

user will receive less token if swap path contains more than one token whose decimal is 0

Summary

For current implementation, while creating token pair in JalaFactory.createPair, the function doesn't check if the token's decimal is larger than 0. So a token pair contains 0-decimal token can be created. If the swap path in JalaRouter02.swapXXX contains two tokens those use 0 as decimal, the output token will be less than expected.

Vulnerability Detail

Take JalaRouter02.swapExactTokensForTokens as an example. Function JalaRouter02.swapExactTokensForTokens calls JalaLibrary.getAmountsOut to calculate the amount of swapped token for each token-pair. Within JalaLibrary.getAmountsOut, JalaLibrary.getAmountOut is used to calculate the amount of output token based on input token.

 70     // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
 71     function getAmountOut(
 72         uint256 amountIn,
 73         uint256 reserveIn,
 74         uint256 reserveOut
 75     ) internal pure returns (uint256 amountOut) {
 76         if (amountIn == 0) revert InsufficientInputAmount();
 77         if (reserveIn == 0 || reserveOut == 0) revert InsufficientLiquidity();
 78         uint256 amountInWithFee = amountIn * 997;
 79         uint256 denominator = (reserveIn * 1000) + amountInWithFee;
 80         uint256 numerator = amountInWithFee * reserveOut;
 81         amountOut = numerator / denominator;
 82     }

As shown between JalaLibrary.sol#L78-L81, because the 0.3% fee and roundDown, amountOut for 0-decimal token will be less than expected. In the following test, the ratio for tokenA and tokenB is 1:1, but we can use 3 wei tokenA to swap 2 wei tokenB only.

For the POC, create a new file JalaRouter02_0.t.sol under test folder and run forge test --mc JalaRouter02Dec_Test --mt test_SwapExactTokensForZeroDecTokens -vv

// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.10;

import "forge-std/Test.sol";
import "../contracts/JalaFactory.sol";
import "../contracts/JalaPair.sol";
import "../contracts/JalaRouter02.sol";
import "../contracts/interfaces/IJalaRouter02.sol";
import "../contracts/mocks/ERC20Mintable_decimal.sol";
import {console2} from "forge-std/console2.sol";

contract JalaRouter02Dec_Test is Test {
    address feeSetter = address(69);
    ERC20Mintable WETH;

    JalaRouter02 router;
    JalaFactory factory;

    ERC20Mintable tokenA;
    ERC20Mintable tokenB;
    ERC20Mintable tokenC;

    function setUp() public {
        WETH = new ERC20Mintable("Wrapped ETH", "WETH", 18);

        factory = new JalaFactory(feeSetter);
        router = new JalaRouter02(address(factory), address(WETH));

        tokenA = new ERC20Mintable("Token A", "TKNA", 0);
        tokenB = new ERC20Mintable("Token B", "TKNB", 0);
        tokenC = new ERC20Mintable("Token C", "TKNC", 0);

        tokenA.mint(200000, address(this));
        tokenB.mint(200000, address(this));
        tokenC.mint(200000, address(this));
    }

    function encodeError(string memory error) internal pure returns (bytes memory encoded) {
        encoded = abi.encodeWithSignature(error);
    }

    function test_SwapExactTokensForZeroDecTokens() public {

        uint256 bigNum = 10000;
        tokenA.approve(address(router), bigNum);
        tokenB.approve(address(router), bigNum);

        router.addLiquidity(
            address(tokenA),
            address(tokenB),
            bigNum,
            bigNum,
            bigNum,
            bigNum,
            address(this),
            block.timestamp
        );

        address[] memory path = new address[](2);
        path[0] = address(tokenA);
        path[1] = address(tokenB);

        console2.log("tokenA.balanceOf(this)                            :", tokenA.balanceOf(address(this)));
        console2.log("tokenB.balanceOf(this)                            :", tokenB.balanceOf(address(this)));

        uint cnt = 3;
        tokenA.approve(address(router), bigNum);
        tokenB.approve(address(router), bigNum);

        uint256[] memory amounts = router.swapExactTokensForTokens(cnt, 0, path, address(this), block.timestamp);
        console2.log("amounts[0]                                        :", amounts[0]);
        console2.log("amounts[1]                                        :", amounts[1]);

        console2.log("tokenA.balanceOf(this)                            :", tokenA.balanceOf(address(this)));
        console2.log("tokenB.balanceOf(this)                            :", tokenB.balanceOf(address(this)));

        path[0] = address(tokenB);
        path[1] = address(tokenA);
        cnt = 2;
        amounts = router.swapExactTokensForTokens(cnt, 0, path, address(this), block.timestamp);
        console2.log("amounts[0]                                        :", amounts[0]);
        console2.log("amounts[1]                                        :", amounts[1]);

        console2.log("tokenA.balanceOf(this)                            :", tokenA.balanceOf(address(this)));
        console2.log("tokenB.balanceOf(this)                            :", tokenB.balanceOf(address(this)));
    }
}

Impact

user will receive less token if swap path contains more than one token whose decimal is 0

Code Snippet

https://github.com/sherlock-audit/2024-02-jala-swap/blob/030d3ed54214754301154bce0e58ea534100a7e3/jalaswap-dex-contract/contracts/libraries/JalaLibrary.sol#L71-L82

Tool used

Manual Review

Recommendation

nevillehuang commented 6 months ago

Invalid, pools hold wrapped tokens with only 18 decimals due to 0 decimal off set being 18. Pools are not expected to support 0 decimal tokens.