InvariantGuard : A framework to make DELEGATECALL safer

Proposed new library for OpenZeppelin: InvariantGuard

Background

DELEGATECALL was introduced very early in Ethereum (EIP-7) as a safer successor to CALLCODE.

It is a particularly powerful opcode: it allows a contract to load and execute code from a target address in the caller’s context. This implies that delegated code can freely modify the caller’s storage, something that plain CALL cannot fully replace.

In addition, DELEGATECALL preserves both msg.sender and msg.value, which makes it extremely useful for composability and immediate reasoning in delegated execution contexts.

However, despite its importance, the protocol has not introduced major improvements around this opcode since its inception. This does not mean that no issues exist. In practice, using DELEGATECALL imposes a significant additional burden on developers, especially regarding storage safety and layout management. Any inconsistency in layout assumptions can lead to catastrophic consequences.

Existing Mitigations and Their Limitations

There have been attempts to mitigate these risks. One notable example is the introduction of explicit storage namespaces (ERC-7201), which aims to reduce layout collisions.

However, such solutions primarily address storage layout assumptions and rely on the proxy delegating to a well-behaved logic contract. This implicitly assumes that there exists at least one “valid” execution path. In reality, layouts can still be broken, for example by unintentionally activating malicious logic embedded in a backdoored contract.

This problem becomes particularly severe in modular smart contract architectures, where users are allowed to install custom modules. Most users lack the expertise to thoroughly analyze the safety of these modules. Once installed, a malicious module can remain dormant and later be triggered by seemingly harmless transactions, ultimately allowing an attacker to seize full control of a wallet and cause irreversible damage.

Some cautious teams have implemented pre- and post-execution value checks to reduce the impact of DELEGATECALL. While helpful, these patterns are not widely adopted, leaving most developers to repeatedly reinvent partial and fragile safety mechanisms. As a result, many deployed contracts remain fundamentally exposed: a single mistake during delegated execution can result in total loss of control.

Motivation and Overview

Based on these observations, the author originally introduced a complete implementation named Safe-Delegatecall, later renamed to Invariant-Guard to reflect a more ambitious goal:

Not only controlling state changes caused by DELEGATECALL, but by any opcode or execution path that may alter critical invariants.

This repository presents the first public Solidity implementation of Invariant-Guard. Feedback from the community is highly appreciated.

The author is also preparing an EIP proposal to provide protocol-level invariant protection, enabling global guarantees that cannot be fully achieved at the contract level alone.

Test Cases

The following example establishes an invariant on the owner variable of the diamond contract, any attempt to replace it within the fallback function results in a revert:

// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.20;
import "https://github.com/Helkomine/invariant-guard/blob/main/invariant-guard/InvariantGuardInternal.sol";

contract InvariantDiamondSimple is InvariantGuardInternal {
    bytes32 constant OWNER_STORAGE_POSITION = keccak256("erc8109.owner");

    /**
     * @notice Storage for owner of the diamond.
     *
     * @custom:storage-location erc8042:erc8109.owner
     */
    struct OwnerStorage {
        address owner;
    }

    bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("erc8109.diamond");

    /**
     * @notice Data stored for each function selector
     * @dev Facet address of function selector
     *      Position of selector in the 'bytes4[] selectors' array
     */
    struct FacetAndPosition {
        address facet;
        uint32 position;
    }

    /**
     * @custom:storage-location erc8042:erc8109.diamond
     */
    struct DiamondStorage {
        mapping(bytes4 functionSelector => FacetAndPosition) facetAndPosition;
        /**
         * Array of all function selectors that can be called in the diamond
         */
        bytes4[] selectors;
    }

    function getDiamondStorage() internal pure returns (DiamondStorage storage s) {
        bytes32 position = DIAMOND_STORAGE_POSITION;
        assembly {
            s.slot := position
        }
    }

    error FunctionNotFound(bytes4 _selector);

    function getInvariantSlots() internal pure returns (bytes32[] memory slots) {
        slots = new bytes32[](1);
        slots[0] = OWNER_STORAGE_POSITION;
    }

    function _diamondFallback() internal invariantStorage(getInvariantSlots()) returns (bool success, bytes memory data) {
        DiamondStorage storage s = getDiamondStorage();
        // Get facet from function selector
        address facet = s.facetAndPosition[msg.sig].facet;
        if (facet == address(0)) {
            revert FunctionNotFound(msg.sig);
        }

        (success, data) = facet.delegatecall(msg.data);
    }

    fallback() external payable {
        (bool success, bytes memory data) = _diamondFallback();
        assembly {
            let ptr := add(data, 32)
            let len := mload(data)
            switch success
            case 0 { revert(ptr, len) }
            default { return(ptr, len) }
        }
    }
}

Reference

Link to InvariantGuard

Link to EIP