Skip to content

Latest commit

 

History

History
95 lines (63 loc) · 4.19 KB

File metadata and controls

95 lines (63 loc) · 4.19 KB

Fast Khaki Raccoon

High

_swapForRewards can make the contract insolvent

Summary

_rewardsSwapAmountInOverride can be overwritten to REWARDS_SWAP_OVERRIDE_MIN which if a bigger value would cause insolvency

Root Cause

The catch overrides _rewardsSwapAmountInOverride to be REWARDS_SWAP_OVERRIDE_MIN, when we are _amountIn / 2 < REWARDS_SWAP_OVERRIDE_MIN

https://github.com/sherlock-audit/2025-01-peapods-finance/blob/main/contracts/contracts/TokenRewards.sol#L313-L318

        } catch {
            _rewardsSwapAmountInOverride = _amountIn / 2 < REWARDS_SWAP_OVERRIDE_MIN 
                ? REWARDS_SWAP_OVERRIDE_MIN 
                : _amountIn / 2;

            IERC20(PAIRED_LP_TOKEN).safeDecreaseAllowance(address(DEX_ADAPTER), _amountIn);
            emit RewardSwapError(_amountIn);
        }

However if we are way bellow this value it can turn out REWARDS_SWAP_OVERRIDE_MIN > _amountIn. And since we have set _rewardsSwapAmountInOverride already, the next time we invoke _swapForRewards we would enter the bellow case and possably fail the swap if REWARDS_SWAP_OVERRIDE_MIN is still bigger than our initial _amountIn.

https://github.com/sherlock-audit/2025-01-peapods-finance/blob/main/contracts/contracts/TokenRewards.sol#L293-L297

    function _swapForRewards(uint256 _amountIn, uint256 _amountOut, uint256 _adminAmt) internal {
        if (_rewardsSwapAmountInOverride > 0) {
            _adminAmt = (_adminAmt * _rewardsSwapAmountInOverride) / _amountIn;
            _amountOut = (_amountOut * _rewardsSwapAmountInOverride) / _amountIn;
            _amountIn = _rewardsSwapAmountInOverride;
        }

Even worse than a failed TX would be the case of insolvency, where there are enough balances to go for the swap, but the non-distributed rewards, aka _amountIn is smaller than _rewardsSwapAmountInOverride, which would pass the swap, but spend some of the rewards, which were accrued prev. cycles.

Internal Pre-conditions

No response

External Pre-conditions

No response

Attack Path

  1. We calculate _amountTkn to be 40
        uint256 _unclaimedPairedLpTkns = rewardsDeposited[PAIRED_LP_TOKEN] - rewardsDistributed[PAIRED_LP_TOKEN];
        
        uint256 _amountTkn = IERC20(PAIRED_LP_TOKEN).balanceOf(address(this)) - _unclaimedPairedLpTkns;
  1. Perform a swap, but fail, entering the catch and saving the new _rewardsSwapAmountInOverride to 100 (we are on ETH and gas may be a few bucks so, it won't be profitable if we swap 10 or 20 USD worth of tokens)
        } catch {
            _rewardsSwapAmountInOverride = _amountIn / 2 < REWARDS_SWAP_OVERRIDE_MIN 
                ? REWARDS_SWAP_OVERRIDE_MIN 
                : _amountIn / 2;

            IERC20(PAIRED_LP_TOKEN).safeDecreaseAllowance(address(DEX_ADAPTER), _amountIn);
            emit RewardSwapError(_amountIn);
        }
  1. Next time when depositFromPairedLpToken calculates _amountTkn to be 90 tokens

  2. We enter _swapForRewards and set the new swapped amount to _rewardsSwapAmountInOverride- 100 tokens

  3. The initial calculation for this was that we had 1000 LP tokens, but 910 were unclaimed, meaning that we have the balance to execute the swap

// 1000 - 910 = 90
uint256 _amountTkn = IERC20(PAIRED_LP_TOKEN).balanceOf(address(this)) - _unclaimedPairedLpTkns;
  1. The swap is executed, which causes insolvency in our PAIRED_LP_TOKEN as we have swapped more of it than there are _unclaimedPairedLpTkns ones, meaning that if all of the users who have it as a reward token try to claim it, they won't be able to, as contract balance is 900, but _unclaimedPairedLpTkns is 910.

This scenario will prob. occur multiple times, and can even be provoked by a malicious user if he just calls depositFromPairedLpToken 2 times in a row and reverts to 1st swap to enter the catch (can be pool price manipulation or anything else, doesn't matter how he reverts it).

Impact

Insolvency, last user/users not being able to claim their rewards.

PoC

No response

Mitigation

Consider redesigning this whole function. The concept of min swap is useful, but make sure to check our existing amount if they are enough to perform the swap.