Function FujiOracle._getUSDPrice() requests the price from chainlink oracle with a given asset. The chainlink then returns the price of 1 "real" token asset (not 1 wei) in usd which means with 10^asset_decimal asset how much usd you can get. From that definition, the function FujiOracle.getPriceOf(address currencyAsset, address commodityAsset, decimals) returns the value:
commodityPrice is the price in usd of 1 wei commodityAsset
currencyPrice is the price in usd of 1 wei currencyAsset
This information is decent to talk about the issue with function BorrowingVault.liquidate(). The issue came from the calculation of variable gainedShares which I reckon that the developer mistakenly consider the decimals() of the asset token is 18.
I write down these 3 lines on a piece of paper for more detail about the formula.
The problem is about the variable "x" in the image. "x" is the corresponding asset which will be repaid when liquidation happens. As we can see on the last red line of the paper, the correspondAsset * (liqFactor / 10^18) / (penalty / 10^18) is the correct value we want to assign to x. Unfortunately there is a weird fraction 10^18 / 10^decimal appearing in the result which made the whole calculation incorrect.
gainedShares = convertToShares(x), which:
x = Math.mulDiv(debt, liquidationFactor, discountedPrice)
<=> x(USDC) = debts(WETH) * 1e18 (liqFactor) / 6e14 (discountPrice)
given debts(WETH) = 1e18
==> x(USDC) = 1e18 * 1e18 / 6e14 ~= 1666e18
(too high, expect 1666e6 because the decimal of USDC in 6)
<=> gainedShares = convertToShares(gainedAssets) = convertToShares(1666e18)
(can't burn min(gainedShares, existingShares) since the owner can still have some debt remain (liquidationFactor < 10^18))
Attack scenario
if the decimal is 18, there won't be any problem with the liquidation. But when the decimal < 18, it will make the calculated gainedShares bigger than expected. It can make the tx revert since the amount of burning share can exceed the maxRedeem(owner) (the burning share in this case will be equal to the share balance of owner but owner can still own some debt share after the liquidation -- liquidationFactor < 10^18).
==> Lead to DOS when calling liquidation ==> bad debt
See the PR: https://github.com/Fujicracy/fuji-v2/pull/312
Note: I saw that you guy just used 18 decimals to describe the tests. I recommend adding more tests with other decimals for diversification. It can help to cover more edge cases though.
Title
Incorrect gainedShared calculation when call function BorrowingVault.liquidate
Affected smart contract
https://github.com/Fujicracy/fuji-v2/blob/1b939ec84af137db430fc2aa1b4c6f15e5254003/packages/protocol/src/vaults/borrowing/BorrowingVault.sol#L604-L606
Description
Function
FujiOracle._getUSDPrice()
requests the price from chainlink oracle with a given asset. The chainlink then returns the price of 1 "real" token asset (not 1 wei) in usd which means with10^asset_decimal
asset how much usd you can get. From that definition, the functionFujiOracle.getPriceOf(address currencyAsset, address commodityAsset, decimals)
returns the value:In which:
This information is decent to talk about the issue with function
BorrowingVault.liquidate()
. The issue came from the calculation of variablegainedShares
which I reckon that the developer mistakenly consider thedecimals()
of the asset token is 18.I write down these 3 lines on a piece of paper for more detail about the formula.
The problem is about the variable "x" in the image. "x" is the corresponding asset which will be repaid when liquidation happens. As we can see on the last red line of the paper, the
correspondAsset * (liqFactor / 10^18) / (penalty / 10^18)
is the correct value we want to assign to x. Unfortunately there is a weird fraction10^18 / 10^decimal
appearing in the result which made the whole calculation incorrect.For example, assume that:
currencyAsset
= WETH (debtAsset
)commodityAsset
= USDC (asset
)decimals = 18
LIQUIDATION_PENALTY = 0.9e18
liquidationFactor = 1e18
_getUSDPrice(USDC) = 1e8
and_getUSDPrice(WETH) = 1500e8
=>price = oracle.getPriceOf(WETH, USDC, 1e18) = 1e18 * 1e8 / 1500e8 ~= 6.6e14
=>discountPrice ~= 6e14
We have:
Attack scenario
if the
decimal
is 18, there won't be any problem with the liquidation. But when thedecimal < 18
, it will make the calculatedgainedShares
bigger than expected. It can make the tx revert since the amount of burning share can exceed themaxRedeem(owner)
(the burning share in this case will be equal to the share balance of owner but owner can still own some debt share after the liquidation -- liquidationFactor < 10^18). ==> Lead to DOS when calling liquidation ==> bad debt See the PR: https://github.com/Fujicracy/fuji-v2/pull/312Recommendation
Modify the calculation as follows:
Note: I saw that you guy just used 18 decimals to describe the tests. I recommend adding more tests with other decimals for diversification. It can help to cover more edge cases though.