Light ModeLight
Light ModeDark

One Bug Per Day

One H/M every day from top Wardens

Checkmark

Join over 1125 wardens!

Checkmark

Receive the email at any hour!

Ad

Wrong use of nftID to check if a Power farm position is an Aave position

criticalCode4rena

Lines of code

https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarm/PendlePowerFarm.sol#L127-L129 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarm/PendlePowerFarm.sol#L64-L70 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarm/PendlePowerFarmLeverageLogic.sol#L575-L590 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarm/PendlePowerFarmMathLogic.sol#L396-L402

Vulnerability details

Vulnerability Details

When a Power Farm position is created its keyId is used as a key in the isAave mapping to indicate if it is an Aave position or not. The keyId is the index of the Power Farm NFT linked with the position.

PendlePowerManager.sol#L129-L130

solidity
isAave[keyId] = _isAave;

The keyId is also linked with another nft Id. This other nft Id is used to hold the keyIds Power Farm position in the WiseLending contract. They are linked together in the farmingKeys mapping of the MinterReserver contract.

MinterReserver.sol#L88C1-L91C46

solidity
uint256 keyId = _getNextReserveKey(); reservedKeys[_userAddress] = keyId; farmingKeys[keyId] = _wiseLendingNFT;

The issue is the check if a position is an Aave position is done using the WiseLending nft Id instead of the Power Farm's keyId. This occurs five times in the code:

It is used in getLiveDebtRatio() to know the pool token borrowed so the borrowShares can be retrieved.

PendlePowerFarm.sol#L64-L70

solidity
uint256 borrowShares = isAave[_nftId] ? _getPositionBorrowSharesAave( _nftId ) : _getPositionBorrowShares( _nftId );

It is used in _manuallyPaybackShares() to know the pool token to pay back.

PendlePowerFarm.sol#L127-L129

solidity
if (isAave[_nftId] == true) { poolAddress = AAVE_WETH_ADDRESS; }

It is used in checkDebtRatio() to know the pool token borrowed so the borrowShares can be retrieved.

PendlePowerFarmMathLogic.sol#L396-L402

solidity
uint256 borrowShares = isAave[_nftId] ? _getPositionBorrowSharesAave( _nftId ) : _getPositionBorrowShares( _nftId );

It is used in _coreLiquidation() to select a token to payback and to know the pool token borrowed so the borrowShares can be retrieved.

PendlePowerFarmLeverageLogic.sol#L575-L590

solidity
address paybackToken = isAave[_nftId] == true ? AAVE_WETH_ADDRESS : WETH_ADDRESS; paybackAmount = WISE_LENDING.paybackAmount( paybackToken, _shareAmountToPay ); uint256 cutoffShares = isAave[_nftId] == true ? _getPositionBorrowSharesAave(_nftId) * FIVTY_PERCENT / PRECISION_FACTOR_E18 : _getPositionBorrowShares(_nftId) * FIVTY_PERCENT / PRECISION_FACTOR_E18;

These have the following effects:

  • For getLiveDebtRatio(), users would get zero when they try to retrieve their debt ratio.
  • For _manuallyPaybackShares() users won't be able to pay back their shares manually from the PowerFarm contract since it'll fetch zero shares as borrow shares.
  • The last two instances are used in liquidation and allow a malicious user to have a position that can't be liquidated even though it is eligible for liquidation. The malicious user can:
    1. Create an Aave Power Farm position.
    2. The position becomes eligible for liquidation after some price changes.
    3. Liquidators cannot liquidate the position because the call to _coreLiquidation first calls checkDebtRatio() which uses the wrong borrowShares to calculate the debt ratio and returns true. Thus causing a revert.

PendlePowerFarmLeverageLogic.sol#L571C1-L574C1

solidity
if (_checkDebtRatio(_nftId) == true) { revert DebtRatioTooLow(); }

Impact

  1. Malicious users can open positions that can't get liquidated.
  2. Users can't pay back manually when it is an Aave position.
  3. getLiveDebtRatio() returns zero always when it is an Aave position.

Proof of Concept

There are 3 tests below and they can all be run in PendlePowerFarmControllerBase.t.sol.

  • testAaveGetLiveDebtRatio() shows that getLiveDebtRatio() returns zero
  • testAaveManuallyPayback() shows that borrowed tokens can't be paid back using manuallyPaybackShares()
  • testCannotLiquidate() shows that Aave positions cannot be liquidated.
solidity
function testAaveGetLiveDebtRatio() public cheatSetup(true){ _prepareAave(); uint256 keyID = powerFarmManagerInstance.enterFarm( true, 1 ether, 15 ether, entrySpread ); uint nftId = powerFarmManagerInstance.farmingKeys(keyID); // gets borrow shares of weth instead of aeth uint ratio = powerFarmManagerInstance.getLiveDebtRatio(nftId); assertEq(ratio, 0); } function testAaveManuallyPayback() public cheatSetup(true){ _prepareAave(); uint256 keyID = powerFarmManagerInstance.enterFarm( true, 1 ether, 15 ether, entrySpread ); uint nftId = powerFarmManagerInstance.farmingKeys(keyID); uint borrowShares = wiseLendingInstance.getPositionBorrowShares(nftId, AWETH); // tries to payback weth instead of aweth and reverts with an arithmetic underflow // since the position has 0 weth borrow shares vm.expectRevert(); powerFarmManagerInstance.manuallyPaybackShares(keyID, borrowShares); } error DebtRatioTooLow(); function testCannotLiquidate() public cheatSetup(true){ _prepareAave(); uint256 keyID = powerFarmManagerInstance.enterFarm( true, 1 ether, 15 ether, entrySpread ); // increase collateral factors to make position eligible for liquidation wiseLendingInstance.setPoolParameters(AWETH, 99e16, type(uint256).max); // increasw Wiselending coll factor vm.store(address(powerFarmManagerInstance), bytes32(uint(2)), bytes32(uint(99e16))); //increasw PowerFarm coll factor assertEq(powerFarmManagerInstance.collateralFactor(), 99e16); uint nftId = powerFarmManagerInstance.farmingKeys(keyID); uint borrowShares = wiseLendingInstance.getPositionBorrowShares(nftId, AWETH); // will revert if it can't be liquidated wiseSecurityInstance.checksLiquidation(nftId, AWETH, borrowShares); uint nftIdLiquidator = positionNftsInstance.mintPosition(); vm.expectRevert(DebtRatioTooLow.selector); powerFarmManagerInstance.liquidatePartiallyFromToken(nftId, nftIdLiquidator, borrowShares ); }

Tools Used

Manual Analysis

Recommended Mitigation Steps

Consider checking if a position is an Aave position using the keyId of the position.

PendlePowerFarm.sol#L64-L70

solidity
- uint256 borrowShares = isAave[_nftId] + uint256 borrowShares = isAave[keyId] ? _getPositionBorrowSharesAave( _nftId ) : _getPositionBorrowShares( _nftId );

PendlePowerFarm.sol#L127-L129

solidity
- if (isAave[_nftId] == true) { + if (isAave[keyId] == true) { poolAddress = AAVE_WETH_ADDRESS; }

PendlePowerFarmMathLogic.sol#L396-L402

solidity
- uint256 borrowShares = isAave[_nftId] + uint256 borrowShares = isAave[keyId] ? _getPositionBorrowSharesAave( _nftId ) : _getPositionBorrowShares( _nftId );

PendlePowerFarmLeverageLogic.sol#L575-L590

solidity
- address paybackToken = isAave[_nftId] == true + address paybackToken = isAave[keyId] == true ? AAVE_WETH_ADDRESS : WETH_ADDRESS; paybackAmount = WISE_LENDING.paybackAmount( paybackToken, _shareAmountToPay ); - uint256 cutoffShares = isAave[_nftId] == true + uint256 cutoffShares = isAave[keyId] == true ? _getPositionBorrowSharesAave(_nftId) * FIVTY_PERCENT / PRECISION_FACTOR_E18 : _getPositionBorrowShares(_nftId) * FIVTY_PERCENT / PRECISION_FACTOR_E18;

Assessed type

Other