Simple Summary
Introduce a protocol-level state safety mechanism through a new opcode.
Abstract
This EIP introduces a new opcode, MUTABLE, which restricts state changes to an explicitly defined scope. Any attempt to modify state outside the permitted scope MUST cause execution to revert.
Motivation
Unintended state mutation during execution is a persistent security risk in smart contract systems. This risk is especially pronounced in proxy-based architectures that rely on DELEGATECALL, where the calling contract effectively relinquishes control over which state changes may occur in the callee’s execution context.
At the protocol level, there is currently no mechanism to stabilize or constrain state layout during execution. Introducing such a mechanism enables safer composition of contracts, supports future extensibility, and improves overall robustness for an increasingly dynamic Layer 1 ecosystem.
At present, there exist contract-level approaches for constraining state mutation, which we refer to as an inner guard. These mechanisms offer strong and programmable protection for explicitly declared locations but fundamentally cannot defend against mutations outside the specified set. Because the number of potentially mutable locations is unbounded, attempting full coverage at the contract level is infeasible under current gas constraints.
To achieve comprehensive protection, a complementary outer guard is required. This can only be implemented at the protocol level. By combining an inner guard with the proposed outer guard, contracts can form a robust firewall against unintended or malicious side effects arising from external calls.
The following example illustrates the limitations of using immutability controls at the contract level.
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;
// Invariants cannot be applied to mapping with InvariantGuard.
mapping(address => uint256) balances;
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;
}
}
PR:
Add EIP: Invariant Layout Guard Opcode by Helkomine · Pull Request #11303 · ethereum/EIPs · GitHub
The scope of this change is very large because it will require changes to many other opcodes. It should also increase the gas of those opcodes to reflect the increase in their complexity.
I didn’t read the whole spec. Does it define how code behaves when there are multiple invocations to MUTABLE in the callstack ,or how a caller might reset this scope after a DELEGATECALL?
Unintended state mutation during execution is a persistent security risk in smart contract systems. This risk is especially pronounced in proxy-based architectures that rely on DELEGATECALL, where the calling contract effectively relinquishes control over which state changes may occur in the callee’s execution context.
Restricting which slots can be manipulated does reduce the scope but the remaining scope still includes those slots, and so this could give a false sense of security. If you can’t trust the execution won’t do something unexpected, it can still do unexpected things to those slots. Really you just shouldn’t execute unsafe code.
Thank you for the feedback.
The scope of this change is very large because it will require changes to many other opcodes. It should also increase the gas of those opcodes to reflect the increase in their complexity.
The proposal does not change the semantics of state-modifying opcodes themselves. It introduces an additional runtime check that is conditionally active only when guardOrigin != NONE.
The complexity increase is comparable to the STATICCALL context check, where state-mutating opcodes already perform an additional branch to determine whether they must exceptional halt.
Therefore:
The baseline gas cost of these opcodes need not change.
The incremental cost is paid explicitly by the MUTABLE opcode via its analysis gas (chunk-based parsing cost).
This follows the same design philosophy used in EIP-214 (STATICCALL) and EIP-2929 (access tracking), where enforcement context is separated from opcode base cost.
I didn’t read the whole spec. Does it define how code behaves when there are multiple invocations to MUTABLE in the callstack ,or how a caller might reset this scope after a DELEGATECALL?
The specification explicitly defines propagation semantics using guardOrigin and frame-local MutableSetList.
If MUTABLE is executed in a frame where guardOrigin is NONE or LOCAL, it replaces the policy and sets guardOrigin = LOCAL.
If executed under INHERITED, the new policy is intersected with the inherited policy and remains INHERITED.
Propagation rules across CALL, DELEGATECALL, CREATE, etc., are defined in the “Propagation” section.
Therefore, multiple invocations in the callstack are deterministic and strictly monotonic in restriction strength. Policy strength can only remain the same or become more restrictive down the callstack — never less restrictive.
Restricting which slots can be manipulated does reduce the scope but the remaining scope still includes those slots, and so this could give a false sense of security. If you can’t trust the execution won’t do something unexpected, it can still do unexpected things to those slots. Really you just shouldn’t execute unsafe code.
The goal of MUTABLE is not to guarantee behavioral correctness of arbitrary code.
It is to reduce the mutation surface at the protocol level.
This is analogous to:
STATICCALL not guaranteeing logical safety, only state immutability.
Access lists not guaranteeing correctness, only access predictability.
MUTABLE provides structural confinement, not semantic verification.
It reduces blast radius — it does not eliminate logical bugs. The statement “you just shouldn’t execute unsafe code” is idealistic but impractical in modular systems. Proxy-based and plugin-based architectures are already common in production.
MUTABLE acknowledges that reality and provides a protocol-level confinement primitive.
Without protocol-level confinement, contract-level invariant guards cannot prevent mutation of undeclared slots in delegated execution contexts. Conversely, without contract-level validation, protocol confinement cannot enforce semantic correctness.
The proposal is designed specifically to enable this composability between global and local enforcement layers.
1 Like