Borrower can potentially close liquidated position
This is not an actual issue right now. But, I'm not sure whether you guys are aware of this or not so I decided to include it in the report.
After a liquidator liquidated a position that was insured, the debtPrincipal value is not modified. If it would be, the user would be able to simply close the position to get their collateral back instead of repurchasing it.
Inside closePosition() the current checks whether the debt was repaid using require(_getDebtAmount(_nftIndex) == 0, "position_not_repaid");. Since the debt was paid off and the debtPrincipal value was unmodified, the _getDebtAmount() function returns the principal. A value that is != 0. Thus, the require statement fails. If you had set that value to zero, the user could simply call closePosition() and get their collateral back.
As I said this seems to be an unintended protection. At least, there are no comments suggesting this to be the reason for not setting debtPrincipal to zero.
An easy solution for this to be an explicit block would be to add the following line to the closePosition() function:
Supposed scenario where vault doesn't hold collateral
The closePosition() function has a comment and logic that suggests that someone could open a position without depositing the ERC721 token to the vault. When reading through the code I couldn't find a scenario where this was expected behaviour.
Other parts of the code where the collateral is transferred this scenario is also ignored. I expect this to be an outdated comment.
transfer() only provides the callee with 2300 gas. For EOA that's not an issue. But, if the receiver is a contract the transfer might fail. Depending on the contract's implementation of the receive() function.
Instead, it's recommended to use call() to transfer Ether.
The DAO is able to set the floor price through the overrideFloor() function. It's the designated function for that.
But, the setNFTTypeValueETH() function can also be used to modify the price. It only has an effect, if the floor oracle is already overwritten by the DAO since the function doesn't set the daoFloorOverride value.
Going by the functions and their comments, the setNFTTypeValueETH() function isn't supposed to be able to modify the floor price at all.
To fix it, add the following require statement: require(_type != bytes32(0));
StrategyPUSDConvex should claim rewards when withdrawing all the tokens
Currently, the withdrawAll() function doesn't claim the rewards. According to the function's comment, it is used primarily to switch strategies. In that case, there is no way for the controller to claim the rewards after the migration besides resetting the strategy to the StrategyPUSDConvex contract.
Since that seems a little tedious and not really wanted, I propose you claim your rewards when withdrawAll() is called. If, there is a scenario where you don't want to claim them you can also introduce a parameter with which you can specify whether to claim or not.
Report
call()
instead oftransfer()
to send ETHsetNFTTypeValueETH
allows setting the floor priceBorrower can potentially close liquidated position
This is not an actual issue right now. But, I'm not sure whether you guys are aware of this or not so I decided to include it in the report.
After a liquidator liquidated a position that was insured, the
debtPrincipal
value is not modified. If it would be, the user would be able to simply close the position to get their collateral back instead of repurchasing it.Inside
closePosition()
the current checks whether the debt was repaid usingrequire(_getDebtAmount(_nftIndex) == 0, "position_not_repaid");
. Since the debt was paid off and thedebtPrincipal
value was unmodified, the_getDebtAmount()
function returns the principal. A value that is!= 0
. Thus, the require statement fails. If you had set that value to zero, the user could simply callclosePosition()
and get their collateral back.As I said this seems to be an unintended protection. At least, there are no comments suggesting this to be the reason for not setting
debtPrincipal
to zero.An easy solution for this to be an explicit block would be to add the following line to the
closePosition()
function:The same thing is done in the
repay()
function.Relevant code:
Supposed scenario where vault doesn't hold collateral
The
closePosition()
function has a comment and logic that suggests that someone could open a position without depositing the ERC721 token to the vault. When reading through the code I couldn't find a scenario where this was expected behaviour.Other parts of the code where the collateral is transferred this scenario is also ignored. I expect this to be an outdated comment.
Relevant code:
Use
call()
instead oftransfer()
to send ETHtransfer()
only provides the callee with 2300 gas. For EOA that's not an issue. But, if the receiver is a contract the transfer might fail. Depending on the contract's implementation of thereceive()
function.Instead, it's recommended to use
call()
to transfer Ether.See https://swcregistry.io/docs/SWC-134
Relevant code:
setNFTTypeValueETH
allows setting the floor priceThe DAO is able to set the floor price through the
overrideFloor()
function. It's the designated function for that.But, the
setNFTTypeValueETH()
function can also be used to modify the price. It only has an effect, if the floor oracle is already overwritten by the DAO since the function doesn't set thedaoFloorOverride
value.Going by the functions and their comments, the
setNFTTypeValueETH()
function isn't supposed to be able to modify the floor price at all.Relevant code:
To fix it, add the following require statement:
require(_type != bytes32(0));
StrategyPUSDConvex should claim rewards when withdrawing all the tokens
Currently, the
withdrawAll()
function doesn't claim the rewards. According to the function's comment, it is used primarily to switch strategies. In that case, there is no way for the controller to claim the rewards after the migration besides resetting the strategy to the StrategyPUSDConvex contract.Since that seems a little tedious and not really wanted, I propose you claim your rewards when
withdrawAll()
is called. If, there is a scenario where you don't want to claim them you can also introduce a parameter with which you can specify whether to claim or not.Relevant code:
Proposed Changes
Change
withdrawAll()
to: