StabilityPool's liquidation fails to ensure complete debt clearance after liquidation. When the StabilityPool liquidates a borrower, their debt can remain non-zero, creating "zombie debt" in the system.
The problem surfaces in the interaction between StabilityPool and LendingPool during liquidation. When the StabilityPool executes liquidateBorrower(), it successfully transfers collateral but fails to ensure the borrower's debt is completely cleared. This creates a scenario where a borrower's position shows as liquidated but still carries outstanding debt, while we expects that after a successful liquidation, getUserDebt(borrower) should return 0. However, the debt can persist after liquidation. This happens because:
The StabilityPool's liquidateBorrower() function
Approves crvUSD transfer to LendingPool
Updates lending pool state
Calls finalizeLiquidation()
The LendingPool's finalizeLiquidation()
Transfers NFTs to StabilityPool
Burns debt tokens
But doesn't guarantee complete debt clearance
Impact. This creates accounting inconsistencies where
Borrowers may retain debt obligations after losing collateral
System debt calculations become unreliable
The stability mechanism's effectiveness is compromised
Unexpected Behavior when examining the interaction between StabilityPool and LendingPool during liquidations, I noticed something concerning. The StabilityPool successfully claims the NFT collateral but fails to ensure the borrower's debt is fully cleared. This creates what we call "zombie debt", debt that persists even after liquidation.
This is what happened, the StabilityPool initiates liquidation by calling liquidateBorrower(). It properly handles the crvUSD transfer and NFT acquisition, but it blindly trusts the LendingPool to clear the debt. This trust is misplaced because the LendingPool's finalizeLiquidation() lacks complete debt clearance verification.
Looking at the core contracts, we can see the flow: function liquidateBorrower
After finalizeLiquidation(), the function doesn't verify if the debt was actually cleared
The NFT collateral transfers but debt could remain
This creates a clear path showing how the liquidation flow can leave "zombie debt" in the system. means a borrower could lose their NFT collateral while still owing debt, a situation that should be impossible in a properly functioning liquidation system.
The StabilityPool assumes the LendingPool will handle debt clearance properly, but there's no verification. This creates a mismatch between collateral ownership and debt obligations, something that should be impossible in the protocol's economic model. This means that when the StabilityPool steps in to liquidate an underwater position, it's like taking the keys to a house while the mortgage mysteriously remains active.
Let's explore what's happening under the hood. The StabilityPool's liquidateBorrower() function interacts with the LendingPool through finalizeLiquidation(). This interaction should cleanly settle the position, transferring the NFT collateral and clearing all associated debt. However, the current implementation blindly trusts the LendingPool to handle debt clearance without verification.
This creates a critical state where the system's books don't balance. When a liquidation occurs, the protocol transfers valuable NFT collateral worth potentially millions in real estate value, but the corresponding debt obligation can remain in the system. This isn't just an accounting quirk, it fundamentally breaks the protocol's economic engine by creating "zombie debt" that shouldn't exist.
manual
After finalizeLiquidation(), the function doesn't verify if the debt was actually cleared
The NFT collateral transfers but debt could remain
Should add: require(lendingPool.getUserDebt(userAddress) == 0, "Debt not cleared");
This creates a clear path showing how the liquidation flow can leave "zombie debt" in the system.
To ensures proper interaction between contracts:
StabilityPool → LendingPool: Sends crvUSD to cover debt
LendingPool → StabilityPool: Transfers NFT collateral
StabilityPool: Verifies debt clearance through getUserDebt()
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.