v2 Distributor

Changelog

Reasons for changes

The Aave protocol, when calling supply(), may return a smaller amount of aToken than was deposited. This leads to the fact that the lastUnderlyingBalance is less than the deposited amount. This is unexpected behavior that causes an error in the Distributor contract code during staking, withdrawal, or claiming operations. An example of such a transaction will be provided below.

Transaction - here.

Location of error, Distributor.sol -> _withdrawYield() :

 uint256 yield_ = depositPool.lastUnderlyingBalance - depositPool.deposited;

Yield

Yield is calculated based on the amount of aToken, which may be a few wei higher or lower than the amount of tokens deposited. This leads to minimal inaccuracies in yield calculation, which is acceptable.

In practice, this means that if a user stake, for example, 100 USDC, the contract may receive 100.1 aUSDC. The yield will be calculated as 0.1 aUSDC and will not be available to the user immediately. This means that if the user stakes and then immediately tries to withdraw the deposited tokens, they may encounter an error on the smart contract, because 100 aUSDC will not be exactly equal to 100 USDC. The solution is to wait a few minutes for the yield from the Aave protocol to be accrued. This will only occur if the user is the last staker and attempts to withdraw all of their tokens after the stake.

Solution

It is necessary to track the aToken balance independently from the deposit token, separating the amount_ specified during deposit and withdrawal, and the one recorded in underlyingAmount_.

Pull request: https://github.com/MorpheusAIs/SmartContracts/pull/60.

Consequences

We do not see any critical consequences from this issue. Only one deposit pool (wBTC) was affected and is currently unable to operate due to the error described above. Other deposit pools are functioning since the amount of aToken is greater than the deposited amount, which does not violate the contract logic. After the update, the distributeRewards() function will overwrite the lastUnderlyingBalance variable to the most recent value.

Code changelog

supply()

A calculation of the actual amount of aToken received by the Distributor contract has been added to the supply() function. Thus, in the case of the Aave pool, we will add the actual aTokens received to the last calculated balance. The deposit token is deducted in the amount specified by the user (without changes). For stETH, the underlyingAmount_ variable is set equal to the deposit amount (without changes).

function supply(
  uint256 rewardPoolIndex_, 
  address holder_, 
  uint256 amount_
) external returns (uint256) {
  ...

  uint256 underlyingAmount_ = amount_;
  if (depositPool.strategy == Strategy.AAVE) {
    ...
    uint256 underlyingTokenBalanceBefore_ = IERC20(depositPool.aToken).balanceOf(address(this));
    AaveIPool(aavePool_).supply(depositPool.token, amount_, address(this), 0);
    uint256 underlyingTokenBalanceAfter_ = IERC20(depositPool.aToken).balanceOf(address(this));
    underlyingAmount_ = underlyingTokenBalanceAfter_ - underlyingTokenBalanceBefore_;
  }

  ...
  depositPool.lastUnderlyingBalance += underlyingAmount_;

  return amount_;
}

withdraw()

When withdrawing the deposit token, the exact amount check, which was previously implemented only for stETH, has now been applied to all deposit pools. This was done to improve security, as Aave declares the "exact" withdrawal amount in the return value, and it can be assumed that this amount may differ. Additionally, a calculation has been added to determine the precise amount of aToken spent.

function withdraw(
  uint256 rewardPoolIndex_,
  address receiver_,
  uint256 amount_
) external returns (uint256) {
  ...
  uint256 underlyingAmount_ = amount_;

  uint256 depositTokenBalanceBefore_ = IERC20(depositPool.token).balanceOf(receiver_);
  if (depositPool.strategy == Strategy.AAVE) {
    uint256 underlyingTokenBalanceBefore_ = IERC20(depositPool.aToken).balanceOf(address(this));
    AaveIPool(AaveIPoolAddressesProvider(aavePoolAddressesProvider).getPool()).withdraw(
      depositPool.token,
      amount_,
      receiver_
    );
    uint256 underlyingTokenBalanceAfter_ = IERC20(depositPool.aToken).balanceOf(address(this));
    underlyingAmount_ = underlyingTokenBalanceBefore_ - underlyingTokenBalanceAfter_;
  } else {
    IERC20(depositPool.token).safeTransfer(receiver_, amount_);
  }
  uint256 depositTokenBalanceAfter_ = IERC20(depositPool.token).balanceOf(receiver_);
  amount_ = depositTokenBalanceAfter_ - depositTokenBalanceBefore_;

  depositPool.deposited -= amount_;
  depositPool.lastUnderlyingBalance -= underlyingAmount_;

  ...
}

_withdrawYield()

Processing has been added to ensure that the lastUnderlyingBalance_ amount must be greater than the deposited_ amount.

function _withdrawYield(
  uint256 rewardPoolIndex_, 
  address depositPoolAddress_
) private {
  ...
  uint256 lastUnderlyingBalance_ = depositPool.lastUnderlyingBalance;
  uint256 deposited_ = depositPool.deposited;
  if (lastUnderlyingBalance_ <= deposited_) {
    return;
  }

  uint256 yield_ = lastUnderlyingBalance_ - deposited_;
  if (depositPool.strategy == Strategy.AAVE) {
    uint256 underlyingTokenBalanceBefore_ = IERC20(depositPool.aToken).balanceOf(address(this));
    AaveIPool(AaveIPoolAddressesProvider(aavePoolAddressesProvider).getPool()).withdraw(
      depositPool.token,
      yield_,
      l1Sender
    );
    uint256 underlyingTokenBalanceAfter_ = IERC20(depositPool.aToken).balanceOf(address(this));
    yield_ = underlyingTokenBalanceBefore_ - underlyingTokenBalanceAfter_;
  } else {
    ...
  }
  ...
}

Other

Some internal variable names were also changed, and the version and name of the smart contract were updated (Distributor -> DistributorV2).

Last updated

Was this helpful?