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

Locked pool scope can be detached from the original agreement and rebound elsewhere

Author Revealed upon completion

Locked pool scope can be detached from the original agreement and rebound elsewhere

Description

A pool's locked scope should continue tracking the risk lifecycle for the covered account. The pool validates scope membership against the original immutable agreement and later reads only that original agreement's registry state. If the scoped account is removed from Agreement A and rebound to Agreement B, the pool remains blind to B's CORRUPTED state.

Vulnerability Details

The pool reads state only for the original agreement:

function _getAgreementState() internal view returns (IAttackRegistry.ContractState) {
address attackRegistry = safeHarborRegistry.getAttackRegistry();
return IAttackRegistry(attackRegistry).getAgreementState(agreement);
}

Scope replacement checks membership against the same immutable agreement:

for (uint256 i; i < accounts.length; ++i) {
address account = accounts[i];
if (!IAgreement(agreement).isContractInScope(account)) {
revert AccountNotInAgreementScope(account);
}
}

After the pool's scope locks, the agreement owner can remove the account from Agreement A and bind it to Agreement B. If Agreement B becomes corrupted while Agreement A remains non-terminal, the pool cannot observe the corrupted lifecycle for the covered account.

Impact

The moderator cannot mark the pool CORRUPTED for a corruption affecting the covered account under its new agreement binding.

The pool can resolve EXPIRED and pay stakers even though the covered account is corrupted elsewhere in the registry graph.

Proof of Concept

function testScopeDetach() external {
_stake(pool, token, alice, 100e18);
_contributeBonus(pool, token, carol, 20e18);
attackRegistry.setAgreementState(address(agreementA), IAttackRegistry.ContractState.UNDER_ATTACK);
pool.pokeRiskWindow();
agreementA.setContractInScope(SCOPE_ACCOUNT, false);
attackRegistry.setAgreementState(address(agreementB), IAttackRegistry.ContractState.CORRUPTED);
vm.expectRevert();
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, false, address(0));
vm.warp(pool.expiry());
pool.claimExpired();
}

Recommended Mitigation

Track the current state for locked scope accounts instead of only the immutable original agreement state.

-return IAttackRegistry(attackRegistry).getAgreementState(agreement);
+return _aggregateStateForLockedScopeAccounts(attackRegistry, _scopeAccounts);

The resolution logic should handle account migration explicitly, or prevent migration from invalidating the pool's locked risk source.

Support

FAQs

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

Give us feedback!