Free 30-min Web3 Consultation
Book Now
Smart Contract Security Audits
Learn More
Custom DeFi Protocol Development
Explore
Full-Stack Web3 dApp Development
View Services
Free 30-min Web3 Consultation
Book Now
Smart Contract Security Audits
Learn More
Custom DeFi Protocol Development
Explore
Full-Stack Web3 dApp Development
View Services
Free 30-min Web3 Consultation
Book Now
Smart Contract Security Audits
Learn More
Custom DeFi Protocol Development
Explore
Full-Stack Web3 dApp Development
View Services
Free 30-min Web3 Consultation
Book Now
Smart Contract Security Audits
Learn More
Custom DeFi Protocol Development
Explore
Full-Stack Web3 dApp Development
View Services
LABS
Guides

How to Design EVM-Compatible Execution Policies

A technical guide for developers on designing and implementing custom execution policies for EVM-compatible blockchains, including code structure and validation logic.
Chainscore © 2026
introduction
DEVELOPER GUIDE

How to Design EVM-Compatible Execution Policies

Execution policies are programmable rules that govern how smart contracts can be executed, enabling granular control over transaction logic, security, and gas optimization.

An EVM execution policy is a smart contract that defines a set of pre- and post-execution checks for another contract's functions. Think of it as a middleware layer that intercepts calls, validates conditions, and can even modify outcomes. This pattern is foundational for building secure upgradeable contracts, implementing role-based access control (RBAC), and creating gas-efficient relayers. Unlike simple modifiers, a policy is a separate, reusable contract that can be composed and swapped, promoting a separation of concerns between core business logic and its governing rules.

Designing an effective policy starts with the IPolicy interface, which typically requires a validate function. This function receives the transaction's msg.sender, target contract address, function selector, and calldata. Your policy logic inside validate must return a boolean and can perform checks like: verifying a user holds a specific NFT, ensuring a timestamp deadline hasn't passed, or confirming a transaction's gas cost is below a limit. For example, a whitelist policy would check if sender is in a stored mapping, while a timelock policy would require block.timestamp > unlockTime.

For complex policies, consider policy composition. Instead of one monolithic policy, you can design several single-responsibility policies (e.g., WhitelistPolicy, GasLimitPolicy, ReentrancyPolicy) and chain them together using a PolicyAggregator. This aggregator can check policies in sequence (AND logic) or provide fallbacks (OR logic). This modular approach, inspired by frameworks like OpenZeppelin's AccessControl, makes systems easier to audit and upgrade. Always ensure your policy's state is minimal and gas-efficient, as its validate function is called in the hot path of every intercepted transaction.

Integration requires a policy-aware executor contract. This executor, often called a PolicyManager or Executor, holds the address of the current policy and the target contract. Its main function, execute, first calls policy.validate(...). Only if it returns true does it use a low-level call or delegatecall to forward the transaction to the target. Here's a simplified snippet:

solidity
function execute(address target, bytes calldata data) external {
    require(policy.validate(msg.sender, target, data), "Policy violated");
    (bool success, ) = target.call(data);
    require(success, "Execution failed");
}

This pattern decouples policy logic from execution, allowing policies to be upgraded without migrating the core contract state.

Key considerations for production include policy initialization security and gas overhead. The policy contract itself must be secure and ideally immutable once set. Use a proxy pattern for the policy if upgrades are needed, controlled by a multisig or DAO. Be mindful of gas costs; each policy check adds overhead. Profile your system to ensure costs remain acceptable, and consider using gas-efficient data structures like storage slots for frequently accessed state. For advanced use, explore policy frameworks like Solady's Policy lib or refer to the design of account abstraction paymasters, which are real-world examples of execution policies for transaction sponsorship.

Ultimately, well-designed execution policies create more resilient and flexible smart contract systems. They enable features like transaction bundling, fee abstraction, and conditional execution flows. By externalizing rule enforcement, you build architectures that can adapt to new regulatory or operational requirements without costly migrations. Start by implementing a simple time-lock or whitelist policy for a governor contract to see the pattern in action before composing more complex rule sets.

prerequisites
HOW TO DESIGN EVM-COMPATIBLE EXECUTION POLICIES

Prerequisites for Policy Development

This guide outlines the technical prerequisites for designing and implementing custom execution policies that can evaluate and authorize transactions on EVM-compatible blockchains.

An execution policy is a smart contract that defines the rules for whether a transaction should be executed. In systems like Safe{Wallet} or ERC-4337 account abstraction, a policy acts as a guard, evaluating transaction parameters like to, value, and data before execution. To design one, you must first understand the policy interface. A standard interface, such as the ISafeProtocolManager's checkTransaction function, requires your policy contract to return a bytes4 magic value (e.g., 0x4a6d7c7f) if the transaction is approved. Your policy's core logic will live inside this check function.

Your development environment must be configured for the target chain. You will need Node.js (v18+), a package manager like npm or yarn, and the Hardhat or Foundry framework. Essential libraries include OpenZeppelin Contracts for secure base contracts and TypeChain for type-safe interactions. Start by forking a template repository, such as the Safe Protocol SDK Examples, to ensure your setup includes the necessary manager contract interfaces and testing utilities for simulating policy checks.

The policy's evaluation logic must be deterministic and gas-efficient. Common checks include: verifying the msg.sender is an authorized module, ensuring the to address is on an allowedlist, limiting the value to a maximum amount, or validating that the data calls a specific function selector. For example, a policy could use a mapping allowedRecipients[address] and revert if tx.to is not present. Always perform checks using require() or similar revert patterns to clearly fail transactions that violate rules. Gas costs are critical, as policies are called on-chain for every transaction.

Thorough testing is non-negotiable. Write unit tests in Hardhat or Forge that simulate the policy being called by the manager contract. Test all permission scenarios: valid transactions should pass and return the correct magic value, while invalid ones should revert. Include integration tests that deploy the policy, attach it to a test Safe account via the manager, and execute real transactions. Fuzz testing with Foundry's forge test --match-test can help uncover edge cases with random to addresses and data payloads. Your test suite should achieve near 100% branch coverage.

Before mainnet deployment, verify your policy's security. Conduct a manual review focusing on: access control (who can set rules?), reentrancy risks, and storage collision issues. Consider formal verification tools like Certora for critical logic. Once audited, deploy your policy contract. The final step is activation, which involves a transaction to register your policy's address with the protocol manager (e.g., via SafeProtocolManager.enableModule). After activation, any transaction routed through the manager will be gated by your custom rules, enabling fine-grained control over account operations.

key-concepts-text
CORE CONCEPTS

How to Design EVM-Compatible Execution Policies

Execution policies are the programmable rules that govern how transactions are processed. This guide explains the key design patterns for creating secure and flexible policies compatible with the Ethereum Virtual Machine.

An execution policy is a smart contract that defines the conditions under which a transaction can be executed. It acts as a programmable guardrail, sitting between a user's intent and the blockchain. For EVM compatibility, these policies are written in Solidity or Vyper and implement a standard interface, typically a function like validateTransaction(). This function receives the transaction's calldata, value, and target address, and must return a boolean indicating whether the transaction is permitted. This design allows policies to enforce rules based on transaction parameters, caller identity, asset types, or on-chain state.

The most critical design principle is composability. A policy should be a single, auditable contract with a clear scope. Avoid monolithic designs; instead, create modular policies that handle specific concerns like spending limits, allowlists, or time locks. These can be combined using a policy manager. For example, a wallet might use one policy to restrict token approvals to known DEX routers and another to enforce a daily ETH transfer limit. This separation of concerns makes the system more secure and easier to reason about.

Security hinges on correctly validating all inputs and avoiding external calls within the policy logic. Your validateTransaction() function should be view or pure; it must not modify state or make calls to untrusted contracts, as this could introduce reentrancy or manipulation risks. Always check the msg.sender within the policy to ensure only authorized modules (like a safe or wallet) can invoke it. A common pattern is to hash the transaction parameters and require a signature from a policy owner, enabling off-chain policy creation with on-chain enforcement.

Consider gas efficiency, as the policy is executed on-chain for every transaction. Use storage sparingly and prefer immutable variables or constants for fixed rules. For dynamic checks, like a rolling time window, optimize storage reads and writes. Here's a minimal example of a policy that restricts calls to a specific contract:

solidity
contract AllowTargetPolicy {
    address public immutable allowedTarget;
    constructor(address _target) { allowedTarget = _target; }
    function validateTransaction(address to, ...) external view returns (bool) {
        return to == allowedTarget;
    }
}

To test your policy, use a framework like Foundry or Hardhat. Write unit tests that simulate both permitted and forbidden transactions. Include edge cases: zero-value transfers, delegate calls, and complex calldata. Furthermore, consider the policy lifecycle. How will it be upgraded or revoked? Design with a clear ownership model, often a multi-sig, and include functions to gracefully sunset a policy without locking funds. By following these patterns—modularity, security, gas efficiency, and testability—you can build robust execution policies that safely extend the functionality of any EVM-compatible account abstraction stack.

ARCHITECTURE

Execution Policy Hook Comparison

Comparison of common hook patterns for implementing custom transaction validation logic in EVM-compatible environments.

Hook TypePre-Execution HookPost-Execution HookReentrancy Guard Hook

Execution Phase

Before CALL/CREATE

After CALL/CREATE

During nested CALLs

Primary Use Case

Validation & Gas Limits

State Reconciliation & Logging

Prevent Reentrancy Attacks

Gas Cost Impact

Low (< 5k gas)

Medium (5k-20k gas)

Very Low (< 1k gas)

State Mutability

View-only

Can mutate state

View-only

Implementation Complexity

Low

High

Medium

Common Examples

Balance checks, whitelists

Fee distribution, slashing

OpenZeppelin ReentrancyGuard

Revert on Failure

Access to msg.sender

design-steps
GUIDE

How to Design EVM-Compatible Execution Policies

A technical walkthrough for developers to create custom transaction validation logic for smart accounts using the ERC-7579 standard.

An execution policy is a smart contract that defines the rules for validating transactions before they are executed by a modular smart account. Under the ERC-7579 standard, policies are a core module type that enable fine-grained control over account security and behavior. They are invoked by the account's executor to check if a proposed transaction complies with predefined logic, such as spending limits, multi-signature requirements, or time-locks. Designing a policy involves implementing a specific interface that returns a simple pass/fail verdict.

The foundation of any policy is the ERC7579Policy interface. Your contract must implement two critical functions: onInstall for one-time setup (like setting initial parameters) and onUninstall for cleanup. The core validation logic resides in the check function, which receives the full transaction callData and msg.value. This function must return a bytes4 magic value (0x4d7c2926) if the transaction is approved, or revert/return a different value if it fails. For gas efficiency, policies should perform minimal computation and storage reads within the check function.

Example: A Simple Spending Limit Policy

Let's build a policy that restricts the total value transferred per day. The contract would store a mapping of days to amounts spent. In onInstall, you would initialize the daily limit. The check function would decode the callData to extract the recipient and value, check if the current day's spent amount plus the new value is under the limit, update the storage, and return the success magic value. If the limit is exceeded, the function would revert. This demonstrates how policies encapsulate stateful validation logic.

For complex policies, consider gas overhead and calldata decoding costs. Use efficient patterns like storing packed data in a single uint256 slot or employing transient storage (EIP-1153) for data only needed during the transaction. Always validate and sanitize all inputs within the check function, as it is a security-critical entry point. Policies should be stateless where possible, but when state is necessary, ensure updates are atomic to prevent race conditions between policy checks and execution.

Once deployed, policies are attached to a smart account via its module manager. The account's executor will call your policy's check function for every transaction. You can test policies thoroughly using forked mainnet environments with tools like Foundry, simulating real transaction flows. Effective policy design is key to creating secure, user-friendly smart accounts that can enforce anything from enterprise compliance rules to simple parental controls on crypto spending.

common-policy-examples
EXECUTION POLICIES

Common Policy Examples

Execution policies define the rules for smart contract interactions. These examples demonstrate practical implementations for security, access control, and gas optimization.

implementation-code
IMPLEMENTATION GUIDE

How to Design EVM-Compatible Execution Policies

A practical guide to building modular, reusable execution policies for smart accounts using Solidity and the ERC-4337 standard.

An execution policy is a set of programmable rules that govern when and how a smart account can execute a transaction. In the context of ERC-4337, these policies are enforced by the EntryPoint contract, which validates a UserOperation against the account's logic before execution. Designing a policy involves creating a validation function that can check conditions like - transaction value limits, - allowed recipient addresses, - specific function selectors, and - time-based constraints. This modular approach separates security logic from core account functionality, enabling reusable and composable security modules.

The core of a policy is a validation function, often implemented in a contract that inherits from interfaces like IAccount or a dedicated policy module standard. A basic policy contract must implement a validateUserOp function. This function receives the UserOperation calldata and the user's signature, and must return a validationData packed uint256. This data encodes a timestamp for time-based policies and an authorizer address for signature validation. If validation fails, the function must revert. Here's a skeleton:

solidity
function validateUserOp(
    UserOperation calldata userOp,
    bytes32 userOpHash,
    uint256 missingAccountFunds
) external returns (uint256 validationData) {
    // 1. Verify the caller is the trusted EntryPoint
    require(msg.sender == ENTRY_POINT, "Unauthorized");
    // 2. Implement custom policy logic here
    _validatePolicy(userOp);
    // 3. Return packed validationData (e.g., 0 for success)
    return 0;
}

For practical implementation, consider a SpendLimitPolicy that restricts daily withdrawals. This policy would store a mapping of token addresses to a struct containing a daily limit and the amount already spent today. In validateUserOp, it would decode the calldata to check if the operation is a token transfer, verify the amount does not exceed the remaining daily allowance, and update the spent amount. The policy must also handle the reset of the daily counter, which can be triggered by a timestamp check. Such policies are often deployed as separate contracts that the smart account references via delegatecall or as a module using standards like ERC-7484 for module registry and discovery.

Advanced policy design involves combining multiple conditions. A composite policy might require both a valid EOA signature and a transaction to an approved DeFi protocol address. This can be achieved by creating a policy contract that aggregates checks from other policy contracts, or by using a predicate tree where each leaf is a condition. Security considerations are paramount: policies must be gas-efficient to avoid making account abstraction prohibitively expensive, and they should avoid external calls to untrusted contracts during validation to prevent reentrancy and oracle manipulation attacks. Always audit policy logic with the same rigor as core smart contract code.

To integrate a policy with a smart account factory like those from Safe{Core} or ZeroDev, you typically pass the policy contract address and any initialization data during account creation. The account's validateUserOp function will then delegate validation to the policy. Testing is crucial; use frameworks like Foundry to simulate UserOperation validation through a local EntryPoint. The future of execution policies is moving towards greater standardization and interoperability, with initiatives like RIP-7560 (Native Account Abstraction) proposing built-in support for modular validation, making policy design a foundational skill for smart account developers.

EXECUTION LAYER

Client Implementation Differences

Comparison of key architectural and performance characteristics across major Ethereum execution clients.

Feature / MetricGethNethermindErigonBesu

Primary Language

Go

C# .NET

Go

Java

State Storage Model

Merkle Patricia Trie

RocksDB with pruning

Flat KV store (MDBX)

RocksDB / ForestDB

Default Sync Mode

Snap Sync

Fast Sync (Snap)

Full Archive

Fast Sync

Archive Node Disk Space (est.)

~12 TB

~9 TB

~3 TB

~11 TB

Memory Usage (Peak Sync)

High

Medium

Very High

Medium

JSON-RPC Batch Request Limit

1000 req

Unlimited

Unlimited

1000 req

Native Tracing Support (debug_trace*)

Built-in MEV-Boost Relay

EXECUTION POLICIES

Troubleshooting and Common Mistakes

Common pitfalls and solutions when designing EVM-compatible execution policies for account abstraction, focusing on gas, validation, and security.

The AA33 reverted error indicates your policy's validateUserOp function reverted. This is the primary validation failure. AA21 didn't pay prefund means the bundler's initial gas transfer failed, often because validateUserOp didn't return the required validationData (a uint256) correctly.

Common causes:

  • Forgetting to return validationData (should be return 0; for success).
  • The policy's signature verification or custom logic failed.
  • The policy contract itself lacks sufficient funds to pay for its own validation gas (the "stake").

Fix: Ensure your validateUserOp function signature ends with returns (uint256 validationData) and returns a proper value. Use require statements carefully and test validation logic independently.

EXECUTION POLICIES

Frequently Asked Questions

Common developer questions and solutions for designing and implementing EVM-compatible execution policies for smart accounts.

An execution policy is a set of programmable rules that govern how a smart account can execute transactions, separate from the account's ownership or signers. Unlike a traditional wallet which is just a keypair, a policy defines constraints like spending limits, allowed destinations, time-locks, and complex multi-step logic.

Key Differences:

  • Wallet/Account: Holds assets and defines who can sign (e.g., 2-of-3 multisig).
  • Execution Policy: Defines what actions are permitted, under which conditions, and when. It's a logic layer attached to the account.

For example, a policy could allow unlimited swaps on Uniswap but limit direct ETH transfers to 1 ETH per day, all enforced on-chain before execution.

conclusion
KEY TAKEAWAYS

Conclusion and Next Steps

Designing effective EVM execution policies requires balancing security, flexibility, and gas efficiency. This guide has covered the core principles and implementation patterns.

You should now understand the core components of an EVM-compatible execution policy: the msg.sender and tx.origin validation, gas management via gasleft() checks, and the critical role of the call or delegatecall target. A robust policy contract acts as a gatekeeper, enforcing rules before any state-changing operation proceeds. Remember, the policy's logic is executed in the context of the caller, so any storage it accesses must be carefully considered to avoid unintended side effects or reentrancy vulnerabilities.

For next steps, start by auditing existing policy implementations from major protocols. Review how Safe{Wallet} handles module transactions or examine the Zodiac standard for DAO execution. Then, write and test your own policies using a development framework like Foundry. A practical exercise is to create a policy that restricts calls to a specific Uniswap V3 pool manager or one that enforces a timelock delay for large token transfers. Use Foundry's forge test with invariant testing to simulate malicious actors attempting to bypass your rules.

Finally, integrate your policy with a real-world executor. Deploy it on a testnet like Sepolia and connect it to a Safe{Wallet} via the Zodiac module. Monitor the transaction execution flow and gas costs. The true test of a policy is not just in its code but in its interaction with the broader ecosystem—ensure it fails safely and provides clear revert messages for debugging. Continue your research by exploring more advanced concepts like policy composition (chaining multiple policies) and context-aware policies that can read from oracles or other on-chain state.