The usage of ConfidencePool::_assertDepositsAllowed internal function inside the ConfidencePool::contributeBonus function leads to the rejection of contributed bonus during an active risk stage (PROMOTION_REQUESTED)
The ConfidencePool::stake function uses the internal function ConfidencePool::_assertDepositAllowed to prevent deposit during PRODUCTIONand CORRUPTED stage, which makes a lot of sense as those are terminal stage and no deposit should allowed anymore, but the ConfidencePool::_assertDepositAllowed internal function also prevents deposit when the agreement the pool is insuring is in the PROMOTION_REQUESTED stage, which does not really make sense at first because it is not a terminal stage but instead it is an active risk stage but after reading ##3 of DESIGN.md I was able to make sense of the situation as the protocol explained the decision for blocking deposit as follows:
As we can see clearly, the Protocol itself clearly states that `the state is still attackable; it is the closing window, not assured survival, which means the stakers are equally at risk as they were at risk in the UNDER_ATTACK stage.
Where the vulnerability arises is that the protocol forgetfully and unknowingly (I say forgetfully and unknowingly because I have read the whole DESIGN.md and protocol-readme.md, and not for once did the protocol mention it as a design choice) uses this same ConfidencePool::_assertDepositAllowed internal function inside the ConfidencePool::contributeBonus function to check if deposit should be accepted or rejected. The ConfidencePool::_assertDepositAllowed internal function blocks incoming funds if the agreement that the pool is insuring is in any of these three states: PRODUCTION, CORRUPTED, or PROMOTION_REQUESTED.
The first two states are terminal states, meaning the agreement has reached finality, and there is no further change in state, which makes it very sensible that bonus donation is rejected, but for the last state (PROMOTION_REQUESTED), it is an active risk state, and the state of the agreement the pool is insuring hasn't reached finality; instead, it is preparing to reach finality which means the possibility exists that staker may loose all their stakes plus any associated bonus if the state of the agreement progress from PROMOTION_REQUESTED to CORRUPTED. So it does not make sense that stakers are still at risk of losing their stakes, and we are rejecting the bonus donation, which is the incentive for the risk they are undertaking.
Let's walk through what your arguments might be to invalidate this finding:
Your possible argument: This is a design choice because the protocol does not want any further bonus donations, as it doesn't want any stake in the PROMOTION_REQUESTED state either.
My response: This argument makes no sense because just by merely looking critically at the ConfidencePool::_assertDepositAllowed internal function name, anyone can easily tell that the function was originally created for the ConfidencePool::stake function and was just later slapped on the ConfidencePool::contributeBonus function out of negligence and unawareness of this effect I am pointing out because depositing into the pool is not and was never treated the same as contributing a bonus to the pool
Your possible argument: The PROMOTION_REQUESTED is a state very close to the terminal state because promotion has been requested for the agreement already, and it is about leaving the risk state, so the protocol does not want to take any more bonuses at this state, hence why they blocked bonus deposit, as they have blocked staking deposit as well
My response: This argument makes no sense as well because the protocol explicitly said the following in the DESIGN.md
They explicitly called both UNDER_ATTACK AND PROMOTION_REQUESTED as active-risk states and said explicitly that both active-risk states can still transition to CORRUPTED; neither implies survival.
// Root cause in the codebase with @> marks to highlight the relevant section
Likelihood:
The likelihood of this vulnerability is high, as it will occur whenever the agreement reaches the PROMOTION_REQUESTED stage, and anyone from anywhere in the world calls the ConfidencePool::contributeBonus function with any amount.
Impact:
This vulnerability blocks bonus deposit, which is the main incentive for stakers who are still actively undertaking risk (risk of losing all their stakes)
Put the code below in BaseConfidencePoolTest.sol
Put the code below in ConfidencePool.K2Bonus.t.sol, then run the test with forge test --mt testBonusForStakersIsRejectedDuringAnActiveRiskPeriod -vvvv
You can have two state variables, isStake and isBonus, that are set inside the respective functions ConfidencePool::stake/ConfidencePool::contributeBonus, then refactor your ConfidencePool::_assertDepositsAllowed function to be as below. Based on those flags, you can decide which of the agreement states would determine if you allow or reject both staking and bonus depositing.
You may also just create a separate internal function that will be called from inside ConfidencePool::contributeBonus instead of calling ConfidencePool::_assertDepositsAllowed.
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.
The contest is complete and the rewards are being distributed.