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

Scope Manipulation After Deposits

Author Revealed upon completion

Description

  • In ConfidencePool.sol, the owner (sponsor) can call setPoolScope() to modify the covered accounts array at any time, as long as scopeLocked is false:

  • Stakers typically deposit their funds while the registry is in NEW_DEPLOYMENT (pre-attack staging). Because scopeLocked is not linked to whether the pool has active deposits, the sponsor can change the pool's scope to cover a completely different set of contracts (or add high-risk/compromised contracts from the parent agreement) after stakers have already staked.

    This directly contradicts the design document's claim:

    "Once locked, the pool's coverage is fixed... stakers' exposure is bounded by what they signed up for at deposit time."

    If the sponsor changes the scope after deposits, stakers are exposed to risk they did not sign up for, with no way to withdraw once the risk window opens.

// Root cause in the codebase with @> marks to highlight the relevant section
```solidity
function setPoolScope(address[] calldata accounts) external onlyOwner {
// aderyn-ignore-next-line(unchecked-return)
_observePoolState();
if (scopeLocked) revert ScopePostLockImmutable();
_replaceScope(accounts);
}
```
However, `scopeLocked` is only set to `true` when the registry state transitions past `NEW_DEPLOYMENT`:
```solidity
if (
!scopeLocked && state != IAttackRegistry.ContractState.NOT_DEPLOYED
&& state != IAttackRegistry.ContractState.NEW_DEPLOYMENT
) {
scopeLocked = true;
emit ScopeLocked(block.timestamp);
}
```

Impact:

  • Sponsors can malicious/grief stakers by swapping the pool scope to cover vulnerable contracts right before an attack registry state change, resulting in staker principal being swept/lost.


Recommended Mitigation

Lock the pool scope (set `scopeLocked = true`) upon the first stake, mirroring the behavior of `expiryLocked`:
```diff
function stake(uint256 amount) external nonReentrant whenPoolNotPaused {
...
if (!expiryLocked) {
expiryLocked = true;
}
+ if (!scopeLocked) {
+ scopeLocked = true;
+ emit ScopeLocked(block.timestamp);
+ }
```

Support

FAQs

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

Give us feedback!