Closed c4-bot-7 closed 5 months ago
- Bob has a reserved
PositionNFT
of Id:5 and he wants to leverage his gains.- Bob enters the power farm by calling
enterFarm
- Bob's
wiselendingNFT
is enumerated to him by internally calling _getWiseLendingNFT at L:107.- Since there's no available NFT in the system (availableNFTCount == 0 condition), his
wiselendingNFT
id becomes 5 as per his Position NFT due to the logic below;
author of this finding makes a critical assumption and puts wrong statement here without really understanding the code. here are the false assumptions:
1) when Bob enters the farm by calling enterFarm
Bob will get keyId 1
that would point to wiseLending nftId 6
, not 5
as was claimed, this is because wiseLending nftId 5
is already reserved for Bob and it is powerFarm that calls to wiseLending to mint a new NFT not Bob directly.
2) what will happen is that powerFarm will request a new wiseLending nftId to be minted into self and that is the one that is going to be assigned to Bob through keyId, Bob cannot use wiseLending nftId 5
in the farm.
later author of this "finding" continues false path of saying that _mintPositionForUser
will be used to mint nft for Bob, but if you look closely to the code it will mint nft into the FARM and it will be nftId
6
the flaw in submiters "finding" is also in saying that
L:191 calls positionNFTs contract and it goes the flow like below; mintPosition -> _mintPositionForUser
here the paramter will be powerFarm address not Bobs address is passed when calling mintPositionForUser
based on false assumptions provided in the middle of the finding the rest can be easily dismissed and marked as invalid finding, otherwise feel free to provide POC working foundry file showing your "finding" proof.
GalloDaSballo marked the issue as insufficient quality report
trust1995 marked the issue as unsatisfactory: Insufficient proof
Lines of code
https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarm/PendlePowerManager.sol#L235-L237
Vulnerability details
Impact
Loss of user funds
Proof of Concept
The
PowerFarmNFT
s are vital for the protocol as the positions being held in the farms are represented by the ID of thePowerFarmNFT
.If the user wants to leverage their gains by entering Power Farms they call;
enterFarm - The NFT id is assigned by calling _getWiseLendingNFT
enterFarmETH - The NFT id is assigned by calling _getWiseLendingNFT
Let´s check the user flow to understand how the positions are handled.
Alice holds a Position NFT and she wants to leverage her gains by the farms.
enterFarm
logic is below;L: 103 & L: 104 are the modifiers and they do the following; isActive : Checks whether the system has been shut down. updatePools : Checks whether there's a re-entrancy & syncs the pools.
L: 107 calls _getWiseLendingNFT to obtain the WiseLendingNFTId. The function logic is as below;
185: function _getWiseLendingNFT() 186: internal 187: returns (uint256) 188: { 189: if (availableNFTCount == 0) { 190: 191: uint256 nftId = POSITION_NFT.mintPosition(); 192: 193: _registrationFarm( 194: nftId 195: ); 196: 197: POSITION_NFT.approve( 198: AAVE_HUB_ADDRESS, 199: nftId 200: ); 201: 202: return nftId; 203: } 204: 205: return availableNFTs[ 206: availableNFTCount-- 207: ]; 208: }
The user can manage their position by this
keyId
.When the time comes;
Alice wants to exit her position and calls exitFarm
Alice's ownership is validated in the
onlyKeyOwner
modifier. The function flow for this modifier is;onlyKeyOwner
->_onlyKeyOwner
->require(isOwner)
and isOWner function is as below;After the keyId ownership is verified, we can have a deeper look at
exitFarm
function logic;210: function exitFarm( 211: uint256 _keyId, 212: uint256 _allowedSpread, 213: bool _ethBack 214: ) 215: external 216: updatePools 217: onlyKeyOwner(_keyId) 218: { 219: uint256 wiseLendingNFT = farmingKeys[ 220: _keyId 221: ]; 222: 223: delete farmingKeys[ 224: _keyId 225: ]; 226: 227: if (reservedKeys[msg.sender] == _keyId) { 228: reservedKeys[msg.sender] = 0; 229: } else { 230: FARMS_NFTS.burnKey( 231: _keyId 232: ); 233: } 234: 235: availableNFTs[ 236: ++availableNFTCount 237: ] = wiseLendingNFT; 238: 239: _closingPosition( 240: isAave[_keyId], 241: wiseLendingNFT, 242: _allowedSpread, 243: _ethBack 244: ); 245: 246: emit FarmExit( 247: _keyId, 248: wiseLendingNFT, 249: _allowedSpread, 250: block.timestamp 251: ); 252: }
L:191 calls
positionNFTs
contract and it goes the flow like below;mintPosition
->_mintPositionForUser
Since Bob has a reserved Position NFT, L:199 returns his reserved NFT id = 5.
_reserveKey
between the Lines: 124 to 127_reserveKey
function logic is as follows;77: function _reserveKey( 78: address _userAddress, 79: uint256 _wiseLendingNFT 80: ) 81: internal 82: returns (uint256) 83: { 84: if (reservedKeys[_userAddress] > 0) { 85: revert AlreadyReserved(); 86: } 87: 88: uint256 keyId = _getNextReserveKey(); 89: 90: reservedKeys[_userAddress] = keyId; 91: farmingKeys[keyId] = _wiseLendingNFT; 92: 93: return keyId; 94: }
Let's assume that there are 2 minted positions and 3
totalReserved
positions at this call. So it will return as 2 (totalMinted) + (3+1)(++totalReserved) = 6Accordingly, Lines 90 & 91 will be;
after this call.
mintReserved
internally calls_mintKeyForUser
with the logic below;115: function _mintKeyForUser( 116: uint256 _keyId, 117: address _userAddress 118: ) 119: internal 120: returns (uint256) 121: { 122: if (_keyId == 0) { 123: revert InvalidKey(); 124: } 125: 126: > delete reservedKeys[ 127: > _userAddress 128: ]; 129: 130: FARMS_NFTS.mintKey( 131: _userAddress, 132: _keyId 133: ); 134: 135: totalMinted++; 136: totalReserved--; 137: 138: return _keyId; 139: }
L:135 increments the
totalMinted
and makes it 3L:136 decrements the
totalReserved
and makes it 3exitFarm
as below;210: function exitFarm( 211: uint256 _keyId, 212: uint256 _allowedSpread, 213: bool _ethBack 214: ) 215: external 216: updatePools 217: onlyKeyOwner(_keyId) 218: { 219: uint256 wiseLendingNFT = farmingKeys[ 220: _keyId 221: ]; 222: 223: > delete farmingKeys[ 224: _keyId 225: ]; 226: 227: if (reservedKeys[msg.sender] == _keyId) { 228: reservedKeys[msg.sender] = 0; 229: } else { 230: > FARMS_NFTS.burnKey( 231: _keyId 232: ); 233: } 234: 235: availableNFTs[ 236: ++availableNFTCount 237: ] = wiseLendingNFT; 238: 239: _closingPosition( 240: isAave[_keyId], 241: wiseLendingNFT, 242: _allowedSpread, 243: _ethBack 244: ); 245: 246: emit FarmExit( 247: _keyId, 248: wiseLendingNFT, 249: _allowedSpread, 250: block.timestamp 251: ); 252: }
enterFarm
with 20 WETH_getWiseLendingNFT
assigns Alice the available NFT id left by Bob (5) and decrements the mapping afterwards while leaving no NFT available.Alice's wiselendingNFT becomes 5.
_reserveKey
function sets the mappings for Alice as below;enterFarm
but with min amount that the protocol allows him. It´s 3 ETH._getWiseLendingNFT
assigns the nftID as Bob's Position NFT id again due to there's no available Power Farm NFT id. Bob's wiselendingNFT becomes 5 too.enterFarm
's logic below happens;109: _safeTransferFrom( //@audit 3 WETH is transferred from Bob. 110: WETH_ADDRESS, 111: msg.sender, 112: address(this), 113: _amount 114: ); 115: 116: _openPosition( // @audit 117: _isAave, 118: wiseLendingNFT, 119: _amount, 120: _leverage, 121: _allowedSpread 122: );