Author: Helkomine (@Helkomine)
A framework to make DELEGATECALL safer.
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.
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.
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.
(Note: the EIP draft is not yet available.)
Invariant-Guard currently provides four variants:
InvariantGuardInternalInvariantGuardExternalInvariantGuardERC20InvariantGuardERC721
If you are only interested in usage examples or prefer to read the implementation directly, please familiarize yourself with the design principles below to avoid confusion.
There are five Invariant-Guard files in total:
- Four functional implementations (listed above)
- One shared helper library:
InvariantGuardHelper.
Invariant-Guard works by:
- Taking snapshots of selected values before execution
- Executing the target logic
- Performing post-execution validation
This design is conceptually similar to the pattern used in flash loan validation.
In the design of InvariantGuard, invariants are extended along two dimensions:
The following state types are supported:
- Code
- Nonce
- Balance
- Storage
- Transient Storage
Each state category can be configured with a threshold policy:
- EXACT – Value must remain exactly unchanged
- INCREASE_EXACT – Must increase by an exact amount
- INCREASE_MAX – May increase up to a maximum bound
- INCREASE_MIN – Must increase by at least a minimum bound
- DECREASE_EXACT – Must decrease by an exact amount
- DECREASE_MAX – May decrease up to a maximum bound
- DECREASE_MIN – Must decrease by at least a minimum bound
Each invariant is exposed as a Solidity modifier whose name is composed of:
<Threshold> + <StateType>
There is a special class of invariants prefixed with assert.
In this case, the expected value is provided explicitly by the calling contract rather than being read directly from the current state.
Despite this difference in value sourcing, these invariants are still classified under the EXACT category.
Additional configurations may be researched and introduced in future versions.
To integrate InvariantGuard into your contract, import the appropriate module into your existing .sol file:
InvariantGuardInternal:
import "https://github.com/Helkomine/invariant-guard/blob/main/invariant-guard/InvariantGuardInternal.sol";
InvariantGuardExternal:
https://github.com/Helkomine/invariant-guard/blob/main/invariant-guard/InvariantGuardExternal.sol
InvariantGuardERC20:
https://github.com/Helkomine/invariant-guard/blob/main/invariant-guard/InvariantGuardERC20.sol
InternalGuardERC721:
https://github.com/Helkomine/invariant-guard/blob/main/invariant-guard/InvariantGuardERC721.sol
After importing, apply the provided modifiers to the functions you wish to protect. Example:
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;
import "https://github.com/Helkomine/invariant-guard/blob/main/invariant-guard/InvariantGuardInternal.sol";
import "@openzeppelin/contracts/utils/Address.sol";
contract InvariantSimple is InvariantGuardInternal {
address owner;
function safeDelegateCall(address target, bytes calldata data) public payable invariantStorage(_getSlot()) {
Address.functionDelegateCall(target, data);
}
function _getSlot() internal pure returns (bytes32[] memory slots) {
bytes32 slot;
assembly {
slot := owner.slot
}
slots = new bytes32[](1);
slots[0] = slot;
}
}
Developers must clearly understand the inherent limitations of this module and proactively account for areas it does not protect. For this reason, the author strongly recommends using Invariant-Guard only for critical state locations, such as:
- Proxy pointers
- Ownership slots
- State explicitly declared as invariant by the original specification
At first glance, the execution logic of InvariantGuard may resemble OpenZeppelin’s ReentrancyGuard. However, their execution models are fundamentally different.
ReentrancyGuard follows the pattern:
Check → Write → Execute → Write
InvariantGuard follows the pattern:
Read → Execute → Read → Check
The only common element is that the protected execution occurs in the middle. All other aspects differ.
ReentrancyGuard:
- Introduces new state (older versions use
storage). - Newer versions rely on
transient storage, but still introduce local state mutation. - Has higher operational cost.
- Is restricted in
staticexecution contexts.
InvariantGuard:
- Only reads state before and after execution.
- Does not introduce new state.
- The primary cost comes from state access.
- Has lower operational overhead.
- Is not restricted under static execution contexts.
Most importantly:
ReentrancyGuardprevents reentrancy attacks.InvariantGuardprevents unintended invariant violations.
These are distinct security concerns. Developers must clearly understand their differences to avoid misuse. Combining both mechanisms is possible but should be done carefully, as their interaction may introduce unintended execution behavior.
Based on the Solidity implementation of Invariant-Guard, the author has identified a clear separation between:
The inner ring: explicitly selected and guarded state locations (well-covered by this library)
The outer ring: all unspecified state locations (vast in number and impractical to enumerate)
This outer ring represents the fundamental weakness of any contract-level approach. Solving this problem requires protocol-level support, such as: A new opcode, or A dedicated precompile that can “fence off” all non-designated state locations
For this reason, the author has decided to propose an EIP that introduces a robust, global solution, effectively eliminating attacks originating from the outer ring and elevating state safety to an absolute level.
(Note: the detailed draft is currently under development.)
Through this implementation, the author hopes to encourage serious discussion around invariant protection during execution, especially in the context of DELEGATECALL.
This topic is increasingly critical as Account Abstraction gains traction, driving widespread adoption of modular smart accounts. Security and scalability should not be treated as mutually exclusive trade-offs.
If you discover any issues in the code—logic errors, naming problems, or otherwise—please feel free to open a pull request.
Thank you very much.