ConfidencePoolFactory is a UUPS proxy: every call reaches its implementation via DELEGATECALL. Nothing in the upgrade path verifies that the proxy can actually DELEGATECALL the candidate.
_authorizeUpgrade checks the caller, not the candidate. The inherited UUPS check calls proxiableUUID() on the candidate - a plain CALL - and verifies the returned slot. The only step that would exercise DELEGATECALL is skipped entirely when data is empty:
So an implementation that answers proxiableUUID() but cannot be delegatecalled passes every check, is installed, and then every path through the proxy reverts - createPool, the setters, pause, and upgradeToAndCall itself. Because recovery runs through the same dead DELEGATECALL, the proxy cannot be rolled back: a replacement factory must be deployed.
One concrete way this arises here. BattleChain is "a ZK rollup built on ZKsync OS" (BattleChain docs), and ZKsync OS is specified as multi-environment: "ZKsync OS will include the following EEs: EVM... WASM... Native RISC V" (ZKsync docs). ZKsync documents delegation being forbidden across environment boundaries: "EVM contracts cannot use delegatecall to EraVM contracts. EraVM contracts cannot use delegatecall to EVM contracts." (ZKsync docs).
Stated precisely: that quote documents the EVM/EraVM boundary on ZKsync Era. Whether ZKsync OS applies the same restriction to its WASM / RISC-V environments, and whether any non-EVM environment is live on BattleChain today, is not established by these docs - both EEs are described as planned. That bounds the likelihood, not the defect: the missing verification is unconditional, and any un-delegatecall-able implementation triggers it.
Severity: Medium - Medium Impact x Low Likelihood.
Likelihood: Low. The owner must upgrade to an implementation the EVM proxy cannot DELEGATECALL, using empty calldata. Standard solc EVM upgrades are unaffected; the risk is operational upgrades that mix execution environments, and the non-EVM environments above are stated as planned rather than confirmed live on BattleChain today.
Impact: Medium. The only factory proxy is permanently disabled - createPool, config, pause and the UUPS recovery entrypoint all revert - and cannot be restored through UUPS, because recovery uses the same broken DELEGATECALL. Existing ConfidencePool clones are non-upgradeable and execute independently, so staker funds are not directly at risk.
Self-contained: drop into test/ and run. Both tests PASS, no RPC. The second confirms the fix, non-empty calldata forces the delegate call, the candidate reverts, and the upgrade rolls back with the slot unchanged.
Reject empty-data upgrades, forcing the proxy to exercise DELEGATECALL - and letting the new implementation validate its own version/storage - before the upgrade can commit:
Deployment tooling then passes a versioned validation call (e.g. abi.encodeCall(ConfidencePoolFactoryV2.postUpgradeCheck, (EXPECTED_VERSION))), and the runbook should require implementations be simulated against BattleChain testnet before mainnet
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.