Open sherlock-admin4 opened 4 weeks ago
1 comment(s) were left on this issue during the judging contest.
Honour commented:
Possibly invalid along with dupes #137 #148 #390. Code is a fork of AAVE and aave doesn't implement a min. borrow. Cant't drain pool as it would require 10^9 (txs/ loops in a single tx) to borrow 1e18 token worth of debt. it's difficult to say how much can actually be borrowed using this.
I think this should be a unique issue on its own
Possible duplicates:
While these issues had some similarity with this report, there's no mention how user can "get away" by withdrawing the initial supplied amount.
I believe it became a whole other issue when user can get away by withdrawing their initial supplied amount.When the pool implementation is upgraded which includes the health check fix by the admin, they could possibly be liquidated if the user didn't withdraw.
Also why is this not a high vulnerability ? This is a direct theft of user funds and protocol which could spread like a wildfire if many people discovered this vulnerability knowing the simplicity to execute the attack which eventually could trigger a bank-run.
Escalate
This is a good catch, but the impact does not meet the criteria for medium severity.
Here is the code where the precision loss occurs:
uint256 userTotalDebt = balance.debtShares;
if (userTotalDebt != 0) userTotalDebt = userTotalDebt.rayMul(reserve.getNormalizedDebt());
userTotalDebt = assetPrice * userTotalDebt;
unchecked {
return userTotalDebt / assetUnit;
}
For precision loss, we require userTotalDebt < assetUnit
in the last line.
This means we require assetPrice*userTotalDebt < assetUnit
This means that userTotalDebt < assetUnit
where assetPrice
is the price returned by getAssetPrice()
which is from the chainlink feed.
Most chainlink feeds return values in 8 decimals.
Take for example assetPrice = 1e8
(DAI)
In this case, assetUnit = 1e18
Hence in order for precision loss, we require userTotalDebt < 1e10
So each iteration, we can borrow less than 1e10 assets
This means it takes 100 million iterations of supply->borrow->withdraw in order to profit $1. (Since assetPrice
is 1e8)
With 4000 iterations per transaction, 25000 transactions are required.
While the above transactions will use extreme amounts of gas we'll optimistically assume they take the average gas cost of an L2 which is $0.02
The cost would be $0.02 * 25k = $500, to earn $1.
Side note:
Note that if an asset like ETH is used instead, while the assetPrice
increases, this proportionally decreases the maximum userTotalDebt
which can be borrowed, so the attack cost remains unchanged regardless of the asset used.
Also, this won't work at all for lower decimal assets like USDT/USDC since userTotalDebt*assetPrice (debt * 1e8)
will always be greater than assetUnit (1e6)
Escalate
This is a good catch, but the impact does not meet the criteria for medium severity.
Here is the code where the precision loss occurs:
uint256 userTotalDebt = balance.debtShares; if (userTotalDebt != 0) userTotalDebt = userTotalDebt.rayMul(reserve.getNormalizedDebt()); userTotalDebt = assetPrice * userTotalDebt; unchecked { return userTotalDebt / assetUnit; }
For precision loss, we require
userTotalDebt < assetUnit
in the last line.This means we require
assetPrice*userTotalDebt < assetUnit
This means that
userTotalDebt < assetUnit
whereassetPrice
is the price returned bygetAssetPrice()
which is from the chainlink feed.Most chainlink feeds return values in 8 decimals.
Take for example
assetPrice = 1e8
(DAI)In this case, assetUnit =
1e18
Hence in order for precision loss, we require
userTotalDebt < 1e10
So each iteration, we can borrow less than 1e10 assets
This means it takes 100 million iterations of supply->borrow->withdraw in order to profit $1. (Since
assetPrice
is 1e8)With 4000 iterations per transaction, 25000 transactions are required.
While the above transactions will use extreme amounts of gas we'll optimistically assume they take the average gas cost of an L2 which is $0.02
The cost would be $0.02 * 25k = $500, to earn $1.
Side note: Note that if an asset like ETH is used instead, while the
assetPrice
increases, this proportionally decreases the maximumuserTotalDebt
which can be borrowed, so the attack cost remains unchanged regardless of the asset used.Also, this won't work at all for lower decimal assets like USDT/USDC since
userTotalDebt*assetPrice (debt * 1e8)
will always be greater thanassetUnit (1e6)
You've created a valid escalation!
To remove the escalation from consideration: Delete your comment.
You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.
Escalate
This is a good catch, but the impact does not meet the criteria for medium severity.
For precision loss, we require
userTotalDebt < assetUnit
in the last line.This means we require
assetPrice*userTotalDebt < assetUnit
This means that
userTotalDebt < assetUnit
whereassetPrice
is the price returned bygetAssetPrice()
which is from the chainlink feed.Most chainlink feeds return values in 8 decimals.
Take for example
assetPrice = 1e8
(DAI)In this case, assetUnit =
1e18
.....................................................
Thanks for the escalation and the breakdown, I agree that this should not be a medium severity. This should be a high severity, however for the record, this report also meets the medium vulnerability:
From https://docs.sherlock.xyz/audits/real-time-judging/judging#v.-how-to-identify-a-medium-issue:
Note: If a single attack can cause a 0.01% loss but can be replayed indefinitely, it will be considered a 100% loss and can be medium or high, depending on the constraints.
This attack can be replayed indefinitely on almost every token out there given 18 decimal format token is the default standard for ERC20 token. Some ways this attack can be stopped is if the admin froze the asset (which could be temporary) or all lenders decided to withdraw the asset.
And for why this deserves a high severity, from https://docs.sherlock.xyz/audits/real-time-judging/judging#iv.-how-to-identify-a-high-issue :
Definite loss of funds without (extensive) limitations of external conditions. The loss of the affected party must exceed 1%.
Take an example, a lender that lends $100 in a $100_000 pool, it is guaranteed that this attack will eventually cause a $100 loss given it can be replayed indefinitely, maybe not in 1 day. But as more users discover this vulnerability, the loss could scale up pretty quick.
danzero
High
Small amount of borrow can drain pool
Summary
A user can supply some amount of a token and borrow a small amount of a token from the pool and withdraw the initial amount they supplied without repaying the borrowed amount which could cause insolvency for the pool.
Root Cause
https://github.com/sherlock-audit/2024-06-new-scope/blob/main/zerolend-one/contracts/core/pool/logic/ValidationLogic.sol#L124 The
validateBorrow
function InValidationLogic.sol
is used to validate borrow request from the user, currently it only requires that the borrowed amount is not 0, this allows user to borrow a minuscule amount of token.https://github.com/sherlock-audit/2024-06-new-scope/blob/main/zerolend-one/contracts/core/pool/logic/ValidationLogic.sol#L239 The
validateHealthFactor
function inValidationLogic.sol
validate the requested amount of withdrawal from the user corresponding to the health factor. ThehealthFactor
variable returned from theGenericLogic.calculateUserAccountData
function is compared with theHEALTH_FACTOR_LIQUIDATION_THRESHOLD
to be bigger or equal or else it reverts with an error.https://github.com/sherlock-audit/2024-06-new-scope/blob/main/zerolend-one/contracts/core/pool/logic/GenericLogic.sol#L69 Inside the
calculateUserAccountData
function thevars.totalDebtInBaseCurrency
variable determines thehealthFactor
variable. Thevars.totalDebtInBaseCurrency
variable is increased by the_getUserDebtInBaseCurrency
function.https://github.com/sherlock-audit/2024-06-new-scope/blob/main/zerolend-one/contracts/core/pool/logic/GenericLogic.sol#L184 Inside this function the
usersTotalDebt
variable is divided by theassetUnit
variable which is fetched from the reserve configuration of the asset. This is a problem if theassetUnit
is bigger than theusersTotalDebt
as it will amount to 0 in the end which would set thehealthFactor
variable totype(uint256).max
which would allow the user to withdraw the initial amount that is supplied while still keeping the borrowed amount.Internal pre-conditions
External pre-conditions
Attack Path
Impact
PoC
Mitigation
Fix the require statement in
validateBorrow
function InValidationLogic.sol
such that it the minimum borrowed amount depends on the decimals of the asset borrowed. An attacker can get away borrowing 1e9 of an asset with 1e18 decimal but fails when it tries to borrow 1e10, hence the require statement could be something like "minimum borrow amount needs to be 8 decimal difference to the asset decimal". Could be something like this: