The liquidation process in StabilityPool.liquidateBorrower() is flawed due to non-atomic fund transfers, requiring managers to manually pre-fund crvUSD tokens before execution. This introduces risks of failed liquidations, race conditions, gas inefficiencies, and potential loss of funds. Even though external scripts can be used to execute prefunding and liquidation in one transaction, the protocol itself should ensure atomic execution rather than relying on external managers to mitigate this design flaw.
liquidateBorrower() expects the Stability Pool to have enough crvUSD tokens before liquidation.
Managers or the owner must manually transfer the exact amount of crvUSD tokens before calling liquidation,
creating risks as the debt amount keeps increasing due to exponentially accrued interest reflected in reserve.usageIndex.
If the prefunded amount is insufficient even in wei, the transaction will fail, resulting in wasted gas and inefficiencies.
This is exacerbated when multiple managers try to liquidate the same borrower. Apparently, another manager’s transaction may execute first, making the prefunded crvUSD tokens unnecessary. Without a rescueToken function, excess prefunded funds may be left stuck in the contract indefinitely.
Just as depositRAACFromPool() implements a safe transfer mechanism instead of requiring prefunding, the same logic should have been implemented in liquidateBorrower(), ensuring liquidations pull funds dynamically instead of requiring managers to send them beforehand.
Now, a well-written external contract could atomically execute the prefunding and liquidation, but this is not a valid reason to dismiss the issue. The protocol itself should ensure correct execution, not depend on external tooling. If atomic execution is enforced within the contract, managers would not have to manually manage prefunding, reducing risks and improving UX.
Failed Liquidations & Gas Wastage: If prefunding is incorrect, liquidation fails, and managers waste gas.
Lost Funds: Extra crvUSD left in the contract due to failed liquidations cannot be retrieved.
Front-Running Risk: Another manager can liquidate first, making prefunded transactions obsolete.
Inconsistent Protocol Design: Some functions acknowledge atomic execution is needed (depositRAACFromPool()), but liquidateBorrower() does not.
Poor UX for Managers: Managers must constantly calculate debt and prefund, increasing operational complexity.
Manual
Make liquidateBorrower() Atomic by Using safeTransferFrom()
Instead of requiring prefunding, pull the needed crvUSD fund directly from the manager or the owner at execution time.
This eliminates failed liquidations due to mismatched prefunding.
Implement a Rescue Function for Stuck Funds
Introduce a rescueFunds() function to withdraw excess crvUSD tokens left in the contract due to failed liquidations.
Prevents funds from being permanently locked in the Stability Pool.
The same function will also have side benefits rescuing tokens mistakenly sent to this contract.
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.