GithubHelp home page GithubHelp logo

2022-10-rage-trade-judging's People

Contributors

evert0x avatar hrishibhat avatar rcstanciu avatar sherlock-admin avatar

Stargazers

 avatar

Watchers

 avatar

2022-10-rage-trade-judging's Issues

rvierdiiev - Share price manipulation by first depositor is possible on DnGmxJuniorVault and DnGmxSeniorVault

rvierdiiev

medium

Share price manipulation by first depositor is possible on DnGmxJuniorVault and DnGmxSeniorVault

Summary

Share price manipulation by first depositor is possible on DnGmxJuniorVault and DnGmxSeniorVault. As result next depositors might lose part of their deposited assets, while attacker will get bigger amount of assets than he deposited.

Vulnerability Detail

DnGmxSeniorVault is created and no one deposited yet.
Alice buys first share for minimum amount of USDC using DnGmxSeniorVault.deposit function. Price of 1 share becomes 1.
Then Alice donates a big amount aliceAmount of aave aUSDC token to DnGmxSeniorVault directly(simple ERC20 transfer). Now we have 1 amount of shares and aliceAmount + 1 of deposited asset controlled by DnGmxSeniorVault. This is how totalAssets are checked.
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L370-L373

    function totalAssets() public view override(IERC4626, ERC4626Upgradeable) returns (uint256 amount) {
        amount = aUsdc.balanceOf(address(this));
        amount += totalUsdcBorrowed();
    }

Then Bob deposits arbitrary amount of assets, that is bobAmount > aliceAmount.
As result Bob receives bobAmount / (aliceAmount + 1) shares because of rounding here. Bob loses part of bobAmount % aliceAmount sent to the vault, alice controls more assets in vault now.

Impact

Next depositors can lost their money, while first user will take all of them or some part.

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

Add limit for the first deposit to be a big amount.

Duplicate of #37

cccz - WithdrawPeriphery: withdrawToken/redeemToken allows users to withdraw other users' approved shares

cccz

high

WithdrawPeriphery: withdrawToken/redeemToken allows users to withdraw other users' approved shares

Summary

WithdrawPeriphery contract's withdrawToken/redeemToken functions allow users to use other users' addresses as from, which allows malicious users to use shares that other users have approved to WithdrawPeriphery to withdraw assets.

Vulnerability Detail

Before using the withdrawToken/redeemToken function of the WithdrawPeriphery contract, the user is required to approve the share of dnGmxJuniorVault to the WithdrawPeriphery contract, but in the withdrawToken/redeemToken function, there is no requirement that from == msg.sender, which allows malicious users to use shares approved by other users to withdraw assets.
Consider the following scenario,
User A wants to withdraw assets using the WithdrawPeriphery.withdrawToken function, he approves 1000 shares to the WithdrawPeriphery contract.
User B observes the call of the dnGmxJuniorVault.approve function, and user B calls the WithdrawPeriphery.withdrawToken function, where from is user A and receiver is user B.
Eventually, User B withdraws the asset using User A's 1000 shares.

Impact

malicious users can use shares approved by other users to withdraw assets.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/periphery/WithdrawPeriphery.sol#L113-L145

Tool used

Manual Review

Recommendation

Consider requiring from == msg.sender in the withdrawToken/redeemToken function of the WithdrawPeriphery contract

Duplicate of #79

8olidity - `slippageThresholdGmxBps` has no limit and can be larger than `MAX_BPS`, causing the `executeBatchStake()` to fail

8olidity

medium

slippageThresholdGmxBps has no limit and can be larger than MAX_BPS, causing the executeBatchStake() to fail

Summary

slippageThresholdGmxBps has no limit and can be larger than MAX_BPS, causing the executeBatchStake() to fail

Vulnerability Detail

There is no limit to how much slippageThresholdGmxBps can be set

    function setThresholds(uint256 _slippageThresholdGmxBps) external onlyOwner {
        slippageThresholdGmxBps = _slippageThresholdGmxBps;
        emit ThresholdsUpdated(_slippageThresholdGmxBps);
    }

If the slippageThresholdGmxBps setting is larger than MAX_BPS, an overflow error will occur and the executeBatchStake() will not run

    function _executeVaultUserBatchStake() internal {
        uint256 _roundUsdcBalance = vaultBatchingState.roundUsdcBalance;

        if (_roundUsdcBalance == 0) revert NoUsdcBalance();

        // use min price, because we are sending in usdc
        uint256 price = gmxUnderlyingVault.getMinPrice(address(usdc));

        // adjust for decimals and max possible slippage
        uint256 minUsdg = _roundUsdcBalance.mulDiv(price * 1e12 * (MAX_BPS - slippageThresholdGmxBps), 1e30 * MAX_BPS);

        vaultBatchingState.roundGlpStaked = _stakeGlp(address(usdc), _roundUsdcBalance, minUsdg);

        emit BatchStake(vaultBatchingState.currentRound, _roundUsdcBalance, vaultBatchingState.roundGlpStaked);
    }

Impact

slippageThresholdGmxBps has no limit and can be larger than MAX_BPS, causing the executeBatchStake() to fail

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L153-L156

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L349

Tool used

Manual Review

Recommendation

    function setThresholds(uint256 _slippageThresholdGmxBps) external onlyOwner {
        require(_slippageThresholdGmxBps <=MAX_BPS );
        slippageThresholdGmxBps = _slippageThresholdGmxBps;
        emit ThresholdsUpdated(_slippageThresholdGmxBps);
    }

ctf_sec - A malicious early user/attacker can manipulate the pricePerShare to take an unfair share of future users' deposits

ctf_sec

high

A malicious early user/attacker can manipulate the pricePerShare to take an unfair share of future users' deposits

Summary

A well known attack vector for almost all shares based liquidity pool contracts, where an early user can manipulate the price per share and profit from late users' deposits because of the precision loss caused by the rather large value of price per share.

Vulnerability Detail

A malicious early user can deposit() with 1 wei of asset token as the first depositor of the underlying token for ERC4626 vault, and get 1 wei of shares.

Then the attacker can send 10000e18 - 1 of asset tokens and inflate the price per share from 1.0000 to an extreme value of 1.0000e22 ( from (1 + 10000e18 - 1) / 1) .

As a result, the future user who deposits 19999e18 will only receive 1 wei (from 19999e18 * 1 / 10000e18) of shares token.

They will immediately lose 9999e18 or half of their deposits if they redeem() right after the deposit().

Impact

The attacker can profit from future users' deposits. While the late users will lose part of their funds to the attacker.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L59-L72

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L231-L235

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L191-L197

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L368-L374

Tool used

Manual Review

Recommendation

Consider requiring a minimal amount of share tokens to be minted for the first minter, and send a port of the initial mints as a reserve to the DAO so that the pricePerShare can be more resistant to manipulation.

Duplicate of #37

rvierdiiev - DnGmxJuniorVault.maxDeposit and DnGmxJuniorVault.afterDeposit calculate maximum assets that are allowed to deposit in different ways

rvierdiiev

medium

DnGmxJuniorVault.maxDeposit and DnGmxJuniorVault.afterDeposit calculate maximum assets that are allowed to deposit in different ways

Summary

DnGmxJuniorVault.maxDeposit and DnGmxJuniorVault.afterDeposit calculate maximum assets that are allowed to deposit in different ways.

Vulnerability Detail

This is how DnGmxJuniorVault.maxDeposit function calculates max amount of assets that are allowed to deposit.
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxJuniorVault.sol#L531-L533

    function maxDeposit(address) public view override(IERC4626, ERC4626Upgradeable) returns (uint256) {
        return state.depositCap - state.totalAssets(true);
    }

And this is how DnGmxJuniorVault.afterDeposit function calculates max amount of assets that are allowed to deposit.
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxJuniorVault.sol#L719-L729

    function afterDeposit(
        uint256,
        uint256,
        address
    ) internal override {
        if (totalAssets() > state.depositCap) revert DepositCapExceeded();
        (uint256 currentBtc, uint256 currentEth) = state.getCurrentBorrows();


        //rebalance of hedge based on assets after deposit (after deposit assets)
        state.rebalanceHedge(currentBtc, currentEth, totalAssets(), false);
    }

As you can see in one case it uses totalAssets() function to get all assets.

    function totalAssets() public view override(IERC4626, ERC4626Upgradeable) returns (uint256) {
        return state.totalAssets();
    }

And in another case it uses state.totalAssets(true).

This is how state.totalAssets() and state.totalAssets(true) are handled in DnGmxJuniorVaultManager.
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L997-L1052

    function totalAssets(State storage state) external view returns (uint256) {
        return _totalAssets(state, false);
    }


    ///@notice returns the total assets deposited to the vault (in glp amount)
    ///@param state set of all state variables of vault
    ///@param maximize true for maximizing the total assets value and false to minimize
    ///@return total asset amount (glp + usdc (in glp terms))
    function totalAssets(State storage state, bool maximize) external view returns (uint256) {
        return _totalAssets(state, maximize);
    }


    ///@notice returns the total assets deposited to the vault (in glp amount)
    ///@param state set of all state variables of vault
    ///@param maximize true for maximizing the total assets value and false to minimize
    ///@return total asset amount (glp + usdc (in glp terms))
    function _totalAssets(State storage state, bool maximize) private view returns (uint256) {
        // usdc deposited by junior tranche (can be negative)
        int256 dnUsdcDeposited = state.dnUsdcDeposited;


        // convert int into two uints basis the sign
        uint256 dnUsdcDepositedPos = dnUsdcDeposited > int256(0) ? uint256(dnUsdcDeposited) : 0;
        uint256 dnUsdcDepositedNeg = dnUsdcDeposited < int256(0) ? uint256(-dnUsdcDeposited) : 0;


        // add positive part to unhedgedGlp which will be added at the end
        // convert usdc amount into glp amount
        uint256 unhedgedGlp = (state.unhedgedGlpInUsdc + dnUsdcDepositedPos).mulDivDown(
            PRICE_PRECISION,
            _getGlpPrice(state, !maximize)
        );


        // calculate current borrow amounts
        (uint256 currentBtc, uint256 currentEth) = _getCurrentBorrows(state);
        uint256 totalCurrentBorrowValue = _getBorrowValue(state, currentBtc, currentEth);


        // add negative part to current borrow value which will be subtracted at the end
        // convert usdc amount into glp amount
        uint256 borrowValueGlp = (totalCurrentBorrowValue + dnUsdcDepositedNeg).mulDivDown(
            PRICE_PRECISION,
            _getGlpPrice(state, !maximize)
        );


        // if we need to minimize then add additional slippage
        if (!maximize) unhedgedGlp = unhedgedGlp.mulDivDown(MAX_BPS - state.slippageThresholdGmxBps, MAX_BPS);
        if (!maximize) borrowValueGlp = borrowValueGlp.mulDivDown(MAX_BPS - state.slippageThresholdGmxBps, MAX_BPS);


        // total assets considers 3 parts
        // part1: glp balance in vault
        // part2: glp balance in batching manager
        // part3: pnl on AAVE (usdc deposit by junior tranche (i.e. dnUsdcDeposited) - current borrow value)
        return
            state.fsGlp.balanceOf(address(this)) +
            state.batchingManager.dnGmxJuniorVaultGlpBalance() +
            unhedgedGlp -
            borrowValueGlp;
    }

That means that one of DnGmxJuniorVault.maxDeposit and DnGmxJuniorVault.afterDeposit functions uses incorrect amount of assets.

Impact

Incorrect calculations because of incorrect assets value.

Code Snippet

Provided above.

Tool used

Manual Review

Recommendation

Use same approach to get total assets in both functions.

yixxas - Use of transferFrom() for arbitrary ERC20 tokens is not recommended

yixxas

medium

Use of transferFrom() for arbitrary ERC20 tokens is not recommended

Summary

There are plenty of non-compliant ERC20 tokens and some do not revert on failure such as ZRX. Instead, they return false and requires to be handled by the calling contract.

Vulnerability Detail

depositToken() uses transferFrom() to receive tokens from users. Failure of this function is not caught by the protocol if token is non-compliant.

Impact

Tokens are then staked based on amount that is transferred. If contract is holding any of such tokens, this function can be called by anyone to stake tokens that do not belong to them.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L187

Tool used

Manual Review

Recommendation

Use OpenZeppelin's safeTransferFrom() instead.

0x0 - Third Parties May Unpause Contract

0x0

high

Third Parties May Unpause Contract

Summary

Open Zeppelin's Pausable library enables contract administrators to prevent the operation of specific functions. This helps to protect users from loses by restricting further deposits into a set of contracts that are experiencing security/operational issues.

Vulnerability Detail

DnGmxBatchingManager.executeBatchDeposit

This function may be called by anybody. There is logic inside that will unpause the contracts if the contracts are currently paused.

Elsewhere in the contracts the ability to unpause is restricted to specific users with a modifier.

Impact

Users of the contracts can incur further loses in the following set of events:

  • An attacker has exploited a vulnerability in the contract
  • The administrator has paused the contract to prevent users depositing and incurring further loses
  • The attacker may unpause the contract to attract and exploit more users

Code Snippet

  • Example of where _unpause() is protected:
function unpauseDeposit() external onlyKeeper {
    _unpause();
}
  • Example of where anybody may call _unpause():
function executeBatchDeposit() external {
    // If the deposit is paused then unpause on execute batch deposit
    if (paused()) _unpause();

Tool used

Manual Review

Recommendation

  • Protect this function by implementing a modifier

Bnke0x0 - Collect modules can fail on zero amount transfers if assets fee is set to zero

Bnke0x0

medium

Collect modules can fail on zero amount transfers if assets fee is set to zero

Summary

Vulnerability Detail

Impact

assets fee can be zero, while collect modules do attempt to send it in such a case anyway as there is no check in place. Some ERC20 tokens do not allow zero-value transfers, reverting such attempts.

This way, a combination of zero assets fee and such a token set as a collection fee currency will revert any collect operations, rendering collect functionality unavailable

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L64

     'IERC20Metadata(asset).safeTransferFrom(msg.sender, address(this), assets);'

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L88

    ' IERC20Metadata(asset).safeTransferFrom(msg.sender, address(this), assets);'

References

Some ERC20 tokens revert on zero value transfers:

https://github.com/d-xo/weird-erc20#revert-on-zero-value-transfers

Tool used

Manual Review

Recommendation

Consider checking the treasury fee amount and do a transfer only when it is positive.

Now:

 IERC20Metadata(asset).safeTransferFrom(msg.sender, address(this), assets);

To be:

if (assets > 0) {
	 IERC20Metadata(asset).safeTransferFrom(msg.sender, address(this), assets);

ctf_sec - Stale oracle price can be used because the oracle source is lack of price refreshness check.

ctf_sec

medium

Stale oracle price can be used because the oracle source is lack of price refreshness check.

Summary

Stale oracle price can be used.

Vulnerability Detail

The code currently uses Aave's oracle price and Aave use chainlink oracle data.

5 results - 2 files

dn-gmx-vaults\contracts\libraries\DnGmxJuniorVaultManager.sol:
  1102          // AAVE oracle
  1103:         uint256 price = state.oracle.getAssetPrice(address(token));
  1104  

  1150          uint256 decimals = token.decimals();
  1151:         uint256 price = state.oracle.getAssetPrice(address(token));
  1152  
  1153          // @dev aave returns from same source as chainlink (which is 8 decimals)
  1154:         uint256 quotePrice = state.oracle.getAssetPrice(address(state.usdc));
  1155  

dn-gmx-vaults\contracts\vaults\DnGmxSeniorVault.sol:
  314      function getPriceX128() public view returns (uint256) {
  315:         uint256 price = oracle.getAssetPrice(address(asset));
  316  

  325          // use aave's oracle to get price of usdc
  326:         uint256 price = oracle.getAssetPrice(address(asset));
  327  

we are calling oracle.getAssetPrice(address(asset)) from Aave, which calls:

https://github.com/aave/aave-v3-core/blob/f3e037b3638e3b7c98f0c09c56c5efde54f7c5d2/contracts/misc/AaveOracle.sol#L104

  /// @inheritdoc IPriceOracleGetter
  function getAssetPrice(address asset) public view override returns (uint256) {
    AggregatorInterface source = assetsSources[asset];

    if (asset == BASE_CURRENCY) {
      return BASE_CURRENCY_UNIT;
    } else if (address(source) == address(0)) {
      return _fallbackOracle.getAssetPrice(asset);
    } else {
      int256 price = source.latestAnswer();
      if (price > 0) {
        return uint256(price);
      } else {
        return _fallbackOracle.getAssetPrice(asset);
      }
    }
  }

note the line:

int256 price = source.latestAnswer();

the Aave's code does not check if the price get from the data is the updated price.

there is no freshness check. This could lead to stale prices being used.

If the market price of the token drops very quickly ("flash crashes"), and Chainlink's feed does not get updated in time, the smart contract will continue to believe the token is worth more than the market value.

Chainlink also advise developers to check for the updatedAt before using the price:

Your application should track the latestTimestamp variable or use the updatedAt value from the latestRoundData() function to make sure that the latest answer is recent enough for your application to use it. If your application detects that the reported answer is not updated within the heartbeat or within time limits that you determine are acceptable for your application, pause operation or switch to an alternate operation mode while identifying the cause of the delay.

Impact

A stale price can cause the malfunction of multiple features across the protocol:

    function _getTokenPriceInUsdc(State storage state, IERC20Metadata token)
        private
        view
        returns (uint256 scaledPrice)
    {
        uint256 decimals = token.decimals();
        uint256 price = state.oracle.getAssetPrice(address(token));

        // @dev aave returns from same source as chainlink (which is 8 decimals)
        uint256 quotePrice = state.oracle.getAssetPrice(address(state.usdc));

        // token price / usdc price
        scaledPrice = price.mulDivDown(PRICE_PRECISION, quotePrice * 10**(decimals - 6));
    }

if the function getTokenPriceInUsdc used a stale price, the function that relies on this function to calculate the optimal threshold to borrow and calculate the optimal amount to flashloan are not accurate

    function _getOptimalCappedBorrows(
        State storage state,
        uint256 availableBorrowAmount,
        uint256 usdcLiquidationThreshold
    ) private view returns (uint256 optimalBtcBorrow, uint256 optimalEthBorrow) {
        // The value of max possible value of ETH+BTC borrow
        // calculated basis available borrow amount, liqudation threshold and target health factor
        // AAVE target health factor = (usdc supply value * usdc liquidation threshold)/borrow value
        // whatever tokens we borrow from AAVE (ETH/BTC) we sell for usdc and deposit that usdc into AAVE
        // assuming 0 slippage borrow value of tokens = usdc deposit value (this leads to very small variation in hf)
        // usdc supply value = usdc borrowed from senior tranche + borrow value
        // replacing usdc supply value formula above in AAVE target health factor formula
        // we can replace usdc borrowed from senior tranche with available borrow amount
        // we can derive max borrow value of tokens possible i.e. maxBorrowValue
        uint256 maxBorrowValue = availableBorrowAmount.mulDivDown(
            usdcLiquidationThreshold,
            state.targetHealthFactor - usdcLiquidationThreshold
        );

        // calculate the borrow value of eth & btc using their weights
        uint256 btcWeight = state.gmxVault.tokenWeights(address(state.wbtc));
        uint256 ethWeight = state.gmxVault.tokenWeights(address(state.weth));

        // get eth and btc price in usdc
        uint256 btcPrice = _getTokenPriceInUsdc(state, state.wbtc);
        uint256 ethPrice = _getTokenPriceInUsdc(state, state.weth);

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L1086-L1108

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L1128-L1156

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L1253-L1279

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L255-L269

Tool used

Manual Review

Recommendation

We recommend the project fetch the price from chainlink by calling the function latestRoundData() and add the freshness check.

function latestRoundData() external view
    returns (
        uint80 roundId,
        int256 answer,
        uint256 startedAt,
        uint256 updatedAt,
        uint80 answeredInRound
    )

clems4ever - Noop rebalance under a particular condition

clems4ever

medium

Noop rebalance under a particular condition

Summary

In DnGmxJuniorVault.sol:
If only one of BTC/ETH price changes (not both), only one side needs to be rebalanced in _rebalanceBorrow(), and this causes the rebalance to fail because of a mistake in the condition

Vulnerability Detail

Here https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L402
if condition is true then the flashloan amounts are computed as follows:
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L409

which will trivially assign zero amounts for the flash loan in either case.

Impact

The rebalance operation is not conducted properly (and cannot be forced by admin), which means the protocol can lose funds since it is not delta neutral anymore.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade-clems4ever/commit/d908908cd4eba777ac60310b9e4255fbc27b39b2

Tool used

Manual Review

Recommendation

change the condition to

if (btcAssetAmount != 0)

Duplicate of #62

0x0 - Non-Standard ERC20 Transfer Safety

0x0

medium

Non-Standard ERC20 Transfer Safety

Summary

There are multiple tokens deployed that do not comply with the ERC20 correctly. The use of these is not handled safely in the contracts.

Vulnerability Detail

DnGmxBatchingManager

The batching manager transfers ERC20 tokens into the contract via a depositToken() function. This implementation does not check for the success of the transfer and continues to convert to GLP.

Impact

  • If there are tokens of the same type already in the contract and the ERC20 transfer fails, these will be used to stake in GLP instead of the tokens of the calling contract.

Code Snippet

IERC20(token).transferFrom(msg.sender, address(this), amount);

Tool used

Manual Review

Recommendation

8olidity - A normal user can call `executeBatchDeposit()` to bypass `unpauseDeposit()`

8olidity

medium

A normal user can call executeBatchDeposit() to bypass unpauseDeposit()

Summary

A normal user can call executeBatchDeposit() to bypass unpauseDeposit()

Vulnerability Detail

If keeper wants to suspend the contract, it can do so through pauseDeposit() or executeBatchStake()

    function executeBatchStake() external whenNotPaused onlyKeeper {
        // Harvest fees prior to executing batch deposit to prevent cooldown
        dnGmxJuniorVault.harvestFees();

        // Convert usdc in round to sglp
        _executeVaultUserBatchStake();

        // To be unpaused when the staked amount is deposited
        _pause();
    }
	function pauseDeposit() external onlyKeeper {
        _pause();
    }

During a contract suspension, an ordinary user can call executeBatchDeposit() to resume the contract and get the suspended contract running

    function executeBatchDeposit() external {
        // If the deposit is paused then unpause on execute batch deposit
        if (paused()) _unpause(); //@audit 普通用户可以将unpause

        // Transfer vault glp directly, Needs to be called only for dnGmxJuniorVault
        if (dnGmxJuniorVaultGlpBalance > 0) {
            uint256 glpToTransfer = dnGmxJuniorVaultGlpBalance;
            dnGmxJuniorVaultGlpBalance = 0;
            sGlp.transfer(address(dnGmxJuniorVault), glpToTransfer);
            emit VaultDeposit(glpToTransfer);
        }

        _executeVaultUserBatchDeposit();
    }

Bypass _unpause() restriction on keeper

    function unpauseDeposit() external onlyKeeper {
        _unpause();
    }

Impact

A normal user can call executeBatchDeposit() to bypass unpauseDeposit()

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L228-L253

Tool used

Manual Review

Recommendation

Restricts normal user calls to executeBatchDeposit()

Duplicate of #59

ctf_sec - Uniswap V3 conversion swap path is incorrectly hardcoded in DnGmxJuniorVaultManager.sol

ctf_sec

high

Uniswap V3 conversion swap path is incorrectly hardcoded in DnGmxJuniorVaultManager.sol

Summary

Uniswap V3 swap path is hardcoded.

Vulnerability Detail

When receiving a token from the balancer flash loan, the contract needs to perform a trade on Uniswap via two function.

  ///@notice swaps usdc into token
  ///@param state set of all state variables of vault
  ///@param token address of token
  ///@param tokenAmount token amount to be bought
  ///@param maxUsdcAmount maximum amount of usdc that can be sold
  ///@return usdcPaid amount of usdc paid for swap
  ///@return tokensReceived amount of tokens received on swap
  function _swapUSDC(
      State storage state,
      address token,
      uint256 tokenAmount,
      uint256 maxUsdcAmount
  ) internal returns (uint256 usdcPaid, uint256 tokensReceived) {
      ISwapRouter swapRouter = state.swapRouter;

      bytes memory path = token == address(state.weth) ? USDC_TO_WETH(state) : USDC_TO_WBTC(state);

We swap swaps usdc into token.

and

    ///@notice swaps token into usdc
    ///@param state set of all state variables of vault
    ///@param token address of token
    ///@param tokenAmount token amount to be sold
    ///@param minUsdcAmount minimum amount of usdc required
    ///@return usdcReceived amount of usdc received on swap
    ///@return tokensUsed amount of tokens paid for swap
    function _swapToken(
        State storage state,
        address token,
        uint256 tokenAmount,
        uint256 minUsdcAmount
    ) internal returns (uint256 usdcReceived, uint256 tokensUsed) {
        ISwapRouter swapRouter = state.swapRouter;

        // path of the token swap
        bytes memory path = token == address(state.weth) ? WETH_TO_USDC(state) : WBTC_TO_USDC(state);

We swap token into usdc. now if we look into how WETH_TO_USDC related path construction implemented:

    /* solhint-disable func-name-mixedcase */
    ///@notice returns usdc to weth swap path
    ///@param state set of all state variables of vault
    ///@return the path bytes
    function USDC_TO_WETH(State storage state) internal view returns (bytes memory) {
        return abi.encodePacked(state.weth, uint24(500), state.usdc);
    }

    ///@notice returns usdc to wbtc swap path
    ///@param state set of all state variables of vault
    ///@return the path bytes
    function USDC_TO_WBTC(State storage state) internal view returns (bytes memory) {
        return abi.encodePacked(state.wbtc, uint24(3000), state.weth, uint24(500), state.usdc);
    }

    ///@notice returns weth to usdc swap path
    ///@param state set of all state variables of vault
    ///@return the path bytes
    function WETH_TO_USDC(State storage state) internal view returns (bytes memory) {
        return abi.encodePacked(state.weth, uint24(500), state.usdc);
    }

    ///@notice returns wbtc to usdc swap path
    ///@param state set of all state variables of vault
    ///@return the path bytes
    function WBTC_TO_USDC(State storage state) internal view returns (bytes memory) {
        return abi.encodePacked(state.wbtc, uint24(3000), state.weth, uint24(500), state.usdc);
    }

clearly we can see the path is hardcoded:

in the current implementation:

USDC_TO_WETH == WETH_TO_USDC,

both use:

return abi.encodePacked(state.weth, uint24(500), state.usdc);

both only trying to construct the path from WETH to USDC.

USDT_TO_WBTC == WBTC_TO_USDC

both use:

return abi.encodePacked(state.wbtc, uint24(3000), state.weth, uint24(500), state.usdc);

both only trying to construct the path from WBTC to USDC.

Another point:

the hardcoded path may not be the optimal path:

If we see the WETH related path,

https://info.uniswap.org/#/tokens/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2

the WBTC/ETH pool that has 0.05% fee has much higher trading volume than the WBTC / ETH pool that charges 0.3% fee,

image

but in the code, the path also assume the WBTC / ETH pool charges 0.3% fee, given the trading volume, the WBTC / ETH pool that charges 0.05% can be a better option.

Impact

The code has issue constructing path from USDC to WETH or USDC to WBTC.
The hardcoded may not have the optimal liquidity / fee charged when conducting the trade.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L1455-L1552

Tool used

Manual Review

Recommendation

We recommend the project at least change from

    function USDC_TO_WBTC(State storage state) internal view returns (bytes memory) {
        return abi.encodePacked(state.wbtc, uint24(3000), state.weth, uint24(500), state.usdc);
    }

to

    function USDC_TO_WBTC(State storage state) internal view returns (bytes memory) {
        return abi.encodePacked(state.usdc, uint24(500), state.weth, uint24(500), state.wbtc);
    }

and

    function USDC_TO_WETH(State storage state) internal view returns (bytes memory) {
        return abi.encodePacked(state.weth, uint24(500), state.usdc);
    }

to

    function USDC_TO_WETH(State storage state) internal view returns (bytes memory) {
        return abi.encodePacked(state.usdc, uint24(500), state.weth);
    }

and it is better to preview the output of the different conversion paths and then use the path that has the optimal liquidity instead of hardcoding the path.

Duplicate of #73

zimu - The _totalSupply can be miscalculated in ERC4626Upgradeable.sol

zimu

medium

The _totalSupply can be miscalculated in ERC4626Upgradeable.sol

Summary

The _totalSupply can be miscalculated when an EOA with depolyed contract accidentally reenters deposit and mint function of ERC4626Upgradeable.sol twice or more.

Vulnerability Detail

It is great that the implementation of deposit and mint function of ERC4626Upgradeable.sol transfer assets before minting or ERC777s could reenter. However, the total supply _totalSupply of the Vault's underlying asset token is calculated after deposit or mint. _totalSupply would be calculated wrong when a depolyed asset contract with callback reenters deposit and mint twice or more.

Let's take the reentrancy of two times for example:

  1. An user approves and calls ERC4626Upgradeable.deposit to deposit his assets to the Vault;
  2. However, In ERC4626Upgradeable.deposit, IERC20Metadata(asset).safeTransferFrom(msg.sender, address(this), assets), the asset is implemented with a callback in its ERC721TokenReceiver to ERC4626Upgradeable.deposit;
  3. Then, 2 assets of the user may transfer to the Vault, but _mint(receiver, shares) in ERC4626Upgradeable.deposit only counts 1 time of shares for the user with miscalculated _totalSupply.

The calling path is as follows:

in ERC4626Upgradeable.deposit(uint256,address) (dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#59-71):
External calls:

- IERC20Metadata(asset).safeTransferFrom(msg.sender,address(this),assets) (dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#64)

State variables written after the call(s):

- _mint(receiver,shares) (dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#66)

- _totalSupply += amount (manual-export/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol#269)

in ERC4626Upgradeable.mint(uint256,address) (dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#84-95):

External calls:

- IERC20Metadata(asset).safeTransferFrom(msg.sender,address(this),assets) (dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#88)

State variables written after the call(s):

- _mint(receiver,shares) (dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#90)

- _totalSupply += amount (manual-export/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol#269)

Impact

If an asset is implemented with a callback in its ERC721TokenReceiver to ERC4626Upgradeable.deposit or ERC4626Upgradeable.mint, the shares of user would be miscalculated with wrong _totalSupply.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L59-L71
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L84-L95

Tool used

Manual Review

Recommendation

To implement another function independently scans, calculates and updates the shares of users of the Vault’s underlying asset token.

joestakey - `DnGmxSeniorVault` share minting can be broken by early depositor.

joestakey

high

DnGmxSeniorVault share minting can be broken by early depositor.

Summary

An early minter can break the DnGmxSeniorVault share price, resulting in future depositors losing USDC upon withdrawal.

Vulnerability Detail

DnGmxSeniorVault allows users to deposit USDC that can serve as collateral on Aave, while earning the Aave Supply APR as well as a portion of ETH Rewards from GMX based on the utilisation ratio.

Users can deposit USDC by calling DnGmxSeniorVault.deposit(). The function calls ERC4626Upgradeable.deposit(), which computes the amount of shares to be minted, and transfers the USDC to the Senior Tranche.

The issue is that because of how convertToShares computes the amount of shares to be minted, an early minter can inflate the share price and steal USDC from future depositors:

  • Alice calls DnGmxSeniorVault.deposit(1), depositing 1 unit of Usdc in the Senior Tranche. She receives 1 share.
  • Alice transfers 1e6 - 1 USDC to the vault using the ERC20.transfer() method.
  • Bob calls DnGmxSeniorVault.deposit(1.999999e6).
  • Because of Alice's transfer, aUsdc.balanceOf(vault) = 1e6. 1 * 1.9999 e6 / 1e6 rounds to 1: Bob receives 1 share: the same amount as Alice.
  • Alice calls DnGmxSeniorVault.withdraw to redeem her shares: because she owns half of the shares, she will receive ~1.5 * 1e6 aUSDC, effectively stealing approximately 0.5 * 1e6 aUSDC from Bob.

Impact

Early minters can essentially steal USDC from future minters

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L195
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L371

Tool used

Manual Review

Recommendation

Consider sending the first 1000 shares to the address 0, a mitigation used in Uniswap V2.

Duplicate of #37

0xmuxyz - `safeTransferFrom()` function should be used instead of `transferFrom()` function

0xmuxyz

medium

safeTransferFrom() function should be used instead of transferFrom() function

Summary

  • safeTransferFrom() function should be used instead of transferFrom() function

Vulnerability Detail

  • transferFrom() function is used in the following lines.
    • However, transferFrom()function does not return whether transferring tokens is successful or not.

Impact

  • This vulnerability lead to unexpected-behavior that a transaction could continue to proceed even if transferring token using transferFrom()function fail. As a result, for example, unexpected-values may be stored even if transferring tokens fail.

Code Snippet

Tool used

  • Manual Review

Recommendation

  • Should use safeTransferFrom() instead of using transferFrom()
    • The benefit of using safeTransferFrom() is that the boolean return value is automatically asserted . If a token returns false on transfer or any other operation, a contract using safeTransferFrom() will revert. This is the easiest way to check whether transferring tokens is successful or not.
      https://docs.openzeppelin.com/contracts/2.x/api/token/erc20#SafeERC20

simon135 - An attacker can give weth to the contract and can send a little weth and cause the else statement to get caused increasing the seniorVaultWethRewards or deceasing and making it zero

simon135

high

An attacker can give weth to the contract and can send a little weth and cause the else statement to get caused increasing the seniorVaultWethRewards or deceasing and making it zero

Summary

An attacker can send weth to the contract that can be small enough for the function to revert or be too much and not go into the right-if statement causing a loss of funds

Vulnerability Detail

2 scenarios:
1.
since the attacker transferred weth to the contract the harvest won't happen and the attacker can do this at every harvest call and make sure nobody gets the harvest.
or not hit the first if statement which balances glp through the batch contract which doesn't happen to cause glp to be not balanced causing liquidations to happen because glp is not balanced and if other tokens aren't doing well. Also, the assumption is broken because it's not evenly balanced.
2. There is not enough weth and if the protocol fee is more than the weth in the contract it will revert. So on the first few harvests, the function will revert which can cause the holders of the strategy not to get the harvest amount which then there Are no incentives to keep funds in.

Impact

loss of funds and loss of incentives for users

Code Snippet

       // total weth harvested which is not compounded
       // its possible that this is accumulated value over multiple rebalance if in all of those it was below threshold
       uint256 wethHarvested = state.weth.balanceOf(address(this)) - state.protocolFee - state.seniorVaultWethRewards;

       if (wethHarvested > state.wethConversionThreshold) {
           // weth harvested > conversion threshold
           uint256 protocolFeeHarvested = (wethHarvested * state.feeBps) / MAX_BPS;
           // protocol fee incremented
           state.protocolFee += protocolFeeHarvested;

           // protocol fee to be kept in weth
           // remaining amount needs to be compounded
           uint256 wethToCompound = wethHarvested - protocolFeeHarvested;

           // share of the wethToCompound that belongs to senior tranche
           uint256 dnGmxSeniorVaultWethShare = state.dnGmxSeniorVault.getEthRewardsSplitRate().mulDivDown(
               wethToCompound,
               FeeSplitStrategy.RATE_PRECISION
           );
           // share of the wethToCompound that belongs to junior tranche
           uint256 dnGmxWethShare = wethToCompound - dnGmxSeniorVaultWethShare;

           // total senior tranche weth which is not compounded
           uint256 _seniorVaultWethRewards = state.seniorVaultWethRewards + dnGmxSeniorVaultWethShare;

           uint256 glpReceived;
           {
               // converts junior tranche share of weth into glp using batching manager
               // we need to use batching manager since there is a cooldown period on sGLP
               // if deposited directly for next 15mins withdrawals would fail
               uint256 price = state.gmxVault.getMinPrice(address(state.weth));

               uint256 usdgAmount = dnGmxWethShare.mulDivDown(
                   price * (MAX_BPS - state.slippageThresholdGmxBps),
                   PRICE_PRECISION * MAX_BPS
               );

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L188

Tool used

Manual Review

Recommendation

send a little weth to the contract or find to only allow harvest after a certain time and put access control on the function

Bnke0x0 - ERC4626 does not work with fee-on-transfer tokens

Bnke0x0

medium

ERC4626 does not work with fee-on-transfer tokens

Summary

Vulnerability Detail

Impact

The ERC4626Upgradeable.deposit/mint functions do not work well with fee-on-transfer tokens as the assets variable is the pre-fee amount, including the fee, whereas the totalAssets do not include the fee anymore.

Code Snippet

This can be abused to mint more shares than desired.

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L59-L71

   '    function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) {
    // Check for rounding error since we round down in previewDeposit.
    require((shares = previewDeposit(assets)) != 0, 'ZERO_SHARES');

    // Need to transfer before minting or ERC777s could reenter.
    IERC20Metadata(asset).safeTransferFrom(msg.sender, address(this), assets);

    _mint(receiver, shares);

    emit Deposit(msg.sender, receiver, assets, shares);

    afterDeposit(assets, shares, receiver);
}'

Tool used

Manual Review

Recommendation

assets should be the amount excluding the fee, i.e., the amount the contract actually received.
This can be done by subtracting the pre-contract balance from the post-contract balance.
However, this would create another issue with ERC777 tokens.

Maybe previewDeposit should be overwritten by vaults supporting fee-on-transfer tokens to predict the post-fee assets. And do the shares computation on that, but then the afterDeposit is still called with the original assets and implementers need to be aware of this.

0x0 - Vaults Could Be Left Without An Owner

0x0

medium

Vaults Could Be Left Without An Owner

Summary

The vaults use OwnableUpgradeable from Open Zeppelin to manage ownership of each deployed vault. There's potential for the owner to accidentally renounce ownership and for the contracts to be left without an owner.

Vulnerability Detail

DnGmxJuniorVault

DnGmxSeniorVault

DnGmxBatchingManager

These vaults all import OwnableUpgradeable. This library has a renounceOwnership() function which if accidentally called by the owner will render the vault without ownership.

Impact

  • The owner could accidentally leave the contract without ownership.

Code Snippet

function renounceOwnership() public virtual onlyOwner {
    _transferOwnership(address(0));
}

Tool used

Manual Review

Recommendation

  • Override this function to prevent accidental contract ownership loss:
function renounceOwnership() public override onlyOwner {
    revert();
}

8olidity - approve(0) first

8olidity

medium

approve(0) first

Summary

approve(0) first

Vulnerability Detail

For some special tokens, such as usdt, you set the value of approve to 0 before setting it to another value

// dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol
    function _stakeGlp(
        address token,
        uint256 amount,
        uint256 minUSDG
    ) internal returns (uint256 glpStaked) {
        // swap token to obtain sGLP
        IERC20(token).approve(address(glpManager), amount);  // @audit approve(0)
        // will revert if notional output is less than minUSDG
        glpStaked = rewardRouter.mintAndStakeGlp(token, amount, minUSDG, 0);
    }

// dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol
    function initialize(
        address _usdc,
        string calldata _name,
        string calldata _symbol,
        address _poolAddressesProvider
    ) external initializer {

        IERC20(asset).approve(address(pool), type(uint256).max);
    }

    function grantAllowances() external onlyOwner {

        // allow aave lending pool to spend asset
        IERC20(asset).approve(aavePool, type(uint256).max);
    }

Impact

approve(0) first

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L335

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L97

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L106

Tool used

Manual Review

Recommendation

approve(0) first

Bnke0x0 - _stakeGlp, grantAllowances and setAddresses functions ERC20 missing return value check

Bnke0x0

medium

_stakeGlp, grantAllowances and setAddresses functions ERC20 missing return value check

Summary

Vulnerability Detail

_stakeGlp, grantAllowances and setAddresses functions performs an ERC20.approve() call but does not check the success return value. Some tokens do not revert if the approval failed but return false instead.

Impact

Tokens that don't actually perform the approve and return false are still counted as correct approve.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L335

    'IERC20(token).approve(address(glpManager), amount);'

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L97

 'IERC20(asset).approve(address(pool), type(uint256).max);'

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L106

    'IERC20(asset).approve(aavePool, type(uint256).max);'

Tool used

Manual Review

Recommendation

I recommend using OpenZeppelin’s SafeERC20 versions with the safeApprove function that handles the return value check as well as non-standard-compliant tokens.

8olidity - Protocol does not work with fee-on-transfer tokens

8olidity

medium

Protocol does not work with fee-on-transfer tokens

Summary

Fee-on-transfer tokens are not supported by the protocol.

Vulnerability Detail

In the depositToken() function, the user sends the amount of tokens to the address of the contract, but the contract cannot receive the amount of tokens if the token is a token that charges a fee

    function depositToken(
        address token,
        uint256 amount,
        uint256 minUSDG
    ) external whenNotPaused onlyDnGmxJuniorVault returns (uint256 glpStaked) {
        // revert for zero values
        if (token == address(0)) revert InvalidInput(0x30);
        if (amount == 0) revert InvalidInput(0x31);

        // dnGmxJuniorVault gives approval to batching manager to spend token
        IERC20(token).transferFrom(msg.sender, address(this), amount); //@audit use safetransferfrom()

        // convert tokens to glp
        glpStaked = _stakeGlp(token, amount, minUSDG);
        dnGmxJuniorVaultGlpBalance += glpStaked.toUint128();

        emit DepositToken(0, token, msg.sender, amount, glpStaked);
    }

Impact

Protocol does not work with fee-on-transfer tokens

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L187

Tool used

Manual Review

Recommendation

Use a token whitelist

rvierdiiev - Aave price oracle can be changed

rvierdiiev

medium

Aave price oracle can be changed

Summary

Aave price oracle can be changed in PoolAddressesProvider, but the protocol will continue use old one as it sets it on initialization instead of fetching from PoolAddressesProvider every time.

Vulnerability Detail

The protocol uses aave price oracle to get token prices.
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L1099-L1107

    function _getTokenPrice(State storage state, IERC20Metadata token) private view returns (uint256) {
        uint256 decimals = token.decimals();

        // AAVE oracle
        uint256 price = state.oracle.getAssetPrice(address(token));

        // @dev aave returns from same source as chainlink (which is 8 decimals)
        return price.mulDivDown(PRICE_PRECISION, 10**(decimals + 2));
    }

For DnGmxJuniorVault state.oracle value is stored in 2 methods. In initialize and in setHedgeParams.

In both cases oracle address is fetched using poolAddressProvider.getPriceOracle(). Once it's set, then oracle address is not updating and protocol using it for calculations.

But aave PoolAddressesProvider has a setter method to change price oracle.
https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/configuration/PoolAddressesProvider.sol#L105-L109

  function setPriceOracle(address newPriceOracle) external override onlyOwner {
    address oldPriceOracle = _addresses[PRICE_ORACLE];
    _addresses[PRICE_ORACLE] = newPriceOracle;
    emit PriceOracleUpdated(oldPriceOracle, newPriceOracle);
  }

That means that in any time owner of PoolAddressesProvider can change price oracle, while the protocol will continue using old oracle. This can lead to unpredictable results from simple reverting on calls or returning outdated values.
In case when prices will be incorrect, protocol will have calculations problems.

Also from docs of aave
https://docs.aave.com/developers/v/1.0/developing-on-aave/the-protocol/price-oracle

Always get the latest price oracle address by calling getPriceOracle() on the LendingPoolAddressProvider contract.

I believe that the same should be considered for PoolAddressesProvider contract as well.

Impact

Protocol will use wrong prices and calculations will be incorrect.

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

Fetch poolAddressProvider.getPriceOracle() address everytime or at least once in some duration.

ctf_sec - Pause function also pause withdraw / redeem, locking user's fund.

ctf_sec

medium

Pause function also pause withdraw / redeem, locking user's fund.

Summary

Pause function also pause withdraw / redeem, locking user's fund.

Vulnerability Detail

The admin can pause the contract, but pausing the contract block the user's withdraw/redeem request in both the junior vault and senior vault.

Impact

The user's funds are locked when the admin pauses the contract.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L245-L255

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L261-L271

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxJuniorVault.sol#L419-L428

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxJuniorVault.sol#L419-L443

Tool used

Manual Review

Recommendation

We recommend the project not block the user's withdraw / redeem request and remove whenNotPaused modifier from the vault redeem / withdraw function.

Nyx - Use safeTransfer/safeTransferFrom instead of transfer/transferFrom

Nyx

medium

Use safeTransfer/safeTransferFrom instead of transfer/transferFrom

Summary

Vulnerability Detail

Some tokens (like USDT) don't correctly implement the EIP20 standard and their transfer/ transferFrom function return void instead of a success boolean. Calling these functions with the correct EIP20 function signatures will always revert.

The ERC20.transfer() and ERC20.transferFrom() functions return a boolean value indicating success. This parameter needs to be checked for success. Some tokens do not revert if the transfer failed but return false instead.

Impact

Tokens that don't actually perform the transfer and return false are still counted as a correct transfer and tokens that don't correctly implement the latest EIP20 spec, like USDT, will be unusable in the protocol as they revert the transaction because of the missing return value.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L947

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L971

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L187

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L202

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L248

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L411

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxJuniorVault.sol#L309

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L193

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L203

Tool used

Manual Review

Recommendation

Recommend using OpenZeppelin's SafeERC20 versions with the safeTransfer and safeTransferFrom functions that handle the return value check as well as non-standard-compliant tokens.

zimu - Unchecked return value of external tranfer call

zimu

high

Unchecked return value of external tranfer call

Summary

The return value of IERC20(token).transfer is not checked. Actually, some tokens do not revert when failure and just return false state.

Vulnerability Detail

In DnGmxJuniorVaultManager._executeOperationToken, IERC20(token).transfer(address(state.balancerVault), amountWithPremium) is executed without check its return. When some tokens do not revert when failure and just return false, it won't act as the comments said "transfer token amount borrowed with premium back to balancer pool", and then, the pool would lost these assets.

In DnGmxBatchingManager.depositToken, IERC20(token).transferFrom(msg.sender,address(this),amount) is the same.

Impact

Unchecked return value would possibly cause loss of assets.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L947

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#187

Tool used

Manual Review

Recommendation

Check the return value of transfer. If return false, then revert the execution.

zimu - Unchecked return value of external AAVE call of IPool interface

zimu

medium

Unchecked return value of external AAVE call of IPool interface

Summary

Unchecked return value of external AAVE call of IPool interface in some functions of DnGmxJuniorVaultManager.sol. It is dangerous when a pool is working abnormal, i.e., liquidity drained, anomalous price fluctuation.

Vulnerability Detail

In function _executeRepay and _executeWithdraw of DnGmxJuniorVaultManager.sol, state.pool.repay(token, amount, VARIABLE_INTEREST_MODE, address(this)) and state.pool.withdraw(token, amount, receiver) are called without checking its return value.

According to the specification of aave\core-v3\contracts\interfaces\IPool.sol, repay and withdraw return the final amount. When repay and withdraw return zero or an abnormal amount number without calling revert, the fund would be lost.

Impact

Unchecked return value of external call to pool will suffer losses under abnormal circumstances.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L828-L834

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L853-L860

Tool used

Manual Review

Recommendation

Check return value, and implement some handles

simon135 - There is no input validation on `withdrawToken()` so an attacker can input any address as `from` and cause loss of funds

simon135

high

There is no input validation on withdrawToken() so an attacker can input any address as from and cause loss of funds

Summary

There is no input validation on withdrawToken() so an attacker can input any address as from and cause a loss of funds

Vulnerability Detail

an attacker can supply an address as from parameter and cause loss of funds because its not msg.sender that is withdrawing and then the receiver is the attacker

Impact

loss of funds

Code Snippet

      dnGmxJuniorVault.withdraw(sGlpAmount, address(this), from);

        amountOut = _convertToToken(token, receiver);

        emit TokenWithdrawn(from, receiver, token, sGlpAmount, amountOut);
    }

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/periphery/WithdrawPeriphery.sol#L113

Tool used

Manual Review

Recommendation

only allow msg.sender to withdraw their funds or their approved

Duplicate of #79

clems4ever - The function redeem is unprotected

clems4ever

high

The function redeem is unprotected

Summary

In WithdrawPeriphery.sol redeemToken is not protected, allowing stealing of user funds under conditions.

https://github.com/clems4ever/2022-10-rage-trade-clems4ever/blob/d435f586667c6312cfadecd3f8c850a89907c5d9/dn-gmx-vaults/contracts/periphery/WithdrawPeriphery.sol#L133

Vulnerability Detail

When a legitimate user desires to interact with WithdrawPeriphery, he has to first approve tokens to the contract. Unfortunately once he has approved his tokens to the contract an attacker can use redeemToken to steal his funds because from and receiver are not checked.

Impact

Theft of user funds

Code Snippet

See the test labeled 2.unprotected_redeem

Tool used

Manual Review

Recommendation

Do not leave the from parameter open. Use msg.sender

Duplicate of #79

0x0 - Inconsistent Storage Gaps

0x0

medium

Inconsistent Storage Gaps

Summary

Storage gaps are used with upgradable contracts to provide safety in new deployments. They allow for new storage variables to be added without overwriting existing state. There is an inconsistent number of gaps between the vaults.

Vulnerability Detail

DnGmxBatchingManager

There are 100 slots reserved in this implementation. In the junior vault there are 50, and there are 50 in the senior vault.

Impact

  • This lack of consistency in the contract implementation could lead to confusion in upgrades and overwrites of state storage.

Code Snippet

uint256[100] private _gaps;

Tool used

Manual Review

Recommendation

  • Have a consistent approach to assigning storage slots with upgradable contracts to avoid confusion and state collisions during deployments

clems4ever - Anyone can unpause deposit on DnGmxBatchingManager

clems4ever

medium

Anyone can unpause deposit on DnGmxBatchingManager

Summary

The issue is here: https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L242

Anyone with no particular permission can unpause deposit on the contract.

Vulnerability Detail

The PoC is here https://github.com/sherlock-audit/2022-10-rage-trade-clems4ever/commit/b7d6387e7e77e592bdd82668582ffefbd39ad43b

Impact

Users can start using the contract before it is ready to use (params are set by the owner) and this can mess up rewards and fees calculations.

Code Snippet

Issue: https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L242

The PoC is here https://github.com/sherlock-audit/2022-10-rage-trade-clems4ever/commit/b7d6387e7e77e592bdd82668582ffefbd39ad43b

Tool used

Test framework

Manual Review

Recommendation

Remove that line and perhaps add whenNotPaused modifier to the executeBatchDeposit function.

Duplicate of #59

Bnke0x0 - Deflationary tokens are not supported

Bnke0x0

medium

Deflationary tokens are not supported

Summary

Vulnerability Detail

There are ERC20 tokens that may make certain customizations to their ERC20 contracts. One type of these tokens is deflationary tokens that charge a certain fee for every transfer() or transferFrom().

Impact

assume that the external ERC20 balance of the contract increases by the same amount as the amount parameter of the transferFrom.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L187

   'IERC20(token).transferFrom(msg.sender, address(this), amount);'

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L202

      'usdc.transferFrom(msg.sender, address(this), amount);'

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L248

      'sGlp.transfer(address(dnGmxJuniorVault), glpToTransfer);'

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L411

      'dnGmxJuniorVault.transfer(receiver, amount);'

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxJuniorVault.sol#L309

   'state.weth.transfer(state.feeRecipient, amount);'

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L193

      'aUsdc.transfer(msg.sender, amount);'

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L203

       'aUsdc.transferFrom(msg.sender, address(this), amount);'

Tool used

Manual Review

Recommendation

One possible mitigation is to measure the asset change right before and after the asset-transferring functions.

ctf_sec - Front-runnable DnGmxSeniorVault.sol#updateBorrowCap

ctf_sec

medium

Front-runnable DnGmxSeniorVault.sol#updateBorrowCap

Summary

The borrower can front run the updateBorrowCap and borrow more than intended.

Vulnerability Detail

The function updateBorrowCap is vulnerable to front-running.

  function updateBorrowCap(address borrowerAddress, uint256 cap) external onlyOwner {
      if (borrowerAddress != address(dnGmxJuniorVault) && borrowerAddress != address(leveragePool))
          revert InvalidBorrowerAddress();

      if (IBorrower(borrowerAddress).getUsdcBorrowed() >= cap) revert InvalidCapUpdate();

      borrowCaps[borrowerAddress] = cap;
      // give allowance to borrower to pull whenever required
      aUsdc.approve(borrowerAddress, cap);

      emit BorrowCapUpdated(borrowerAddress, cap);
  }

the borrower can use borrow to front-run the updateBorrowCap.

  function borrow(uint256 amount) external onlyBorrower {
      // revert on invalid borrow amount
      if (amount == 0 || amount > availableBorrow(msg.sender)) revert InvalidBorrowAmount();

      // lazily harvest fees (harvest would return early if not enough rewards accrued)
      dnGmxJuniorVault.harvestFees();

      // transfers aUsdc to borrower
      // but doesn't reduce totalAssets of vault since borrwed amounts are factored in
      aUsdc.transfer(msg.sender, amount);
  }

the onlyBorrower modifier restrict that only the dnGmxJuniorVault contract can call this function so far, how can we trigger the dnGmxJuniorVault to borrow from the senior vault?

The dnGmxJuniorVault call DnGmxSeniorVault.sol#borrow in DnGmxJuniorVaultManager.sol#rebalanceHedge

// rebalance the unhedged glp (increase/decrease basis the capped optimal token hedges)
_rebalanceUnhedgedGlp(state, optimalUncappedEthBorrow, optimalEthBorrow);

if (availableBorrow > 0) {
    // borrow whatever is available since required > available
    state.dnGmxSeniorVault.borrow(availableBorrow);
}
} else {
//No unhedged glp remaining so just pass same value in capped and uncapped (should convert back any ausdc back to sglp)
_rebalanceUnhedgedGlp(state, optimalEthBorrow, optimalEthBorrow);

// Take from LB Vault
state.dnGmxSeniorVault.borrow(targetDnGmxSeniorVaultAmount - currentDnGmxSeniorVaultAmount);
}

this function DnGmxJuniorVaultManager.sol#rebalanceHedge is called in the beforeWithdraw and afterDeposit hook in the junior Vault.

  function beforeWithdraw(
      uint256 assets,
      uint256,
      address
  ) internal override {
      (uint256 currentBtc, uint256 currentEth) = state.getCurrentBorrows();

      //rebalance of hedge based on assets after withdraw (before withdraw assets - withdrawn assets)
      state.rebalanceHedge(currentBtc, currentEth, totalAssets() - assets, false);
  }

  function afterDeposit(
      uint256,
      uint256,
      address
  ) internal override {
      if (totalAssets() > state.depositCap) revert DepositCapExceeded();
      (uint256 currentBtc, uint256 currentEth) = state.getCurrentBorrows();

      //rebalance of hedge based on assets after deposit (after deposit assets)
      state.rebalanceHedge(currentBtc, currentEth, totalAssets(), false);
  }

Consider this case,

the admin owner wants to update the updateBorrowCap,

the old borrow cap is 100,

the admin wants to update the borrow to 50

A user detect this transaction.

He frontrun the updateBorrowCap, he call the deposit in Junior vault, which trigger the afterDeposit hook, which borrow from the senior vault.
and borrow 100 amount.

The transaction landed, the borrow cap is adjusted to 50 amount.

The user backrun the updateBorrowCap, he call the withdraw in Junior vault, which trigger the beforeWithdrawal hook, which borrow another 50 amount from the senior vualt.

Impact

User can borrow more than the admin intended.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L155-L168

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxJuniorVault.sol#L707-L729

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L591-L607

Tool used

Manual Review

Recommendation

Instead of setting the given amount, one can reduce from the current approval. By doing so, it checks whether the previous approval is spend.

0x0 - Two Storage Gap Implementations In Same Contract

0x0

medium

Two Storage Gap Implementations In Same Contract

Summary

Storage gaps provide state safety against collisions on upgrades. In one contract there are two implementations.

Vulnerability Detail

DnGmxBatchingManager

Line 46 implements a storage gap of 100 slots. Line 74 implements a further 50.

Impact

  • Multiple implementations can create confusion during upgrades and lead to state being overwritten.

Code Snippet

uint256[100] private _gaps;
uint256[50] private __gaps2;

Tool used

Manual Review

Recommendation

  • Be explicit with a single implementation to avoid confusion and collisions at a later upgrade time.

Prefix - feeRecipient can be set to unavailable address, leading to losing funds with withdrawFees

Prefix

low

feeRecipient can be set to unavailable address, leading to losing funds with withdrawFees

Medium- feeRecipient can be set to unavailable address, leading to losing funds with withdrawFees

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxJuniorVault.sol#L294-L311 .

Owner can set the feeRecipient to unavailable address, e.g. 0 by mistake. The next call to withdrawFees loses all the accumulated fees from contract. Even if the owner quickly recognizes the mistake, malicious actor can frontrun next call to setFeeParams with withdrawFees.

Remediation

Consider setting the feeRecipient in two steps like in following example: OpenZeppelin/openzeppelin-contracts#3620 .

rvierdiiev - Fees can be burnt by 3 party actor if state.feeRecipient is set to 0 in DnGmxJuniorVault

rvierdiiev

medium

Fees can be burnt by 3 party actor if state.feeRecipient is set to 0 in DnGmxJuniorVault

Summary

Fees can be burnt by 3 party actor if state.feeRecipient is set to 0.

Vulnerability Detail

DnGmxJuniorVault.withdrawFees is a function that is allowed to call by anyone in order to send accumulated fees to the state.feeRecipient param.

DnGmxJuniorVault.setFeeParams function allows owner to provide state.feeRecipient param. But it doesn't check if address is not 0. Also while initializing there is no state.feeRecipient setting. That means that after the contract is created and initialized state.feeRecipient param is address(0).

As the result when DnGmxJuniorVault.withdrawFees function is called then all fees will be burnt to address(0). And what is more dangerous is that the function can be called by anyone. While protocol owners will notice the mistake and will try to change fee recipient someone can already burn their fees.

Similar problem has DnGmxJuniorVault.claimVestedGmx, but in this case it's only callable by owner, so in case if owner will see that no fee recipient is provided he has time to do that.

Impact

Protocol fees can be burnt because of lack of input checking.

Code Snippet

    function setFeeParams(uint16 _feeBps, address _feeRecipient) external onlyOwner {
        if (state.feeRecipient != _feeRecipient) {
            state.feeRecipient = _feeRecipient;
        } else revert InvalidFeeRecipient();


        if (_feeBps > 3000) revert InvalidFeeBps();
        state.feeBps = _feeBps;


        emit FeeParamsUpdated(_feeBps, _feeRecipient);
    }


    /// @notice withdraw accumulated WETH fees
    function withdrawFees() external {
        uint256 amount = state.protocolFee;
        state.protocolFee = 0;
        state.weth.transfer(state.feeRecipient, amount);
        emit FeesWithdrawn(amount);
    }

Tool used

Manual Review

Recommendation

Check that fee recipient is not address(0) in DnGmxJuniorVault.withdrawFees and DnGmxJuniorVault.claimVestedGmx functions.

cccz - Attacker can manipulate the pricePerShare to profit from future users' deposits

cccz

medium

Attacker can manipulate the pricePerShare to profit from future users' deposits

Summary

By manipulating and inflating the pricePerShare to a super high value, the attacker can cause all future depositors to lose a significant portion of their deposits to the attacker due to precision loss.

Vulnerability Detail

For example, in the DnGmxSeniorVault contract, a malicious early user can deposit() with 1 wei of USDC as the first depositor of the LToken, and get 1 wei of shares.

Then the attacker can send 10000e18 of aUSDC and inflate the price per share from 1.0000 to an extreme value of 1.0000e22 ( from (10000e18 ) / 1) .

As a result, the future user who deposits 9999e18 will only receive 0 (from 9999e18 * 1 / 10000e18) of shares token.

They will immediately lose all of their deposits.

Impact

The attacker can profit from future users' deposits. While the late users will lose part of their funds to the attacker.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L59-L71
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L192-L196
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L273-L277
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L370-L373

Tool used

Manual Review

Recommendation

Consider requiring a minimal amount of share tokens to be minted for the first minter, and send part of the initial mints as a permanent reserve to the DAO/treasury/deployer so that the pricePerShare can be more resistant to manipulation.

Duplicate of #37

Ruhum - Junior and Senior vault can't handle slippage

Ruhum

medium

Junior and Senior vault can't handle slippage

Summary

The ERC4626 contract doesn't have slippage checks built in. With time the ratio of assets to shares will increase. It won't be 1:1. Anybody who deposits or withdraws is at the risk of being sandwiched by MEV bots which will cause a loss of funds for the user.

Vulnerability Detail

The user-facing functions to deposit and withdraw assets in the DnGmxJuniorVault and DnGmxSeniorVault contracts don't offer any slippage protection. Anybody using these functions through the public mempool is at the risk of being sandwiched. The ERC4626 standard doesn't have slippage checks built-in. Developers have to add it themselves. When Tribe initially launched the standard, they provided a router contract with slippage checks, see here.

Impact

Any user depositing/withdrawing assets to/from the vault is at risk of being sandwiched. With the current scale of MEV, it's pretty likely that someone will pick up on this. Users will lose funds.

Code Snippet

Vaults use standard ERC4626 function that have no slippage checks:

Tool used

Manual Review

Recommendation

Allow the user to pass a minimum value to deposit() and redeem() as well as a maximum value to mint() and withdraw(). On the client side, the deposit/withdrawal should be previewed using the respective functions and that value should be passed to the tx.

clems4ever - The function withdraw is unprotected

clems4ever

high

The function withdraw is unprotected

Summary

In WithdrawPeriphery.sol withdrawToken is not protected, allowing stealing of user funds under conditions.

https://github.com/clems4ever/2022-10-rage-trade-clems4ever/blob/d435f586667c6312cfadecd3f8c850a89907c5d9/dn-gmx-vaults/contracts/periphery/WithdrawPeriphery.sol#L113

Vulnerability Detail

When a legitimate user desires to interact with WithdrawPeriphery, he has to first approve tokens to the contract. Unfortunately once he has approved his tokens to the contract an attacker can use withdraw to steal his funds because from and receiver are not checked.

Impact

Theft of user funds

Code Snippet

See the test labeled 1.unprotected_withdraw

https://github.com/sherlock-audit/2022-10-rage-trade-clems4ever/commit/0b92f8313235caf5b05f97708116a9116b7c4a40

Tool used

Manual Review

Recommendation

Do not leave from open. Use msg.sender

Duplicate of #79

Bnke0x0 - ERC20 return values not checked

Bnke0x0

high

ERC20 return values not checked

Summary

Some tokens (like USDT) don't correctly implement the EIP20 standard and their transfer/transferFrom function return void instead of a successful boolean. Calling these functions with the correct EIP20 function signatures will always revert.

Vulnerability Detail

Impact

Tokens that don't correctly implement the latest EIP20 spec, like USDT, will be unusable in the protocol as they revert the transaction because of the missing return value. As there is a cToken with USDT as the underlying issue directly applies to the protocol.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L187

     'IERC20(token).transferFrom(msg.sender, address(this), amount);'

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L202

      'usdc.transferFrom(msg.sender, address(this), amount);'

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L203

  'aUsdc.transferFrom(msg.sender, address(this), amount);'

Tool used

Manual Review

Recommendation

We recommend using OpenZeppelin’s SafeERC20 versions with the safeTransfer and safeTransferFrom functions that handle the return value check as well as non-standard-compliant tokens.

Nyx - Attacker can steal funds

Nyx

high

Attacker can steal funds

Summary

Attacker can steal funds with withdrawToken() function.

Vulnerability Detail

Missing validation on withdrawTokens from parameter can cause attackers to steal funds.

Impact

Users who deposited dnGmxJuniorVault can lose their funds.

Code Snippet

Code :
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/periphery/WithdrawPeriphery.sol#L113-L125
Test :

it('withdrawToken - attack', async () => {
    const { usdc, users, periphery, gmxVault, dnGmxJuniorVault, dnGmxSeniorVault } = await dnGmxJuniorVaultFixture();

    await dnGmxSeniorVault.connect(users[1]).deposit(parseUnits('100', 6), users[1].address);

    const amount = parseEther('100');
    const withdrawAmount = parseEther('20');

    await dnGmxJuniorVault.connect(users[0]).deposit(amount, users[0].address);
    await dnGmxJuniorVault.connect(users[0]).approve(periphery.address, constants.MaxUint256);

    console.log('user[0] balance', await dnGmxJuniorVault.balanceOf(users[0].address));
    const usdcBalBefore = await usdc.balanceOf(users[2].address);
    console.log('usdcBalBefore', usdcBalBefore.toString());

    const tx = periphery
      .connect(users[2])
      .withdrawToken(users[0].address, usdc.address, users[2].address, withdrawAmount);

    await expect(tx).to.emit(periphery, 'TokenWithdrawn');

    const usdcBalAfter = await usdc.balanceOf(users[2].address); // attacker balance
    console.log('usdcBalAfter', usdcBalAfter.toString());
    console.log('user[0] balance', await dnGmxJuniorVault.balanceOf(users[0].address)); // user balance

Tool used

Manual Review

Recommendation

from needs to be msg.sender.

Duplicate of #79

simon135 - if the owner is resetting the fee recipient an attacker can frontrun the the resetting and the new fee recipient will not get the fees

simon135

medium

if the owner is resetting the fee recipient an attacker can frontrun the the resetting and the new fee recipient will not get the fees

Summary

if the owner is resetting the fee recipient an attacker can front-run the resetting and the new fee recipient will not get the fees

Vulnerability Detail

if the owner is resetting the fee recipient an attacker can front-run the resetting and the new fee recipient will not get the fees
because there is no access control on the withdrawFees() function anyone can call it maybe if the fee recipient has a deal with the actor who initiated withdrawFees().The problem also is that the owner changes the recipient but why is it fair they miss out on the fees on tx before

Impact

the new recipient doesn't get fees

Code Snippet

    function withdrawFees() external {
        uint256 amount = state.protocolFee;
        state.protocolFee = 0;
        state.weth.transfer(state.feeRecipient, amount);
        emit FeesWithdrawn(amount);
    }

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxJuniorVault.sol#L306

Tool used

Manual Review

Recommendation

make it only admin or some keeper can call it

function withdrawFees() external onlyAdmin{
}

clems4ever - Anyone can trigger a transfer of protocol fees

clems4ever

high

Anyone can trigger a transfer of protocol fees

Summary

Transfer of protocol fee is not protected. The attacker can trigger the transfer whatever the parameters are set to. It could be problematic if the protocol owner made a mistake with the fee recipient address.

Vulnerability Detail

Anyone can call withdrawFees() method on DnGmxJuniorVault leading to unwanted transfer of funds to whoever was the wrong recipient.

The issue is here, the method should be protected: https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxJuniorVault.sol#L306

Impact

This can lead to loss of funds or transfer to an unexpected address if the protocol owner did not select the right fee recipient.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade-clems4ever/commit/d435f586667c6312cfadecd3f8c850a89907c5d9

Tool used

Manual Review & Test Framework

Recommendation

Add a require testing that msg.sender is actually the fee recipient. This would limit the damages because it would require that the owner of the wrong address be aware that some funds are waiting for him. In the meantime, the protocol owner could still update the fee recipient to target the right account instead.

0x52 - DnGmxJuniorVaultManager#_totalAssets current implementation doesn't properly maximize or minimize

0x52

medium

DnGmxJuniorVaultManager#_totalAssets current implementation doesn't properly maximize or minimize

Summary

The maximize input to DnGmxJuniorVaultManager#_totalAssets indicates whether to either maximize or minimize the NAV. Internal logic of the function doesn't accurately reflect that because under some circumstances, maximize = true actually returns a lower value than maximize = false.

Vulnerability Detail

    uint256 unhedgedGlp = (state.unhedgedGlpInUsdc + dnUsdcDepositedPos).mulDivDown(
        PRICE_PRECISION,
        _getGlpPrice(state, !maximize)
    );

    // calculate current borrow amounts
    (uint256 currentBtc, uint256 currentEth) = _getCurrentBorrows(state);
    uint256 totalCurrentBorrowValue = _getBorrowValue(state, currentBtc, currentEth);

    // add negative part to current borrow value which will be subtracted at the end
    // convert usdc amount into glp amount
    uint256 borrowValueGlp = (totalCurrentBorrowValue + dnUsdcDepositedNeg).mulDivDown(
        PRICE_PRECISION,
        _getGlpPrice(state, !maximize)
    );

    // if we need to minimize then add additional slippage
    if (!maximize) unhedgedGlp = unhedgedGlp.mulDivDown(MAX_BPS - state.slippageThresholdGmxBps, MAX_BPS);
    if (!maximize) borrowValueGlp = borrowValueGlp.mulDivDown(MAX_BPS - state.slippageThresholdGmxBps, MAX_BPS);

To maximize the estimate for the NAV of the vault underlying debt should minimized and value of held assets should be maximized. Under the current settings there is a mix of both of those and the function doesn't consistently minimize or maximize. Consider when NAV is "maxmized". Under this scenario the value of when estimated the GlpPrice is minimized. This minimizes the value of both the borrowedGlp (debt) and of the unhedgedGlp (assets). The result is that the NAV is not maximized because the value of the assets are also minimized. In this scenario the GlpPrice should be maximized when calculating the assets and minimized when calculating the debt. The reverse should be true when minimizing the NAV. Slippage requirements are also applied incorrectly when adjusting borrowValueGlp. The current implementation implies that if the debt were to be paid back that the vault would repay their debt for less than expected. When paying back debt the slippage should imply paying more than expected rather than less, therefore the slippage should be added rather than subtracted.

Impact

DnGmxJuniorVaultManager#_totalAssets doesn't accurately reflect NAV. Since this is used when determining critical parameters it may lead to inaccuracies.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L1013-L1052

Tool used

Manual Review

Recommendation

To properly maximize the it should assume the best possible rate for exchanging it's assets. Likewise to minimize it should assume it's debt is a large as possible and this it encounters maximum possible slippage when repaying it's debt. I recommend the following changes:

    uint256 unhedgedGlp = (state.unhedgedGlpInUsdc + dnUsdcDepositedPos).mulDivDown(
        PRICE_PRECISION,
-       _getGlpPrice(state, !maximize)
+       _getGlpPrice(state, maximize)
    );

    // calculate current borrow amounts
    (uint256 currentBtc, uint256 currentEth) = _getCurrentBorrows(state);
    uint256 totalCurrentBorrowValue = _getBorrowValue(state, currentBtc, currentEth);

    // add negative part to current borrow value which will be subtracted at the end
    // convert usdc amount into glp amount
    uint256 borrowValueGlp = (totalCurrentBorrowValue + dnUsdcDepositedNeg).mulDivDown(
        PRICE_PRECISION,
        _getGlpPrice(state, !maximize)
    );

    // if we need to minimize then add additional slippage
    if (!maximize) unhedgedGlp = unhedgedGlp.mulDivDown(MAX_BPS - state.slippageThresholdGmxBps, MAX_BPS);
-   if (!maximize) borrowValueGlp = borrowValueGlp.mulDivDown(MAX_BPS - state.slippageThresholdGmxBps, MAX_BPS);
+   if (!maximize) borrowValueGlp = borrowValueGlp.mulDivDown(MAX_BPS + state.slippageThresholdGmxBps, MAX_BPS);

yixxas - `executeBatchDeposit()` is missing access control

yixxas

medium

executeBatchDeposit() is missing access control

Summary

executeBatchDeposit() is callable by anyone which _unpause() deposits. 15 minutes cool down set by the protocol can be bypassed due to this.

Vulnerability Detail

Calling executeBatchDeposit() will unpause() deposit as it is required for batch deposit, but this can be called at anytime by anyone.

Impact

Pause state that is being used to prevent deposits can be bypassed.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L242

Tool used

Manual Review

Recommendation

Add onlyKeeper modifier to this function.

Duplicate of #59

0xmuxyz - Anyone (any external users) can mint `any amount` of `vault shares` - because of lack of access control modifier on `mint()` function

0xmuxyz

high

Anyone (any external users) can mint any amount of vault shares - because of lack of access control modifier on mint() function

Summary

  • Anyone (any external users) can mint any amount of vault shares - because of lack of access control modifier on mint() function

Vulnerability Detail

  • Lack of access control modifier on mint() function that allow any external users to be able to mint any amount of vault shares .

Impact

  • There is no access control modifier on mint() function in the DnGmxJuniorVault.sol and DnGmxSeniorVault.sol.
    • As a result, any external users can mint any amount of vault shares
      • This vulnerability lead to an exploit that give large vault shares to malicious attackers without proper efforts.

Code Snippet

Tool used

  • Manual Review

Recommendation

defsec - Use `safeTransfer/safeTransferFrom` consistently instead of `transfer/transferFrom`

defsec

medium

Use safeTransfer/safeTransferFrom consistently instead of transfer/transferFrom

Summary

Replace transferFrom() with safeTransferFrom() since _tokenIn can be any ERC20 token implementation. If transferFrom() does not return a value (e.g., USDT), the transaction reverts because of a decoding error.

Vulnerability Detail

Replace transferFrom() with safeTransferFrom() since _tokenIn can be any ERC20 token implementation. If transferFrom() does not return a value (e.g., USDT), the transaction reverts because of a decoding error. Revert without error.

Impact

It is good to add a require() statement that checks the return value of token transfers or to use something like OpenZeppelin’s safeTransfer/safeTransferFrom unless one is sure the given token reverts in case of a failure. Failure to do so will cause silent failures of transfers and affect token accounting in the contract.

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L187

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L947

Tool used

Manual Review

Recommendation

Consider using safeTransfer/safeTransferFrom or require() consistently.

0x52 - WithdrawPeriphery uses incorrect value for MAX_BPS which will allow much higher slippage than intended

0x52

medium

WithdrawPeriphery uses incorrect value for MAX_BPS which will allow much higher slippage than intended

Summary

WithdrawPeriphery accidentally uses an incorrect value for MAX_BPS which will allow for much higher slippage than intended.

Vulnerability Detail

uint256 internal constant MAX_BPS = 1000;

BPS is typically 10,000 and using 1000 is inconsistent with the rest of the ecosystem contracts and tests. The result is that slippage values will be 10x higher than intended.

Impact

Unexpected slippage resulting in loss of user funds, likely due to MEV

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/periphery/WithdrawPeriphery.sol#L47

Tool used

Manual Review

Recommendation

Correct MAX_BPS:

-   uint256 internal constant MAX_BPS = 1000;
+   uint256 internal constant MAX_BPS = 10_000;

clems4ever - Share manipulation in senior vault

clems4ever

high

Share manipulation in senior vault

Summary

First users can manipulate share allocation to ensure next users receive less shares than due.

Vulnerability Detail

By depositing into the senior vault a user can provide capital and gets shares of the total locked capital. The calculation of shares is pretty straightforward:
shares=(totalSupply/totalAssets)
where

function totalAssets() public view override(IERC4626, ERC4626Upgradeable) 
returns (uint256 amount) {
    amount = aUsdc.balanceOf(address(this));
    amount += totalUsdcBorrowed();
}

and totalSupply is the number of shares issued yet.

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L370

so a malicious user can deposit some capital first, and then send aUsdc to the contract, which would modify the ratio used to convert deposit to shares.

When the ratio is very small, rounding errors become significant. If we take the example where the number of shares is 19, and the total deposits amount to 10000 USDC, a new user depositing 1000 USDC will receive only 1 share, almost 50% less than what he's due.

If enough deposits accumulate after this manipulation, the attacker shares are worth more than what he deposited (because next shares are truncated compared to deposits).

Impact

Malicious users can manipulate share prices and withdraw other users funds.

Code Snippet

See the test labeled 3.share_manipulation
https://github.com/sherlock-audit/2022-10-rage-trade-clems4ever/commit/548b071b47bb05916f24c0f2459ae1cde9dd16a0

Tool used

Manual Review

Recommendation

Duplicate of #37

0x52 - Early depositors to DnGmxSeniorVault can manipulate exchange rates to steal funds from later depositors

0x52

high

Early depositors to DnGmxSeniorVault can manipulate exchange rates to steal funds from later depositors

Summary

To calculate the exchange rate for shares in DnGmxSeniorVault it divides the total supply of shares by the totalAssets of the vault. The first deposit can mint a very small number of shares then donate aUSDC to the vault to grossly manipulate the share price. When later depositor deposit into the vault they will lose value due to precision loss and the adversary will profit.

Vulnerability Detail

function convertToShares(uint256 assets) public view virtual returns (uint256) {
    uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero.

    return supply == 0 ? assets : assets.mulDivDown(supply, totalAssets());
}

Share exchange rate is calculated using the total supply of shares and the totalAsset. This can lead to exchange rate manipulation. As an example, an adversary can mint a single share, then donate 1e8 aUSDC. Minting the first share established a 1:1 ratio but then donating 1e8 changed the ratio to 1:1e8. Now any deposit lower than 1e8 (100 aUSDC) will suffer from precision loss and the attackers share will benefit from it.

This same vector is present in DnGmxJuniorVault.

Impact

Adversary can effectively steal funds from later users

Code Snippet

https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L211-L221

Tool used

Manual Review

Recommendation

Initialize should include a small deposit, such as 1e6 aUSDC that mints the share to a dead address to permanently lock the exchange rate:

    aUsdc.approve(address(pool), type(uint256).max);
    IERC20(asset).approve(address(pool), type(uint256).max);

+   deposit(1e6, DEAD_ADDRESS);

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.