-
Notifications
You must be signed in to change notification settings - Fork 0
Yuki - Wrong accounting of the storage balances results for the protocol to be in debt even when the bad debt is repaid. #68
Comments
Escalate This issue should be high, as it results to permanent loss of funds for liquidity providers. What the issue leads to:
The referred issue describes the outcome of the issue well, but l am still going to provide an additional explanation in this escalation. Detailed explanationPart 1 - Showing how bad debt accrues.The internal function There could be two scenarios when the function is called.
@internal
def _close_position(_position_uid: bytes32, _min_amount_out: uint256) -> uint256:
"""
@notice
Closes an existing position, repays the debt plus
accrued interest and credits/debits the users margin
with the remaining PnL.
"""
# fetch the position from the positions-dict by uid
position: Position = self.positions[_position_uid]
# assign to local variable to make it editable
min_amount_out: uint256 = _min_amount_out
if min_amount_out == 0:
# market order, add some slippage protection
min_amount_out = self._market_order_min_amount_out(
position.position_token, position.debt_token, position.position_amount
)
position_debt_amount: uint256 = self._debt(_position_uid)
amount_out_received: uint256 = self._swap(
position.position_token,
position.debt_token,
position.position_amount,
min_amount_out,
)
if amount_out_received >= position_debt_amount:
# all good, LPs are paid back, remainder goes back to trader
trader_pnl: uint256 = amount_out_received - position_debt_amount
self.margin[position.account][position.debt_token] += trader_pnl
self._repay(position.debt_token, position_debt_amount)
else:
# edge case: bad debt
self.is_accepting_new_orders = False # put protocol in defensive mode
bad_debt: uint256 = position_debt_amount - amount_out_received
self.bad_debt[position.debt_token] += bad_debt
self._repay(
position.debt_token, amount_out_received
) # repay LPs as much as possible
log BadDebt(position.debt_token, bad_debt, position.uid)
# cleanup position
self.positions[_position_uid] = empty(Position)
log PositionClosed(position.account, position.uid, position, amount_out_received)
return amount_out_received Conclusion:
The system provides a way for users to repay the bad debt accrued in the mapping bad_debt.
@nonreentrant("lock")
@external
def repay_bad_debt(_token: address, _amount: uint256):
"""
@notice
Allows to repay bad_debt in case it was accrued.
"""
self.bad_debt[_token] -= _amount
self._safe_transfer_from(_token, msg.sender, self, _amount) Part 2 - Liquidity providers not able to withdraw their full amount of fundsSo far we've explained the main part of the issue, but lets see the impact on the liquidity providers.
@nonreentrant("lock")
@external
def withdraw_liquidity(_token: address, _amount: uint256, _is_safety_module: bool):
"""
@notice
Allows LPs to withdraw their _token liquidity.
Only liquidity that is currently not lent out can be withdrawn.
"""
assert (self.account_withdraw_liquidity_cooldown[msg.sender] <= block.timestamp), "cooldown"
assert _amount <= self._available_liquidity(_token), "liquidity not available"
self._account_for_withdraw_liquidity(_token, _amount, _is_safety_module)
self._safe_transfer(_token, msg.sender, _amount)
log WithdrawLiquidity(msg.sender, _token, _amount) What we know so far:
What happens when calculating the available liquidity:
@internal
@view
def _available_liquidity(_token: address) -> uint256:
return self._total_liquidity(_token) - self.total_debt_amount[_token] @internal
@view
def _total_liquidity(_token: address) -> uint256:
return (
self.base_lp_total_amount[_token]
+ self.safety_module_lp_total_amount[_token]
- self.bad_debt[_token]
) Conclusion:
|
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. |
The escalation is correct, the issue should be high severity. |
Recommendation:
The missed updated total debt will:
Based on the above, the long term impact could be deemed as "material loss of funds", high is appropriate. |
Result: |
Escalations have been resolved successfully! Escalation status:
|
Fixed by moving the |
Yuki
high
Wrong accounting of the storage balances results for the protocol to be in debt even when the bad debt is repaid.
Summary
Wrong accounting of the storage balances results for the protocol to be in debt even when the bad debt is repaid.
Vulnerability Detail
When a position is fully closed, it can result to accruing bad debt or being in profit and repaying the borrowed liquidity which all depends on the amount_out_received from the swap.
In a bad debt scenario the function calculates the bad debt and accrues it based on the difference between the position debt and the amount out received from the swap. And after that repays the liquidity providers with the same received amount from the swap.
The mapping total_debt_amount holds all debt borrowed from users, and this amount accrues interest with the time.
Prior to closing a position, the bad debt is calculated and accrued to the mapping bad_debt, but it isn't subtracted from the mapping total_debt_amount which holds all the debt even the bad one accrued through the time.
As the bad debt isn't subtracted from the total_debt_amount when closing a position, even after repaying the bad debt. It will still be in the total_debt_amount, which will prevent the full withdrawing of the liquidity funds.
Impact
The issue leads to liquidity providers unable to withdraw their full amount of funds, as even after repaying the bad debt it will still be in the total_debt_amount mapping.
Code Snippet
https://github.com/sherlock-audit/2023-06-unstoppable/blob/main/unstoppable-dex-audit/contracts/margin-dex/Vault.vy#L233
Tool used
Manual Review
Recommendation
The only way to fix this problem is to repay the position debt amount prior to closing a position and not only the received amount from the swap. Because total_debt_amount holds the bad debt as well.
The text was updated successfully, but these errors were encountered: