User can directly offboard a term without having reached quorum if a term was re-onboarded before cleanup() is called #935
Labels
2 (Med Risk)
Assets not at direct risk, but function/availability of the protocol could be impacted or leak value
bug
Something isn't working
duplicate-1147
edited-by-warden
satisfactory
satisfies C4 submission criteria; eligible for awards
sufficient quality report
This report is of sufficient quality
Lines of code
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/governance/LendingTermOffboarding.sol#L153
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/governance/LendingTermOffboarding.sol#L175
Vulnerability details
Impact
The protocol has the functionality that during the process of offboarding if a lending term is re-onboarded before the
cleanup()
function is called, then the transaction should revert. This functionality is flawed, because it does not really prevent the offboarding process from completing and breaks the functionality of thenOffboardingsInProgress
storage variable. A user can just calloffboard()
and thencleanup()
again, and the transaction would be successful because the contract storage state has not beed updated. Additionally, if a user was to abuse this, then thenOffboardingsInProgress
storage variable would break and would become permanently stricktly larger than zero. This would break the functionality of PSM redemptions being unpaused when there are no offboardings in progress in that gauge type, causing the permanent need of an additonal governance proposal to unpause PSM redemptions every time a lending term is offboarded.Proof of Concept
In the process of offboarding a term there are several steps. In the LendingTermOffboarding first proposeOffboard() is called, then users vote for the proposal via supportOffboard. Once there are enough votes for the offboarding, the
canOffboard[term]
variable is set to true in thesupportOffboard()
function. This storage variable will remain set totrue
until the complete offboarding process is completed andcleanup()
is called.https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/governance/LendingTermOffboarding.sol#L116
Once there are enough votes, the
offboard()
is called and the loans auctioning can begin. Once all loans are closed, thecleanup()
function is closed.However, there is the caveat that a term can be re-onboarded before
cleanup()
is called. When this happens the term gauge is marked as no longer deprecated in the GuildToken, the offboarding process must be stopped and the cleanup function must be prevented from being called. In thecleanup()
function there is a check if this has happened as we can see below.https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/governance/LendingTermOffboarding.sol#L175
However, the
canOffboard[term]
variable is still set to true, even after the term has been re-onboarded. This means that a user can just calloffboard()
to reset theGuildToken(guildToken).isDeprecatedGauge(term)
to return true and just callcleanup()
again. This circuimvents creating a new proposal for offboarding that term by allowing the user to directly offboard a term that has just been reonboarded.There is the additional side effect of this that breaks the storage variable
nOffboardingsInProgress
. There are only two ways to increment/decrement this variable.In
offboard()
once quorum is reached, we increment this variable by1
:https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/governance/LendingTermOffboarding.sol#L163
Then in
cleanup()
this variable is decremented by1
.As we can see this variable is used to track wether redemptions in the
SimplePSM
are allowed or not. If the variable is0
, then the redemptions are allowed, if it is set to1
or more, they are paused.This means, that in the process of offboarding lending terms the
offboard()
function should be called the exact same number of times as thecleanup()
function. To keep this variable at0
when there are no offboardings running. The above described case would cause theoffboard()
function being called one more time than thecleanup()
function, which would break this invariant and will permanently set thenOffboardingsInProgress
storage variable to be stricktly larger than0
.This would mean that in the consecutive call of
cleanup()
the PSM redemptions would be set to allowed, even though in practice, there are no offboardings in progress. This variable can in practice still be externally set to true again, but it needs to be done through a separate governing proposal inSimplePSM
:https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/loan/SimplePSM.sol#L149
This additional governor proposal action would need to happen every time a term is offboarded in that gauge type is offboarded to keep the functionality of the that market running.
To test this bug copy the below lines of code at the end of the
testCannotCleanupAfterReonboard()
unit test like this:https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/test/unit/governance/LendingTermOffboarding.t.sol#L358
To run this test run the command:
forge test --match-test testCannotCleanupAfterReonboard
The test passes and this example invalidates this test, as it shows that in fact a user can cleanup after reonboard. He just needs to call
offboard()
before he callscleanup()
Tools Used
Manual review and unit testing.
Recommended Mitigation Steps
That depends on the intended functionality of the protocol. Re-onboarding should not be possible while offboarding is in progress, or for example calling
cleanup()
should reset the contract storage state instead of reverting, making it necessary to again follow through the complete process of offboarding, starting from callingproposeOffboard()
. There are many ways to approach this bug, but that would depend on how the developers want to handle this functionality.Assessed type
Governance
The text was updated successfully, but these errors were encountered: