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.
View preliminary resultsAppeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.