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: 79/127
Findings: 1
Award: $85.84
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: serial-coder
Also found by: 0x70C9, 0xAlix2, 0xmystery, 0xpiken, Arz, DanielArmstrong, Shubham, deth, rbserver, rvierdiiev, xeros
85.8444 USDC - $85.84
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/governance/LendingTermOffboarding.sol#L191-L195 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/loan/SimplePSM.sol#L149-L154
According to Docs: -
"Since liquidation occurs by auction, it will still set a market price instead of allowing some users to redeem above peg after a loss has occurred. During a LendingTerm offboarding (while auctions of the collateral of a term are running), redemptions in the PSM are paused."
The LendingTermOffboarding contract allows any GUILD holder to poll for the removal of a lending term, and if enough GUILD holders vote for a removal poll, the term can be offboarded without delay.
After the proposal to off board a lending term reaches the desired quorum, the offboard()
is called to remove that lending term & pause
redemptions in the SimplePSM contract. After all the loans for that lending term have been settled, the cleanup()
is called to revoke the minter & pnl notifier role.
If all the lending terms have been off boarded from the protocol, the redemption are started/unpaused in the SimplePSM contract.
Otherwise the only other way to enable/disable redemptions is through the Governor role.
However it is easy for an attacker to enable the redeem functionality allowing users to transfer the peg tokens.
nOffboardingsInProgress
keeps track of the number of off boardings currently happening.
File: LendingTermOffboarding.sol /// @notice number of offboardings in progress. uint256 public nOffboardingsInProgress;
After the quorum has been reached, offboard()
is called which removes the lending term & pauses the redemptions.
File: LendingTermOffboarding.sol function offboard(address term) external whenNotPaused { ... // pause psm redemptions if ( nOffboardingsInProgress++ == 0 && @note !SimplePSM(psm).redemptionsPaused() ) { SimplePSM(psm).setRedemptionsPaused(true); } ... }
If there are no off-boardings left in the protocol, the redemptions are unpaused as can be seen inside the if
statement & canOffboard
is set to false for that lending term.
File: LendingTermOffboarding.sol function cleanup(address term) external whenNotPaused { ... // unpause psm redemptions if ( --nOffboardingsInProgress == 0 && SimplePSM(psm).redemptionsPaused() ) { SimplePSM(psm).setRedemptionsPaused(false); @note - unpaused if no off boardings in progress } canOffboard[term] = false; }
If redemptions are paused, then users cannot redeem their peg tokens.
File: SimplePSM.sol /// @notice set `redemptionsPaused` /// governor-only, to allow full governance to update the psm mechanisms, /// or automated processes to pause redemptions under certain conditions. function setRedemptionsPaused( bool paused ) external onlyCoreRole(CoreRoles.GOVERNOR) { redemptionsPaused = paused; emit RedemptionsPaused(block.timestamp, paused); }
Attack Scenario:
offboard()
has been called for both X & Y pausing redemptions in psm. This sets nOffboardingsInProgress = 2
cleanup()
is called setting nOffboardingsInProgress = 1
. Redemptions are still paused.supportOffboard()
with lending term X as parameter which passes because the duration to veto for X hasn't ended yet.canOffboard[term]
is again set to true
because the quorum already been reached previously.cleanup()
which passes all the checks & .revokeRole()
would just return false in this case.if
condition is executed because now --nOffboardingsInProgress == 0
& which ultimately sets redemptions to false (unpause).Another situation would be if 10 off boarding are in progress & any 1 of them successfully calls cleanup()
within the POLL_DURATION_BLOCKS
(7 day) period, other who haven't voted for that term can call supportOffboard()
followed by cleanup()
to unpause redemptions.
According to docs:
"The SimplePSM targets a value equal to ProfitManager.creditMultiplier(), so when bad debt is created and all loans are marked up, they stay the same in terms of peg token, because new CREDIT can be minted with fewer peg tokens from the PSM."Conversely, when new loans are issued, if there are funds available in the SimplePSM, borrowers know the amount of peg tokens they'll be able to redeem their borrowed CREDIT for.
The above attack breaks this core functionality. Users are able to redeem their peg tokens phase thereby decreasing the supply.
Manual Review
Add a check in supportOffboard()
to ensure the lending term being voted on exists in the gauge. This way if the attacker called to veto the already removed term, the function would revert.
Error
#0 - c4-pre-sort
2024-01-02T20:35:24Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-02T20:35:44Z
0xSorryNotSorry marked the issue as duplicate of #1141
#2 - c4-judge
2024-01-25T18:50:11Z
Trumpero changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-01-25T18:53:36Z
Trumpero marked the issue as satisfactory
#4 - Trumpero
2024-02-07T17:05:24Z
dup of #1141 due to the same root cause: supportOffboard can still be called after offboarding.
#5 - c4-judge
2024-02-07T17:05:31Z
Trumpero marked the issue as not a duplicate
#6 - c4-judge
2024-02-07T17:05:40Z
Trumpero marked the issue as duplicate of #1141