Platform: Code4rena
Start Date: 11/12/2023
Pot Size: $90,500 USDC
Total HM: 29
Participants: 127
Period: 17 days
Judge: TrungOre
Total Solo HM: 4
Id: 310
League: ETH
Rank: 33/127
Findings: 1
Award: $430.75
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 3docSec
Also found by: aslanbek, btk, ether_sky, rvierdiiev
430.7502 USDC - $430.75
Whenever bad debt occurs, it is absorbed among all gUSDC holders by decreasing creditMultiplier
, so even the last lenders to quit the market are able to redeem their credit tokens.
However, creditMultiplier
formula uses totalSupply
, which does not reflect the pending rebasing rewards. This will result in creditMultiplier
becoming lower than needed, making a percentage of the PSM's pegToken balance unredeemable for all lenders.
Alice calls psm#mintAndEnterRebase
(500_000 USDC) and receives 500_000 gUSDC.
Bob supplies 200_000 USDT and borrows 100_000 gUSDC from LendingTerm, redeems 100_000 gUSDC for 100_000 USDC in PSM.
Charlie, same as Bob, supplies 200_000 USDT, borrows 100_000 gUSDC from LendingTerm, redeems 100_000 gUSDC for 100_000 USDC in PSM.
1 year later, just before the loan is due, Bob returns principal + interest (10%), by minting 110_000 gUSDC for 110_000 USDC in PSM and calling term#repay
; of which 100_000 gUSDC is burnt, 10_000 gUSDC go to rewards (for the sake of example, the config is such that 100% of interest goes to gUSDC rebasing).
On the same day, Charlie fails to repay his debt, so his loan is called and auctioned. The auction fails and the principal is forgiven (e.g. due to USDT being frozen in the contact), which results in:
creditMultiplier = (gUSDC.totalSupply() - debt) / gUSDC.totalSupply() = (500_000 - 100_000) * 1e18 / 500_000 = 0.8e18
1 month later Alice owns 510_000 gUSDC, and is able to redeem 408_000 USDC from PSM, but PSM has:
500_000 (alice) - 100_000 (bob) - 100_000 (charlie) + 110_000 (bob) = 410_000 USDC
As a result, 2_000 USDC without gUSDC representation are left in PSM. These 2_000 USDC could be temporarily taken out from the PSM by someone who mints gUSDC in a LendingTerm. But once they repay the debt, these unclaimable 2_000 USDC will be put back into the PSM. In the end of the SimplePSM's lifecycle (when all future loans are closed and all lenders left the market), these 2_000 USDC will stay in the contract.
Note that this is an oversimplified example:
creditMultiplier
will be computed incorrectly as long as bad debt is marked down while there are any pending rebasing rewards, which is extremely likely.The issue exists because creditMultiplier
formula uses totalSupply
, which neglects rebasing rewards. Using targetTotalSupply
will result in all gUSDC lenders being able to redeem exactly the whole PSM balance:
creditMultiplier = (targetTotalSupply() - debt) * 1e18 / targetTotalSupply() = (510_000 - 100_000) * 1e18 / 510_000 = 410_000 / 510_000 * 1e18
Therefore, use targetTotalSupply
instead of totalSupply
:
- uint256 creditTotalSupply = CreditToken(_credit).totalSupply(); + uint256 creditTotalSupply = CreditToken(_credit).targetTotalSupply(); uint256 newCreditMultiplier = (creditMultiplier * (creditTotalSupply - loss)) / creditTotalSupply; creditMultiplier = newCreditMultiplier;
Error
#0 - c4-pre-sort
2024-01-05T14:43:39Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-05T14:45:30Z
0xSorryNotSorry marked the issue as duplicate of #292
#2 - c4-judge
2024-01-29T18:32:55Z
Trumpero marked the issue as satisfactory