Closed c4-bot-3 closed 4 months ago
GalloDaSballo marked the issue as primary issue
GalloDaSballo marked the issue as sufficient quality report
This doesnt lead to loss of user funds though. Hence it should be downgraded since one could just migrate and redeploy after discovering that. Otherwise good find 👍
Would agree with that! This is a good insight, but users' funds are never at risk. This is realted to the feeManager and the fees taken from the protocol. Therefore a medium issue.
@GalloDaSballo please mark this as disagree with severity
(add label)
Fully deserving of High severity from the C4 rulebook, which doesn't constrain loss to user's funds for High.
trust1995 marked the issue as satisfactory
trust1995 marked the issue as selected for report
Fully deserving of High severity from the C4 rulebook, which doesn't constrain loss to user's funds for High.
Based on our agreement and scope we've agreed that high will be only those that lead to loss of funds. @vonMangoldt and @Foon256 can agree on that
@trust1995 you might want to recheck with organizers on our initial agreement for what is high, not by C4 book
Hi, You are referred to this decision of the the Supreme Court, which I am part of. Loss of yield / Loss of fees shall be treated as any other loss of capital. It is within my authority to decide on the severities of each issue presented. If you have additional concerns you may reach out through your own communication channels with C4 staff.
Hey @trust1995, shouldn't this issue be tagged together with #74?
trust1995 marked the issue as not selected for report
trust1995 marked the issue as duplicate of #74
Lines of code
https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseSecurity/WiseSecurity.sol#L427-L429 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/FeeManager/FeeManagerHelper.sol#L94 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseSecurity/WiseSecurity.sol#L431-L434 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/FeeManager/FeeManagerHelper.sol#L83 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/FeeManager/FeeManagerHelper.sol#L147-L149 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/FeeManager/FeeManagerHelper.sol#L157-L159 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/FeeManager/FeeManagerHelper.sol#L179-L181 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/FeeManager/FeeManager.sol#L663-L670 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/FeeManager/FeeManager.sol#L697-L699
Vulnerability details
Impact
The
WiseSecurity::checkBadDebtLiquidation()
got a bad accounting bug while updating the global bad debt (i.e.,totalBadDebtETH
variable).This bug can be triggered by normal liquidation events or even by an attacker who forces the liquidable position (with bad debt) to be liquidated twice (i.e., executing the
WiseLending::liquidatePartiallyFromTokens()
upon the target position twice).In addition to the attack cost, an attacker can even pay
1 wei
(in terms of the payback token) for each liquidation by specifying the_shareAmountToPay
parameter == 0 when invoking theliquidatePartiallyFromTokens()
. Thus, the attack cost is very cheap.Subsequently, this bug will permanently increment the
totalBadDebtETH
variable (global bad debt). In other words, thetotalBadDebtETH
variable will not be able to decrease to 0 anymore, even if all bad debt of that associated position is paid back in full.As a result, the
incentiveOwnerA
andincentiveOwnerB
will no longer receive their incentives via theFeeManager::claimWiseFees()
since the_distributeIncentives()
will no longer be executed. Furthermore, allbeneficiaries
will no longer claim gathered fees via theFeeManager::claimFeesBeneficial()
since the transaction will always be reverted.For this reason, this issue deserves a high severity rating:
incentiveOwnerA
,incentiveOwnerB
, and allbeneficiaries
cannot claim incentives and gathered fees permanently.Please refer to the
Proof of Concept
for more details.Proof of Concept
This PoC section can be categorized into two subsections, as follows.
1. Code Walkthrough
The
WiseSecurity::checkBadDebtLiquidation()
got a bad accounting bug while updating the global bad debt (i.e.,totalBadDebtETH
variable).Specifically, if the liquidating position has bad debt, the
checkBadDebtLiquidation()
will perform two primary steps:Execute the
FeeManager::increaseTotalBadDebtLiquidation()
to increase thetotalBadDebtETH
variable by the position's bad debt (i.e.,debt
variable).Execute the
FeeManager::setBadDebtUserLiquidation()
to set the position's bad debt (badDebtPosition[_nftId]
).The bad accounting bug in question is the 2nd step. The
setBadDebtUserLiquidation()
will execute theFeeManagerHelper::_setBadDebtPosition()
to replace the position's previous bad debt with the new bad debt.To elaborate, assume that the position (with bad debt) is liquidated twice. The
checkBadDebtLiquidation()
will increase thetotalBadDebtETH
variable (global bad debt) twice, but the function will account for the position's bad debt only once.@1.1 -- The increaseTotalBadDebtLiquidation() will eventually execute the _increaseTotalBadDebt() to increase the totalBadDebtETH (global debt) -- see @1.2
: https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseSecurity/WiseSecurity.sol#L427-L429@1.2 -- Increase the totalBadDebtETH (global debt)
: https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/FeeManager/FeeManagerHelper.sol#L94@2.1 -- The setBadDebtUserLiquidation() will eventually execute the _setBadDebtPosition(). This step will replace the position's previous bad debt with the new one. -- see @2.2
: https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseSecurity/WiseSecurity.sol#L431-L434@2.2 -- The position's previous bad debt will be replaced with the new bad debt
: https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/FeeManager/FeeManagerHelper.sol#L83This bug will lead to a permanent increment of the
totalBadDebtETH
variable (global bad debt). When a user executes theFeeManager::paybackBadDebtForToken()
orFeeManager::paybackBadDebtNoReward()
to pay back the position's bad debt, theFeeManagerHelper::_updateUserBadDebt()
will be triggered to update the position's bad debt.If there is no more bad debt, the
_updateUserBadDebt()
will remove the position's tracked bad debt (currentBadDebt
) from the global bad debt (totalBadDebtETH
). But if there exists bad debt, the_updateUserBadDebt()
will update the global bad debt (totalBadDebtETH
) by accordingly adjusting (increasing/decreasing) the debt value from thenewBadDebt
andcurrentBadDebt
(previously tracked bad debt) variables.However, the previously tracked bad debt (
currentBadDebt
) will not include the replaced debt value previously mentioned. In other words, thetotalBadDebtETH
variable (global bad debt) will not be able to decrease to 0 anymore, even if all bad debt of that position is paid back in full.@3 -- Here, the _updateUserBadDebt() will load the position's previous bad debt (currentBadDebt)
: https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/FeeManager/FeeManagerHelper.sol#L147-L149@4 -- If there is no more bad debt, the function will remove the position's previous bad debt (currentBadDebt) from the global bad debt (totalBadDebtETH)
: https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/FeeManager/FeeManagerHelper.sol#L157-L159@5 -- If there exists bad debt, the function will update the global bad debt (totalBadDebtETH) by accordingly adjusting (increasing/decreasing) the debt value from the newBadDebt and currentBadDebt variables
: https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/FeeManager/FeeManagerHelper.sol#L179-L181This bug can be triggered by normal liquidation events or even by an attacker who forces the liquidable position (with bad debt) to be liquidated twice (i.e., executing the
WiseLending::liquidatePartiallyFromTokens()
upon the target position twice).After the bug is triggered, the
incentiveOwnerA
andincentiveOwnerB
will no longer receive their incentives via theFeeManager::claimWiseFees()
since the_distributeIncentives()
will no longer be executed. Furthermore, allbeneficiaries
will no longer claim gathered fees via theFeeManager::claimFeesBeneficial()
since the transaction will always be reverted.@6 -- After the bug is triggered, the incentiveOwnerA and incentiveOwnerB will no longer receive their incentives (the _distributeIncentives() will no longer be executed)
: https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/FeeManager/FeeManager.sol#L663-L670@7 -- After the bug is triggered, all beneficiaries will no longer claim gathered fees (the transaction will always be reverted)
: https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/FeeManager/FeeManager.sol#L697-L6992. Attack Illustration With Simple Math
This subsection will illustrate the possible attack with simple math that should be self-explanatory.
The attack consists of two liquidations on the same liquidable position with bad debt. The two liquidations can be executed in the same transaction, and the attacker can pay only
1 wei
of the payback token for each liquidation to minimize the attack cost and preserve the bad debt ratio.After the attack, the
totalBadDebtETH
= 6 (global debt), whereas thebadDebtPosition[nftId]
= 3 (the position's bad debt). As you can see, the previous debt (from the 1st liquidation) tracked by thebadDebtPosition[nftId]
was replaced with the new debt (from the 2nd liquidation).Below simply simulates the execution of the
FeeManager::paybackBadDebtForToken()
to pay back the total position's bad debt.As a result:
badDebtPosition[nftId]
= 0 (the position's bad debt) -- No more position's bad debttotalBadDebtETH
= 3 (global debt) -- Permanent increase of the global bad debtTools Used
Manual Review
Recommended Mitigation Steps
Fixing this vulnerability depends on the developer's design decisions.
One possible solution is to rework the
checkBadDebtLiquidation()
to set the liquidating position's bad debt with thediff + currentBadDebt
instead of only thediff
, like the snippet below.Assessed type
Other