Description
L525 inside finalizeLiquidation() tries to transfer crvUSD from stability pool to itself. Also, prior to this L457-L461 inside liquidateBorrower() of StabilityPool.sol checks crvUSD balance & grants allowance to LendingPool.sol:
File: contracts/core/pools/LendingPool/LendingPool.sol
496: function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
497: if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
498:
499:
500: ReserveLibrary.updateReserveState(reserve, rateData);
501:
502: if (block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod) {
503: revert GracePeriodNotExpired();
504: }
505:
506: UserData storage user = userData[userAddress];
507:
508: uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
509:
510:
511: isUnderLiquidation[userAddress] = false;
512: liquidationStartTime[userAddress] = 0;
513:
514: for (uint256 i = 0; i < user.nftTokenIds.length; i++) {
515: uint256 tokenId = user.nftTokenIds[i];
516: user.depositedNFTs[tokenId] = false;
517: raacNFT.transferFrom(address(this), stabilityPool, tokenId);
518: }
519: delete user.nftTokenIds;
520:
521:
522: (uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
523:
524:
525:@---> IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
526:
527:
528: user.scaledDebtBalance -= amountBurned;
529: reserve.totalUsage = newTotalSupply;
530:
531:
532: ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
533:
534:
535: emit LiquidationFinalized(stabilityPool, userAddress, userDebt, getUserCollateralValue(userAddress));
536: }
and
File: contracts/core/pools/StabilityPool/StabilityPool.sol
449: function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
450: _update();
451:
452: uint256 userDebt = lendingPool.getUserDebt(userAddress);
453: uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
454:
455: if (userDebt == 0) revert InvalidAmount();
456:
457:@---> uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
458: if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
459:
460:
461:@---> bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
462: if (!approveSuccess) revert ApprovalFailed();
463:
464: lendingPool.updateState();
465:
466:
467: lendingPool.finalizeLiquidation(userAddress);
468:
469: emit BorrowerLiquidated(userAddress, scaledUserDebt);
470: }
However there is no flow or function which deposits crvUSD into StabilityPool.sol at any point of time. The deposit by a user is of rToken (and RAAC reward tokens also make their way into the contract).
File: contracts/core/pools/StabilityPool/StabilityPool.sol
170:
➡️➡️ 171: * @notice Allows a user to deposit rToken and receive deToken.
172: * @param amount Amount of rToken to deposit.
173: */
174: function deposit(uint256 amount) external nonReentrant whenNotPaused validAmount(amount) {
175: _update();
176: rToken.safeTransferFrom(msg.sender, address(this), amount);
177: uint256 deCRVUSDAmount = calculateDeCRVUSDAmount(amount);
178: deToken.mint(msg.sender, deCRVUSDAmount);
179:
180: userDeposits[msg.sender] += amount;
181: _mintRAACRewards();
182:
183: emit Deposit(msg.sender, amount, deCRVUSDAmount);
184: }
So the code will fail at both the aforementioned points:
Impact
Can't liquidate a bad debt.
Mitigation
Either pull crvUSD into the StabilityPool contract from LendingPool when rTokens are deposited
OR add a function inside StabilityPool which can sell the collateral NFTs transferred into it to increase its crvUSD balance
OR change the mechanics to operate on rToken instead of crvUSD