Orbiting Sangria Porpoise
Medium
NumaComptroller.sol
specifies that:
@--> // closeFactorMantissa must be strictly greater than this value
uint internal constant closeFactorMinMantissa = 0.05e18; // 0.05
@--> // closeFactorMantissa must not exceed this value
uint internal constant closeFactorMaxMantissa = 0.9e18; // 0.9
However this check is never implemented inside _setCloseFactor:
function _setCloseFactor(
uint newCloseFactorMantissa
) external returns (uint) {
// Check caller is admin
require(msg.sender == admin, "only admin can set close factor");
uint oldCloseFactorMantissa = closeFactorMantissa;
closeFactorMantissa = newCloseFactorMantissa;
emit NewCloseFactor(oldCloseFactorMantissa, closeFactorMantissa);
return uint(Error.NO_ERROR);
}
Someone could put forward an argument that this is just a cosmetic bound decided somewhat arbitrarily and even if the admin sets closeFactorMantissa
to say 1e18
or 0.9999e18
, there's no harm. In fact the current tests use closeFactorMantissa = 1e18
!
However that's not true. The following flow shows how an attack path emerges in such a scenario:
- Bob has
15 ether
of debt which has gone underwater. Let's assume admin has correctly setcloseFactorMantissa = 0.9e18
. - Alice tries to maliciously liquidate Bob partially for amount of
14.99999 ether
so that a dust amount of leftover debt remains. This is allowed by the logic here and here. As long ascurrent debt value > minBorrowAmountAllowPartialLiquidation
, partial liquidations are allowed. By doing so with many shortfall debts, she can flood the system with dust debts too unprofitable for anyone to liquidate, specially on a chain like Ethereum with high gas costs. - Alice's attempt is thwarted by the checks related to
maxClose
andcloseFactorMantissa
. She can't repay more than 90% of the debt in the worst case:maxClose
is calculated by multiplyingborrowBalance
withcloseFactorMantissa
- repayAmount > maxClose is not allowed by the protocol.
- Attack not possible because
closeFactorMantissa = 0.9e18
. But if it were1e18
, she could have repaid14.99999 ether
and the attack works. - With
closeFactorMantissa = 0.9e18
, the smallest leftover debt that can remain after an attacker's attempt is around1 ether
. This is because the protocol implements the minBorrowAmountAllowPartialLiquidation limit of10 ether
and partial repayment below this limit is not possible. So even if an attacker tries the attack whenborrowBalance = 10 ether + 1
, they will be able to repay only around9 ether
and not more.
Admin can inadvertently set it to values outside bounds and dilute/negate the protection mechanisms in place against avoidance of dust leftover debts. This issue is contingent on an admin error hence setting severity as medium.
function _setCloseFactor(
uint newCloseFactorMantissa
) external returns (uint) {
// Check caller is admin
require(msg.sender == admin, "only admin can set close factor");
+ require(newCloseFactorMantissa > closeFactorMinMantissa && newCloseFactorMantissa <= closeFactorMaxMantissa, "newCloseFactorMantissa outside limits");
uint oldCloseFactorMantissa = closeFactorMantissa;
closeFactorMantissa = newCloseFactorMantissa;
emit NewCloseFactor(oldCloseFactorMantissa, closeFactorMantissa);
return uint(Error.NO_ERROR);
}