Wrong use of nftID to check if a Power farm position is an Aave position
criticalLines 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
solidityisAave[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
solidityuint256 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.
solidityuint256 borrowShares = isAave[_nftId] ? _getPositionBorrowSharesAave( _nftId ) : _getPositionBorrowShares( _nftId );
It is used in _manuallyPaybackShares() to know the pool token to pay back.
solidityif (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:
- Create an Aave Power Farm position.
- The position becomes eligible for liquidation after some price changes.
- Liquidators cannot liquidate the position because the call to
_coreLiquidationfirst callscheckDebtRatio()which uses the wrong borrowShares to calculate the debt ratio and returns true. Thus causing a revert.
PendlePowerFarmLeverageLogic.sol#L571C1-L574C1
solidityif (_checkDebtRatio(_nftId) == true) { revert DebtRatioTooLow(); }
Impact
- Malicious users can open positions that can't get liquidated.
- Users can't pay back manually when it is an Aave position.
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 thatgetLiveDebtRatio()returns zerotestAaveManuallyPayback()shows that borrowed tokens can't be paid back usingmanuallyPaybackShares()testCannotLiquidate()shows that Aave positions cannot be liquidated.
solidityfunction 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.
solidity- uint256 borrowShares = isAave[_nftId] + uint256 borrowShares = isAave[keyId] ? _getPositionBorrowSharesAave( _nftId ) : _getPositionBorrowShares( _nftId );
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
