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

`scopeLocked` variable when read off-chain will return stale flag, when no interaction related to stake or bonus made

Author Revealed upon completion

Root + Impact

Description

  • scopeLocked is intended to tell callers, both on-chain and off-chain, whether the pool's BattleChain scope is final and immutable. Per its own NatSpec: "One-way flag flipped on the first interaction that observes the registry in any state other than NOT_DEPLOYED or NEW_DEPLOYMENT."

  • The flag is only ever updated lazily, as a side effect of some entrypoint (stake, contributeBonus, withdraw, flagOutcome, claimExpired, setPoolScope, pokeRiskWindow) calling _observePoolState().

  • There is no method that keeps it synchronized with the live registry state in between such calls. Consequently, an off-chain reader (a dApp, indexer, block explorer, or a staker checking state before depositing) that calls scopeLocked() directly can receive false even though the underlying BattleChain registry has already transitioned past NOT_DEPLOYED/NEW_DEPLOYMENT — the stored flag simply hasn't been flipped yet because no one has submitted an observing transaction since the transition occurred.

  • Even though one calls pokeRiskWindow() it won't be updated as it is explicitly for risk window updates, and there is no way else to update it directly without staking, or contributing bonus, etc.

function pokeRiskWindow() external {
// No-op once resolved: the snapshot globals are frozen, so the risk-window markers must
// be too.
if (outcome != PoolStates.Outcome.UNRESOLVED) return;
// Revert only when nothing has been sealed — registry never reached active risk or
// a terminal state.
// aderyn-ignore-next-line(unchecked-return)
_observePoolState();
@> if (riskWindowStart == 0 && riskWindowEnd == 0) revert RiskWindowNotReached();
}

Risk

Likelihood:

  • This occurs whenever the registry transitions past NOT_DEPLOYED/NEW_DEPLOYMENT and no pool interaction is made for staking, bonus contribution, withdraw.

Impact:

  • An off-chain reader can be misled into believing scope is still editable (scopeLocked() == false), but in reality on the basis of current state it should have been true.

Recommended Mitigation

Add a scope lock retrieval method, which gives results basis on current condition, not stored result.

+ /// @notice Re-derives whether scope WOULD be locked based on the live registry state,
+ /// without mutating storage. Off-chain consumers should prefer this over `scopeLocked`
+ /// when they need a live signal rather than a lazily-updated one.
+ function isScopeLocked() external view returns (bool) {
+ if (scopeLocked) return true;
+ IAttackRegistry.ContractState state = _getAgreementState();
+ return state != IAttackRegistry.ContractState.NOT_DEPLOYED
+ && state != IAttackRegistry.ContractState.NEW_DEPLOYMENT;
+ }

Support

FAQs

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

Give us feedback!