In the scenario where the mode's canRepay status is set to false, positions using that mode cannot be repaid and liquidated. However, users are allowed to change their position's mode to one where the canRepay status is currently set to false. This could be exploited when a position owner observes that their position's health is approaching the liquidation threshold, allowing them to prevent liquidation.
Proof of Concept
It can be observed that when setPosMode is called, it check that newModeStatus.canBorrow and currentModeStatus.canRepay is set to true. However, it doesn't the status of newModeStatus.canRepay flag.
function setPosMode(uint _posId, uint16 _mode)
public
virtual
onlyAuthorized(_posId)
ensurePositionHealth(_posId)
nonReentrant
{
IConfig _config = IConfig(config);
// get current collaterals in the position
(address[] memory pools,, address[] memory wLps, uint[][] memory ids,) =
IPosManager(POS_MANAGER).getPosCollInfo(_posId);
uint16 currentMode = _getPosMode(_posId);
ModeStatus memory currentModeStatus = _config.getModeStatus(currentMode);
ModeStatus memory newModeStatus = _config.getModeStatus(_mode);
if (pools.length != 0 || wLps.length != 0) {
_require(newModeStatus.canCollateralize, Errors.COLLATERALIZE_PAUSED);
_require(currentModeStatus.canDecollateralize, Errors.DECOLLATERALIZE_PAUSED);
}
// check that each position collateral belongs to the _mode
for (uint i; i < pools.length; i = i.uinc()) {
_require(_config.isAllowedForCollateral(_mode, pools[i]), Errors.INVALID_MODE);
}
for (uint i; i < wLps.length; i = i.uinc()) {
for (uint j; j < ids[i].length; j = j.uinc()) {
_require(_config.isAllowedForCollateral(_mode, IBaseWrapLp(wLps[i]).lp(ids[i][j])), Errors.INVALID_MODE);
}
}
// get current debts in the position
uint[] memory shares;
(pools, shares) = IPosManager(POS_MANAGER).getPosBorrInfo(_posId);
IRiskManager _riskManager = IRiskManager(riskManager);
// check that each position debt belongs to the _mode
for (uint i; i < pools.length; i = i.uinc()) {
_require(_config.isAllowedForBorrow(_mode, pools[i]), Errors.INVALID_MODE);
_require(newModeStatus.canBorrow, Errors.BORROW_PAUSED);
_require(currentModeStatus.canRepay, Errors.REPAY_PAUSED);
// update debt on current mode
_riskManager.updateModeDebtShares(currentMode, pools[i], -shares[i].toInt256());
// update debt on new mode
_riskManager.updateModeDebtShares(_mode, pools[i], shares[i].toInt256());
}
// update position mode
IPosManager(POS_MANAGER).updatePosMode(_posId, _mode);
emit SetPositionMode(_posId, _mode);
}
As mentioned before, if users see his position's health status is about to reach liquidation threshold and change the mode, this will allow users to prevent their positions from getting liquidated, as both liquidate and liquidateWLp will check the canRepay flag and revert if it's not allowed.
function _repay(IConfig _config, uint16 _mode, uint _posId, address _pool, uint _shares)
internal
returns (address tokenToRepay, uint amt)
{
// check status
>>> _require(_config.getPoolConfig(_pool).canRepay && _config.getModeStatus(_mode).canRepay, Errors.REPAY_PAUSED);
// get position debt share
uint positionDebtShares = IPosManager(POS_MANAGER).getPosDebtShares(_posId, _pool);
uint sharesToRepay = _shares < positionDebtShares ? _shares : positionDebtShares;
// get amtToRepay (accrue interest)
uint amtToRepay = ILendingPool(_pool).debtShareToAmtCurrent(sharesToRepay);
// take token from msg.sender to pool
tokenToRepay = ILendingPool(_pool).underlyingToken();
IERC20(tokenToRepay).safeTransferFrom(msg.sender, _pool, amtToRepay);
// update debt on the position
IPosManager(POS_MANAGER).updatePosDebtShares(_posId, _pool, -sharesToRepay.toInt256());
// call repay on the pool
amt = ILendingPool(_pool).repay(sharesToRepay);
// update debt on mode
IRiskManager(riskManager).updateModeDebtShares(_mode, _pool, -sharesToRepay.toInt256());
emit Repay(_pool, _posId, msg.sender, _shares, amt);
}
Tools Used
Manual review
Recommended Mitigation Steps
Add a canRepay check status inside setPosMode; if it is paused, revert the change. Besides that, the canRepay and canBorrow checks don't need to be inside the pools check loop.
function setPosMode(uint _posId, uint16 _mode)
public
virtual
onlyAuthorized(_posId)
ensurePositionHealth(_posId)
nonReentrant
{
IConfig _config = IConfig(config);
// get current collaterals in the position
(address[] memory pools,, address[] memory wLps, uint[][] memory ids,) =
IPosManager(POS_MANAGER).getPosCollInfo(_posId);
uint16 currentMode = _getPosMode(_posId);
ModeStatus memory currentModeStatus = _config.getModeStatus(currentMode);
ModeStatus memory newModeStatus = _config.getModeStatus(_mode);
if (pools.length != 0 || wLps.length != 0) {
_require(newModeStatus.canCollateralize, Errors.COLLATERALIZE_PAUSED);
_require(currentModeStatus.canDecollateralize, Errors.DECOLLATERALIZE_PAUSED);
}
// check that each position collateral belongs to the _mode
for (uint i; i < pools.length; i = i.uinc()) {
_require(_config.isAllowedForCollateral(_mode, pools[i]), Errors.INVALID_MODE);
}
for (uint i; i < wLps.length; i = i.uinc()) {
for (uint j; j < ids[i].length; j = j.uinc()) {
_require(_config.isAllowedForCollateral(_mode, IBaseWrapLp(wLps[i]).lp(ids[i][j])), Errors.INVALID_MODE);
}
}
// get current debts in the position
uint[] memory shares;
(pools, shares) = IPosManager(POS_MANAGER).getPosBorrInfo(_posId);
IRiskManager _riskManager = IRiskManager(riskManager);
// check that each position debt belongs to the _mode
+ _require(newModeStatus.canBorrow, Errors.BORROW_PAUSED);
+ _require(currentModeStatus.canRepay, Errors.REPAY_PAUSED);
+ _require(newModeStatus.canRepay, Errors.REPAY_PAUSED);
for (uint i; i < pools.length; i = i.uinc()) {
_require(_config.isAllowedForBorrow(_mode, pools[i]), Errors.INVALID_MODE);
- _require(newModeStatus.canBorrow, Errors.BORROW_PAUSED);
- _require(currentModeStatus.canRepay, Errors.REPAY_PAUSED);
// update debt on current mode
_riskManager.updateModeDebtShares(currentMode, pools[i], -shares[i].toInt256());
// update debt on new mode
_riskManager.updateModeDebtShares(_mode, pools[i], shares[i].toInt256());
}
// update position mode
IPosManager(POS_MANAGER).updatePosMode(_posId, _mode);
emit SetPositionMode(_posId, _mode);
}
Lines of code
https://github.com/code-423n4/2023-12-initcapital/blob/main/contracts/core/InitCore.sol#L169-L213
Vulnerability details
Impact
In the scenario where the mode's
canRepay
status is set to false, positions using that mode cannot be repaid and liquidated. However, users are allowed to change their position's mode to one where thecanRepay
status is currently set to false. This could be exploited when a position owner observes that their position's health is approaching the liquidation threshold, allowing them to prevent liquidation.Proof of Concept
It can be observed that when
setPosMode
is called, it check thatnewModeStatus.canBorrow
andcurrentModeStatus.canRepay
is set to true. However, it doesn't the status ofnewModeStatus.canRepay
flag.https://github.com/code-423n4/2023-12-initcapital/blob/main/contracts/core/InitCore.sol#L203-L204
As mentioned before, if users see his position's health status is about to reach liquidation threshold and change the mode, this will allow users to prevent their positions from getting liquidated, as both
liquidate
andliquidateWLp
will check thecanRepay
flag and revert if it's not allowed.https://github.com/code-423n4/2023-12-initcapital/blob/main/contracts/core/InitCore.sol#L282-L314 https://github.com/code-423n4/2023-12-initcapital/blob/main/contracts/core/InitCore.sol#L317-L353 https://github.com/code-423n4/2023-12-initcapital/blob/main/contracts/core/InitCore.sol#L587-L599
https://github.com/code-423n4/2023-12-initcapital/blob/main/contracts/core/InitCore.sol#L530-L551
Tools Used
Manual review
Recommended Mitigation Steps
Add a
canRepay
check status insidesetPosMode
; if it is paused, revert the change. Besides that, thecanRepay
andcanBorrow
checks don't need to be inside the pools check loop.Assessed type
Invalid Validation