FoundrySolidityLayer 2
7.25 ETH
Submission Details
Impact: low
Likelihood: medium

Sponsor can change the pool's covered scope accounts after users have deposited stake.

Author Revealed upon completion

Root + Impact

Deposits are permitted while the agreement registry is in NEW_DEPLOYMENT (pre-attack staging). However, the pool's covered scope does not lock until the registry moves past this state, allowing a malicious pool sponsor to change the insured accounts after receiving stakers' funds.

Description

The check _observePoolState() only sets scopeLocked = true when the registry state is not NOT_DEPLOYED or NEW_DEPLOYMENT. Stakers depositing during NEW_DEPLOYMENT are exposed to the risk of the sponsor modifying _scopeAccounts afterwards.

solidity
// File: src/ConfidencePool.sol
function _observePoolState() internal returns (IAttackRegistry). ContractState state) {
state = _getAgreementState();
// @> Only locks scope when state moves past NEW_DEPLOYMENT
if (
!scopeLocked && state != IAttackRegistry. ContractState.NOT_DEPLOYED
ContractState. ttackRegistry.ContractState.NEW_DEPLOYMENT
) {
scContractState. ue;
emit ScopeLocked(block.timestamp);
}
...
}

Risk

Likelihood:

  • Stakers deposit while the agreement is in NEW_DEPLOYMENT state.

  • The sponsor updates the scope via setPoolScope() before the registry state changes.

Impact:

  • Stakers' capital is exposed to a different set of contracts than they originally underwrote and verified at deposit time.

Proof of Concept

The test below stakes tokens for Alice while the registry is in NEW_DEPLOYMENT. The sponsor then successfully calls setPoolScope to change the covered accounts, demonstrating that the scope remains mutable post-staking.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {Test} from "forge-std/Test.sol";
import {ConfidencePool} from "src/ConfidencePool.sol";
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
contract PostStakeScopeMutationPoC is BaseConfidencePoolTest {
function test_postStakeScopeMutationPoC() public {
// 1. Alice stakes while in NEW_DEPLOYMENT state
_stake(alice, 100 * ONE);
// 2. Sponsor defines a new scope
address[] memory newScope = new address[](1);
newScope[0] = address(0xBAD);
agreementContract.setContractInScope(address(0xBAD), true);
// 3. Sponsor successfully replaces the scope post-stake
vm.prank(owner);
pool.setPoolScope(newScope);
// 4. Verify scope was mutated
assertTrue(pool.isAccountInScope(address(0xBAD)));
assertFalse(pool.isAccountInScope(SCOPE_ACCOUNT));
}
}

Recommended Mitigation

Lock the scope immediately upon the first stake to protect staker reliance, regardless of the registry state.

diff --git a/src/ConfidencePool.sol b/src/ConfidencePool.sol
index a5f3d4b..f4b8c9a 100644
--- a/src/ConfidencePool.sol
+++ b/src/ConfidencePool.sol
@@ -227,6 +227,9 @@ contract ConfidencePool is Initializable, Ownable2Step, ReentrancyGuard, Pausabl
_assertDepositsAllowed(_observePoolState());
if (!expiryLocked) {
expiryLocked = true;
}
+ if (!scopeLocked) {
+ scopeLocked = true;
+ }

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!