xanoutas/solidity-security-findings huggingface dataset:
saidam017 - Convex and Aura `getReward` can be called directly, breaking liquidation rows functionality and automation
Audit: 2023-06-tokemak-judging
Severity: HIGH

saidam017

medium

# Convex and Aura `getReward` can be called directly, breaking liquidation rows functionality and automation
## Summary

Convex and Aura `getReward` can be called by anyone on behalf of destination vault, this will allow malicious users to call `getReward` and break the liquidation functionality.

## Vulnerability Detail

Convex and Aura rewarded allow anyone to call getReward on behalf of any users :

https://github.com/convex-eth/platform/blob/main/contracts/contracts/BaseRewardPool.sol#L263-L279

When Liquidation Row's `claimsVaultRewards` is called, it will loop trough destination vaults and call `collectRewards` : 

https://github.com/sherlock-audit/2023-06-tokemak/blob/main/v2-core-audit-2023-07-14/src/liquidation/LiquidationRow.sol#L104-L131

```solidity
    function claimsVaultRewards(IDestinationVault[] memory vaults)
        external
        nonReentrant
        hasRole(Roles.LIQUIDATOR_ROLE)
    {
        if (vaults.length == 0) revert Errors.InvalidParam("vaults");

        for (uint256 i = 0; i < vaults.length; ++i) {
            uint256 gasBefore = gasleft();
            IDestinationVault vault = vaults[i];

            destinationVaultRegistry.verifyIsRegistered(address(vault));

            (uint256[] memory amounts, address[] memory tokens) = vault.collectRewards();

            uint256 tokensLength = tokens.length;
            for (uint256 j = 0; j < tokensLength; ++j) {
                address token = tokens[j];
                uint256 amount = amounts[j];
                if (amount > 0 && token != address(0)) {
                    // slither-disable-next-line reentrancy-no-eth
                    _increaseBalance(address(token), address(vault), amount);
                }
            }
            uint256 gasUsed = gasBefore - gasleft();
            emit GasUsedForVault(address(vault), gasUsed, bytes32("claim"));
        }
    }
```

If the dest vault is `CurveConvexDestinationVault`, it will eventually trigger `ConvexRewards.claimRewards` : 

https://github.com/sherlock-audit/2023-06-tokemak/blob/main/v2-core-audit-2023-07-14/src/vault/CurveConvexDestinationVault.sol#L178-L180

```solidity
    function _collectRewards() internal virtual override returns (uint256[] memory amounts, address[] memory tokens) {
        (amounts, tokens) = ConvexRewards.claimRewards(convexStaking, defaultStakingRewardToken, msg.sender);
    }
```

It can be observed inside `ConvexRewards.claimRewards` that it will track before and after `getReward` balance of dest vault to calculate `amountsClaimed` that will be send liquidator : 

https://github.com/sherlock-audit/2023-06-tokemak/blob/main/v2-core-audit-2023-07-14/src/destinations/adapters/rewards/ConvexRewardsAdapter.sol#L49-L112

```solidity
    function _claimRewards(
        address gauge,
        address defaultToken,
        address sendTo
    ) internal returns (uint256[] memory amounts, address[] memory tokens) {
        Errors.verifyNotZero(gauge, "gauge");

        addr
bin2chen - _performLiquidation() not working properly
Audit: 2023-06-tokemak-judging
Severity: HIGH

bin2chen

medium

# _performLiquidation() not working properly
## Summary
in `LiquidationRow._performLiquidation()`
Wrong direct call to method `IAsyncSwapper(asyncSwapper).swap(params)`
Should be using `DelegateCall`

## Vulnerability Detail
in `LiquidationRow._performLiquidation()`
direct call to method `IAsyncSwapper(asyncSwapper).swap(params)`

```solidity
contract LiquidationRow is ILiquidationRow, ReentrancyGuard, SystemComponent, SecurityBase {
....
    function _performLiquidation(
        uint256 gasBefore,
        address fromToken,
        address asyncSwapper,
        IDestinationVault[] memory vaultsToLiquidate,
        SwapParams memory params,
        uint256 totalBalanceToLiquidate,
        uint256[] memory vaultsBalances
    ) private {
        uint256 length = vaultsToLiquidate.length;
        // the swapper checks that the amount received is greater or equal than the params.buyAmount
@>      uint256 amountReceived = IAsyncSwapper(asyncSwapper).swap(params);

        // if the fee feature is turned on, send the fee to the fee receiver
        if (feeReceiver != address(0) && feeBps > 0) {
            uint256 fee = calculateFee(amountReceived);
            emit FeesTransfered(feeReceiver, amountReceived, fee);

            // adjust the amount received after deducting the fee
            amountReceived -= fee;
            // transfer fee to the fee receiver
            IERC20(params.buyTokenAddress).safeTransfer(feeReceiver, fee);
        }

```
The current implementation of `BaseAsyncSwapper`, for example, doesn't do any token transfers

```solidity
    function swap(SwapParams memory swapParams) public virtual nonReentrant returns (uint256 buyTokenAmountReceived) {
        if (swapParams.buyTokenAddress == address(0)) revert TokenAddressZero();
        if (swapParams.sellTokenAddress == address(0)) revert TokenAddressZero();
        if (swapParams.sellAmount == 0) revert InsufficientSellAmount();
        if (swapParams.buyAmount == 0) revert InsufficientBuyAmount();

        IERC20 sellToken = IERC20(swapParams.sellTokenAddress);
        IERC20 buyToken = IERC20(swapParams.buyTokenAddress);

        uint256 sellTokenBalance = sellToken.balanceOf(address(this));

        if (sellTokenBalance < swapParams.sellAmount) {
            revert InsufficientBalance(sellTokenBalance, swapParams.sellAmount);
        }

        LibAdapter._approve(sellToken, AGGREGATOR, swapParams.sellAmount);

        uint256 buyTokenBalanceBefore = buyToken.balanceOf(address(this));

        // we don't need the returned value, we calculate the buyTokenAmountReceived ourselves
        // slither-disable-start low-level-calls,unchecked-lowlevel
        // solhint-disable-next-line avoid-low-level-calls
        (bool success,) = AGGREGATOR.call(swapParams.data);
        // slither-disable-end low-level-calls,unchecked-lowlevel

        if (!success) {
            revert SwapFailed();
        }

        uint256 buyTokenBalanceAfter = buyToken.balanceOf(addre
bin2chen - _withdraw()  idleIncrease might be less
Audit: 2023-06-tokemak-judging
Severity: HIGH

bin2chen

medium

# _withdraw()  idleIncrease might be less
## Summary
in `LMPVault._withdraw()`
Erroneous overwriting of the old `info.idleIncrease` may result in a lower number

## Vulnerability Detail
in `LMPVault._withdraw()` 
The code to calculate `info.idleIncrease` is as follows.
```solidity
    function _withdraw(
        uint256 assets,
        uint256 shares,
        address receiver,
        address owner
    ) internal virtual returns (uint256) {
        uint256 idle = totalIdle;
        WithdrawInfo memory info = WithdrawInfo({
            currentIdle: idle,
            assetsFromIdle: assets >= idle ? idle : assets,
            totalAssetsToPull: assets - (assets >= idle ? idle : assets),
            totalAssetsPulled: 0,
            idleIncrease: 0,
            debtDecrease: 0
        });

        // If not enough funds in idle, then pull what we need from destinations
        if (info.totalAssetsToPull > 0) {
            uint256 totalVaultShares = totalSupply();

            // Using pre-set withdrawalQueue for withdrawal order to help minimize user gas
            uint256 withdrawalQueueLength = withdrawalQueue.length;
            for (uint256 i = 0; i < withdrawalQueueLength; ++i) {
                IDestinationVault destVault = IDestinationVault(withdrawalQueue[i]);
                (uint256 sharesToBurn, uint256 totalDebtBurn) = _calcUserWithdrawSharesToBurn(
                    destVault,
                    shares,
                    info.totalAssetsToPull - Math.max(info.debtDecrease, info.totalAssetsPulled),
                    totalVaultShares
                );
                if (sharesToBurn == 0) {
                    continue;
                }

                uint256 assetPreBal = _baseAsset.balanceOf(address(this));
                uint256 assetPulled = destVault.withdrawBaseAsset(sharesToBurn, address(this));

                // Destination Vault rewards will be transferred to us as part of burning out shares
                // Back into what that amount is and make sure it gets into idle
@>              info.idleIncrease += _baseAsset.balanceOf(address(this)) - assetPreBal - assetPulled;
                info.totalAssetsPulled += assetPulled;
                info.debtDecrease += totalDebtBurn;

                // It's possible we'll get back more assets than we anticipate from a swap
                // so if we do, throw it in idle and stop processing. You don't get more than we've calculated
                if (info.totalAssetsPulled > info.totalAssetsToPull) {
@>                  info.idleIncrease = info.totalAssetsPulled - info.totalAssetsToPull;
                    info.totalAssetsPulled = info.totalAssetsToPull;
                    break;
                }

                // No need to keep going if we have the amount we're looking for
                // Any overage is accounted for above. Anything lower and we need to keep going
                // slither-disable-next-line incorrect-equality
Aymen0909 - Some `idle` amount is neglected in `LMPVault._withdraw` function
Audit: 2023-06-tokemak-judging
Severity: HIGH

Aymen0909

high

# Some `idle` amount is neglected in `LMPVault._withdraw` function
## Summary

In the `LMPVault._withdraw` function when accounting for the `info.idleIncrease` amount, some of the reward amount it is neglected when the condition `info.totalAssetsPulled > info.totalAssetsToPull` is satisfied which will result in a loss of funds for the protocol.

## Vulnerability Detail

The issue occurs in the `LMPVault._withdraw` function below which is called when a user tries to withdraw/redeem his funds :

```solidity
function _withdraw(
    uint256 assets,
    uint256 shares,
    address receiver,
    address owner
) internal virtual returns (uint256) {
    uint256 idle = totalIdle;
    WithdrawInfo memory info = WithdrawInfo({
        currentIdle: idle,
        assetsFromIdle: assets >= idle ? idle : assets,
        totalAssetsToPull: assets - (assets >= idle ? idle : assets),
        totalAssetsPulled: 0,
        idleIncrease: 0,
        debtDecrease: 0
    });

    // If not enough funds in idle, then pull what we need from destinations
    if (info.totalAssetsToPull > 0) {
        uint256 totalVaultShares = totalSupply();

        // Using pre-set withdrawalQueue for withdrawal order to help minimize user gas
        uint256 withdrawalQueueLength = withdrawalQueue.length;
        for (uint256 i = 0; i < withdrawalQueueLength; ++i) {
            IDestinationVault destVault = IDestinationVault(withdrawalQueue[i]);
            (uint256 sharesToBurn, uint256 totalDebtBurn) = _calcUserWithdrawSharesToBurn(
                destVault,
                shares,
                info.totalAssetsToPull - Math.max(info.debtDecrease, info.totalAssetsPulled),
                totalVaultShares
            );
            if (sharesToBurn == 0) {
                continue;
            }

            uint256 assetPreBal = _baseAsset.balanceOf(address(this));
            uint256 assetPulled = destVault.withdrawBaseAsset(sharesToBurn, address(this));

            // Destination Vault rewards will be transferred to us as part of burning out shares
            // Back into what that amount is and make sure it gets into idle
            // @audit rewards sent will be account for here
            info.idleIncrease += _baseAsset.balanceOf(address(this)) - assetPreBal - assetPulled;
            info.totalAssetsPulled += assetPulled;
            info.debtDecrease += totalDebtBurn;

            // It's possible we'll get back more assets than we anticipate from a swap
            // so if we do, throw it in idle and stop processing. You don't get more than we've calculated
            if (info.totalAssetsPulled > info.totalAssetsToPull) {
                // @audit will neglect previous `info.idleIncrease` and overwrite it
                // @audit should use += instead of =
                info.idleIncrease = info.totalAssetsPulled - info.totalAssetsToPull;
                info.totalAssetsPulled = info.totalAssetsToPull;
                break;
            }
Aymen0909 - `averagePrice` has wrong decimals due to `updatePricingInfo` wrong calculation
Audit: 2023-06-tokemak-judging
Severity: HIGH

Aymen0909

high

# `averagePrice` has wrong decimals due to `updatePricingInfo` wrong calculation
## Summary

The calculation of the `averagePrice` variable inside the `updatePricingInfo` function is wrong as the value `existing._initAcc` which is already in 18 decimals is multiplied again by 1e18 resulting in a `averagePrice` with 36 decimals, this error will cause issues as both `existing.fastFilterPrice ` and `existing.slowFilterPrice` are given the value of `averagePrice` (which is wrong) and are later used in the `getPrice` function.

## Vulnerability Detail

The issue occurs in the `updatePricingInfo` function below :

```solidity
function updatePricingInfo(IRootPriceOracle pricer, address token) internal {
    // tokenPricing info can be in 3 possible phases
    // 1) initialize phase, just want to accumulate the price to calculate an average
    // 2) exactly met the number of samples for init, update fast/slow filters with the average price
    // 3) post-init, only update the filter values
    TokenSnapshotInfo storage existing = tokenSnapshotInfo[token];

    // slither-disable-next-line timestamp
    if (existing.lastSnapshot + MIN_INTERVAL > block.timestamp) revert TokenSnapshotNotReady(token);

    // pricer handles reentrancy issues
    // slither-disable-next-line reentrancy-no-eth
    // @audit price is already in 18 decimals
    uint256 price = pricer.getPriceInEth(token);

    // update the timestamp no matter what phase we're in
    existing.lastSnapshot = uint40(block.timestamp);

    if (existing._initComplete) {
        // post-init phase, just update the filter values
        existing.slowFilterPrice = Stats.getFilteredValue(SLOW_ALPHA, existing.slowFilterPrice, price);
        existing.fastFilterPrice = Stats.getFilteredValue(FAST_ALPHA, existing.fastFilterPrice, price);
    } else {
        // still the initialization phase
        existing._initCount += 1;
        // @audit `existing._initAcc` will also be in 18 decimals
        existing._initAcc += price;

        // snapshot count is tracked internally and cannot be manipulated
        // slither-disable-next-line incorrect-equality
        if (existing._initCount == INIT_SAMPLE_COUNT) {
            // if this sample hits the target number, then complete initialize and set the filters
            existing._initComplete = true;
            // @audit should not multiply `existing._initAcc` by e18 => `averagePrice` is in e36
            uint256 averagePrice = existing._initAcc * 1e18 / INIT_SAMPLE_COUNT;
            existing.fastFilterPrice = averagePrice;
            existing.slowFilterPrice = averagePrice;
        }
    }

    emitSnapshotTaken(token, existing);
}
```

From the above code we can see that when `existing._initComplete == false` the value of `existing._initAcc` is incremented by the price returned from the root oracle `price`, we should also note that the `getPriceInEth` function always return a value with 18 decimals of precision. Hence the values of `pr
Aymen0909 - `queueNewRewards` transferring wrong amount of reward token
Audit: 2023-06-tokemak-judging
Severity: HIGH

Aymen0909

high

# `queueNewRewards` transferring wrong amount of reward token
## Summary

Upon invocation of the `queueNewRewards` function, an erroneous transfer of the `newRewards` amount occurs from the caller's account instead of the intended `startingNewRewards` value. This error results in an excessive deduction of funds from the caller, potentially leading to the call being reverted (creating a potential Denial of Service (DOS) situation, as the caller might lack the necessary funds) and inaccurate reward accounting.

## Vulnerability Detail

The issue occurs in the `queueNewRewards` function below :

```solidity
function queueNewRewards(uint256 newRewards) external onlyWhitelisted {
    uint256 startingQueuedRewards = queuedRewards;
    uint256 startingNewRewards = newRewards;

    // @audit value of newRewards changed
    newRewards += startingQueuedRewards;

    if (block.number >= periodInBlockFinish) {
        notifyRewardAmount(newRewards);
        queuedRewards = 0;
    } else {
        uint256 elapsedBlock = block.number - (periodInBlockFinish - durationInBlock);
        uint256 currentAtNow = rewardRate * elapsedBlock;
        uint256 queuedRatio = currentAtNow * 1000 / newRewards;

        if (queuedRatio < newRewardRatio) {
            notifyRewardAmount(newRewards);
            queuedRewards = 0;
        } else {
            queuedRewards = newRewards;
        }
    }

    emit QueuedRewardsUpdated(startingQueuedRewards, startingNewRewards, queuedRewards);

    // Transfer the new rewards from the caller to this contract.
    // @audit should only transfer `startingNewRewards` not `newRewards`
    IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), newRewards);
}
```

As we can see the `queueNewRewards` function receive the reward amount `newRewards` as input from the caller, the function at the start save this value into a memory variable `startingNewRewards` before incrementing it by the old queued rewards `startingQueuedRewards` (which is the same as `queuedRewards`).

So the `newRewards` amount now represent the new reward to be transferred plus the old queued amount `queuedRewards` (and not just the input rewards), but when the function tries to transfer the reward amout from the caller it still uses `newRewards` instead `startingNewRewards` thus asking the caller to give a larger reward amount at each call and not the amount he has given as input to the function (represented by `startingNewRewards`).

The impact of this issue is the following :

- Pulling a larger reward token amount from the caller at each call which will lead to a loss of funds for the caller and can lead to a revert of the call if the caller has insufficient amount which will result in a DOS.

- Wrong rewards accounting.

## Impact

See summary

## Code Snippet

https://github.com/sherlock-audit/2023-06-tokemak/blob/main/v2-core-audit-2023-07-14/src/rewarders/AbstractRewarder.sol#L235-L261

## Tool used

Manual Review

## Recommendation

T
0x73696d616f - Lost rewards claimed in `LiquidationRow` as the `swap()` function is not delegate called to in `liquidateVaultsForToken()`
Audit: 2023-06-tokemak-judging
Severity: HIGH

0x73696d616f

high

# Lost rewards claimed in `LiquidationRow` as the `swap()` function is not delegate called to in `liquidateVaultsForToken()`
## Summary
Can't send rewards to rewarders via `LiquidationRow:liquidateVaultsForToken()` because `_performLiquidation()` does not delegate call to the `asyncSwapper`, but uses a "normal" call instead. The claimed tokens will be forever stuck, as there is no way to withdraw them.

## Vulnerability Detail
`LiquidationRow` queues rewards to rewarders in 2 steps:
- `claimsVaultRewards()` goes through a list of vaults and collects the rewards to the `LiquidationRow`.
- `liquidateVaultsForToken()` fetches the previous claimed rewards, swaps them for the base asset and sends them to the rewarders pro-rata via `queueNewRewards()`.

Thus, if `liquidateVaultsForToken()` is broken, the claimed funds in `claimsVaultRewards()` are lost.

In this case, `liquidateVaultsForToken()` calls `IAsyncSwapper(asyncSwapper).swap(params);`, without ever approving the `asyncSwapper`. In fact, the `asyncSwapper` was expected to be delegate called to, not "regular" called. 

This bug was not found in the tests because the `asyncSwapper` was mocked in `LiquidationRow.t.sol`.

Tried to figure out a way to withdraw via the `feeReceiver`, by setting the fee to `100%`, but it's impossible, the path always ends up reverting.

## Impact
Lost rewards stuck in the `LiquidationRow` contract. 

## Code Snippet
- [`LiquidationRow:claimsVaultRewards()`](https://github.com/sherlock-audit/2023-06-tokemak/blob/main/v2-core-audit-2023-07-14/src/liquidation/LiquidationRow.sol#L104)
- `LiquidationRow` in `_performLiquidation()`, called by `liquidateVaultsForToken()`, [calling the `asyncSwapper`](https://github.com/sherlock-audit/2023-06-tokemak/blob/main/v2-core-audit-2023-07-14/src/liquidation/LiquidationRow.sol#L251) instead of delegate calling.
- [Diagram](https://app.diagrams.net/#G1s_vQgDn0cFG4PZSY_pkKUzk31h23Vs6k) explaining that the `asyncSwapper` should be delegate called to.
- `asyncSwapper` being [mocked](https://github.com/sherlock-audit/2023-06-tokemak/blob/main/v2-core-audit-2023-07-14/test/liquidators/LiquidationRow.t.sol#L46) in the tests.

## Tool used
Vscode
Foundry
Draw.io
Manual Review

## Recommendation
Delegate call to the `asyncSwapper`, similarly to the `LMPVaultRouter`:
```solidity
bytes memory data = asyncSwapper.functionDelegateCall(
    abi.encodeWithSignature("swap((address,uint256,address,uint256,bytes,bytes))", swapParams), "SwapFailed"
);
```

Duplicate of #205