Core/Periphery Design Pattern for Immutable Protocols

For teams that decide not to use upgradeable contracts (see the gradual immutability path), the core/periphery split is a practical design choice that can minimize security issues while still allowing controlled changes of functionality.

What Is the Core/Periphery Split?

The core is the immutable, minimal set of contracts that define protocol-critical logic and invariants. The periphery is the replaceable layer that handles user interactions, UX improvements, and convenience features.

Core

  • Immutable once deployed.

  • Holds critical state and enforces protocol-level invariants.

  • Example: Uniswap V2/V3 Pool contracts (AMM math, reserves, invariant checks).

Periphery

  • Can be re-deployed and versioned over time.

  • Abstracts away complexity and adds features like batching, multicall, safer UX.

  • Example: Uniswap Router, NonfungiblePositionManager.

Why Use This Pattern?

  • Security: Core stays locked down, surface area minimized.

  • Flexibility: Teams can adapt UX, improve efficiency, or patch integration bugs without touching core.

  • Clarity: Easier to communicate security guarantees: "The pool logic is immutable. Routers can change."

Upgrade Strategy Without Proxies

Even if core is immutable, periphery contracts can evolve. Typical approaches include:

  • Versioned Routers: Deploy new routers with improved logic; users migrate voluntarily.

  • Registries: Maintain a registry contract pointing to the latest periphery; frontends can read from it.

  • Migrations: Encourage/force users to migrate state from old periphery to new periphery.

Security Model

  • Core: must be fully audited, formally verified if possible, and designed for immutability.

  • Periphery: can be updated, but still requires review since it can influence user behavior.

  • Interaction: Core contracts should never trust periphery; only enforce strict invariants.

Common Patterns

  • Router → Pool Calls: Router bundles user actions and calls into pools.

  • Callback Interfaces: Core pools define strict callback requirements, which routers must satisfy.

  • Position Managers: Higher-level contracts for abstracting LP positions.

  • Fee Hooks / Incentives: Kept in periphery to avoid contaminating invariant logic.

Pitfalls

  • Too Much in Periphery: If periphery enforces invariants instead of core, security assumptions break.

  • Silent Upgrades: Replacing periphery without notice can create governance or trust issues.

  • Registry Risks: If the registry is compromised, users may be routed to malicious periphery contracts.

Example: Minimal Core/Periphery Split

// Core: immutable pool
contract Pool {
    uint112 reserve0;
    uint112 reserve1;

    function swap(uint amount0Out, uint amount1Out) external {
        // Invariant: x * y >= k
        require(amount0Out == 0 || amount1Out == 0, "Only one side out");
        // ...
    }
}

// Periphery: router that interacts with core
contract Router {
    function swapExactTokensForTokens(...) external {
        // Handle approvals, slippage checks, path routing
        Pool(pool).swap(...);
    }
}

Last updated