Crypto Training

Deployment-Layer Security: CPIMP and the Proxy Initialization Race

Many teams secure implementation code and still lose funds during deployment. This post maps the proxy deploy-init race, CPIMP persistence techniques, and practical hardening.

Crypto Training2025-12-282 min read

A protocol can pass audits and still be exploitable in the first minute of life.

That is not a contradiction. It is a scope gap.

Code audits focus on implementation correctness. CPIMP-class failures exploit deployment sequencing.

sequenceDiagram participant Team participant Chain participant Attacker Team->>Chain: tx1 deploy proxy (uninitialized) Attacker->>Chain: frontrun initialize / implementation slot write Team->>Chain: tx2 initialize (too late) Chain-->>Attacker: control foothold persists

What actually goes wrong#

The common failure pattern:

  1. Proxy is deployed without initialization calldata.
  2. Initialization is sent in a separate transaction.
  3. Adversary races the gap.

This gap can be seconds. That is enough.

Why this keeps recurring#

AssumptionReality
“The mempool delay is tiny”Tiny is enough for automation
“Only ownership can be stolen”Implementation indirection can be parasitic and persistent
“Explorer UI shows correct implementation”Event and storage misdirection can hide compromise

Attack anatomy#

flowchart TD A[Deploy uninitialized proxy] --> B[Attacker writes malicious intermediate] B --> C[Proxy delegates to shadow layer] C --> D[Shadow forwards to real implementation] D --> E[Normal behavior appears intact] E --> F[Selective siphon / control trigger]

A strong attacker avoids obvious breakage. They prefer silent parasitism.

Hardening: make deploy+init atomic#

Use constructor initialization data for proxy deployment.

SOLIDITY
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract ProxyFactory {
    function deploy(address impl, address owner) external returns (address proxy) {
        bytes memory init = abi.encodeCall(MyVault.initialize, (owner));
        proxy = address(new ERC1967Proxy(impl, init));
    }
}

This removes the public race window between deployment and initialization.

Deterministic deployments with circular dependencies#

When contracts depend on each other’s addresses, teams often split deployment into multiple mutable steps and recreate race windows.

Use deterministic address precomputation and atomic orchestration.

flowchart LR P[Precompute CREATE2 addresses] --> D[Deploy all proxies atomically] D --> I[Initialize with precomputed peers] I --> V[Post-deploy verification]

Post-deploy verification that matters#

Read canonical storage slots directly.

  • Verify ERC1967 implementation slot value on-chain.
  • Verify admin slot.
  • Verify code hash at target implementation.

Do not rely only on explorer panels or emitted upgrade events.

Operational runbook#

PhaseRequired control
Pre-deploySigned deployment plan, deterministic addresses
DeploySingle atomic transaction where possible
Immediate post-deploySlot reads + codehash checks
First hourReal-time monitors on impl/admin slot drift

Common anti-patterns to remove now#

  • Deploy now, initialize later.
  • Mutable deploy scripts with manual “fix-up” calls.
  • No slot-level post-deploy checks.
  • Assuming old audit reports cover deployment operations.

Further reading#