
Safe Research

The Next Level of Guarding: We’ve gone from a simple Guardrail to the feature-rich Fiducia. Now, imagine if you could deploy multiple guards at once, each specialized for a different purpose, and have them all work together seamlessly. This is essentially what the Safe Policy Engine achieves. It’s a major step in Safe’s guard evolution, introducing a system where you can set up any number of fine-grained policies for a single Safe and have each transaction checked against the appropriate policy before execution. If Fiducia is a Swiss Army knife, the Policy Engine is a full toolbox – you can mix and match tools (policies) to build exactly the security setup you need.
At its core, the Policy Engine is an onchain mandatory access control system for Safe transactions. It consists of a guard contract (called SafePolicyGuard) that is installed on your Safe (again via setGuard). This guard doesn’t have hard-coded rules itself; instead, it references a collection of external Policy contracts that you, the user, can add or configure. Every transaction that the Safe attempts will be matched to one of these policies, and only if the policy approves (passes its check) will the transaction execute. If no policy approves, the transaction is denied. This is a shift to a deny-by-default model: anything not explicitly allowed by a policy is blocked. It’s like running a very tight ship where every action has a corresponding rule. That may sound strict (and it is), but it offers maximum security and transparency.
A Policy in this engine is basically a tiny, modular guard of its own focused on a specific condition or set of conditions. Technically, a policy is a contract that implements a simple interface: a function checkTransaction(safe, to, value, data, operation, context) returns (bytes4). When invoked, it looks at the transaction details and returns a magic value indicating approval (or it could revert/return nothing to indicate rejection). Each policy contract can be dedicated to one type of rule. For example, one policy might enforce “transfers over X require Y condition,” another might say “calls to DeFi protocol Z are allowed only if made by module M,” etc.
How does the Policy Engine decide which policy to use for a given transaction? It uses an Access Selector key, which is a unique identifier for “to + function + call type” triple. Think of the access selector as a fancy way of encoding “this transaction is calling function F of contract C using operation O.” For instance, a selector could represent “CALL to UniswapRouter.swap()” or “DELEGATECALL to SafeMultiSendCallOnly.multiSend()” or even “simple Ether transfer to address X”. In the Policy Engine, each such selector is mapped to a specific policy contract that should handle it. If no exact match is found, the engine can fall back to a more general policy (there are wildcard fallback selectors for any CALL or any DELEGATECALL that doesn’t have a specific rule). This fallback could be, for example, a policy that says “anything not allowed needs cosigner approval” – yes, you can integrate cosigner logic as a policy too, which is very powerful.
To illustrate: imagine you have Policy A for token transfers, Policy B for DeFi interactions, Policy C for everything else. You would configure the engine such that the selectors corresponding to ERC20 transfer functions map to Policy A, selectors for functions of DeFi protocols map to Policy B, and the fallback selectors map to Policy C. Then, when a transaction comes in:
If it’s an ERC20 transfer, the engine calls Policy A’s checkTransaction. Policy A might enforce a daily limit or whitelist of recipients; if the transfer complies, it returns the approval magic value, and the engine lets it through (the SafePolicyGuard sees the magic value and knows the policy is satisfied). If not, no magic value – engine blocks the tx.
If it’s a DeFi call (say adding liquidity on Uniswap), engine invokes Policy B. Policy B could check that the call is to the correct contract and perhaps that certain parameters are within expected range. Or maybe Policy B consults an oracle or does some computation to ensure it’s safe. If all good, returns approval code; if not, transaction denied.
If it’s something that doesn’t match A or B (maybe a strange contract call), the engine goes to Policy C via the fallback selector. Policy C might be simplistic, like “require cosigner signature embedded in the context” (the context parameter can carry extra data like a cosigner signature or other info). This way you can, for instance, default to requiring a cosigner for anything not explicitly green-lit by other policies – effectively layering the cosigner idea we discussed in Fiducia into the engine as a policy.
In short, the Policy Engine lets you enforce multiple policies in parallel and ensures that for every transaction, some policy explicitly okays it (or else it doesn’t run). This is the highest degree of fine-tuning: you’re not limited to one guard’s internal logic; you can plug in many, even ones created by different developers for different purposes. It’s like having a whole team of specialists – one checks financial limits, another checks addresses, another monitors unusual activity – all coordinated by the SafePolicyGuard as the manager.
The Safe Research team made some opinionated design choices to keep this system robust:
They chose mandatory single-policy enforcement per transaction rather than combinatorial/cascading policies. That means the engine doesn’t try to require approval from all policies (which could get complicated if policies overlap or conflict) or at least one policy (which might be overly optimistic). Instead, each transaction is routed to exactly one appropriate policy (plus a fallback policy if no specific match). This avoids ambiguity and ordering issues – you don’t get into situations like “Policy X allowed it but Policy Y denied it, which one wins?” By design, there’s only one policy that needs to approve any given tx. This keeps things simpler and easier to reason about formally. If you did want a transaction to require multiple conditions, you can still achieve that by writing a policy that itself checks multiple conditions or perhaps even calls other small contracts. But the engine core stays straightforward.
Deny by default: Worth emphasizing again – anything not explicitly allowed is denied. This is a security-first posture. It might require more setup (you have to define policies for what you want to allow), but it gives strong guarantees. You won’t wake up to find some odd transaction slipped by just because you forgot to consider it – if you didn’t consider it, the engine blocks it. Period. This is akin to a firewall that blocks all ports by default except those you opened. More work upfront, but safer in the long run.
Fallback policies: Including the concept of a fallback (or “catch-all”) policy specifically so you can handle the default-deny in a nuanced way if needed. For example, one could set the fallback for CALLs to a policy that requires a cosigner approval (hey, cosigner again!). That means “any call we didn’t whitelist will need Bob-the-Cosigner’s OK.” This is actually very powerful: you could allow a broad cosigner to cover miscellaneous transactions, while still having stricter onchain checks for known critical transactions.
Real-World Analogy: If this sounds a bit like corporate security policies, that’s because it is! Think of a large organization: They might say “Financial transfers over X need CFO approval. Deleting records requires two admin keys,” etc. The Policy Engine is bringing that level of structured policy enforcement into your smart contract wallet. For an individual user, it might be overkill – but for an organization or power user, it’s incredibly assuring.
Some of you might be thinking, “Wasn’t there something like this already?” There was an earlier system in the Safe ecosystem called Zodiac Roles (a modifier) which let you configure allowed function signatures and addresses using a GUI or config file (a DSL). That was indeed a way to implement multi-policy-like behavior (for example, “Role1 allows these addresses with these funcs, Role2 allows those…”).
The Policy Engine covers similar ground but takes a different approach: instead of a single contract that interprets a static configuration, it uses actual code for each policy. The rationale (as noted by Safe Research) is that writing policies as separate small contracts can be safer and more flexible: each policy can be independently audited or even formally verified, and the core engine stays clean and simple.
In short, Policy engine is an access control mechanism, so it applies access control to authenticated actors (threshold of owners, or a module in the case of Safe), whereas Roles mod is a fine grained authentication system that lets you authenticate to the Safe in different ways for different transactions (so, account X can authenticate to the Safe, regardless of whether they are an owner or a module, in order to transfer USDC to a DeFi pool). Both of which serve slightly different purposes.
Policy Engine Safe App Link: https://safe.dev/policy-engine (Add as a custom Safe App in Safe{Wallet})
Deployment: You deploy the SafePolicyGuard contract (the main guard) to your network. You also deploy whatever Policy contracts you need (there might be a library or template to create common ones, or you can custom code them). For example, you might deploy a TransferPolicy (with internal logic for token limits) and a DefiPolicy (with logic for certain protocols), and maybe a CosignerPolicy (that checks for a cosigner sig in the context data).
Configuration: You call functions on the SafePolicyGuard to register policies for different selectors.
Activation: Set the guard on your Safe to the address of SafePolicyGuard. Now your Safe will consult the policy engine on every transaction.
Execution: When a transaction is initiated, the guard computes the access selector from the transaction’s target address, function sig, and operation type. It finds the matching policy (or fallback). It then calls that policy’s checkTransaction(...). If the policy returns the magic approval code, the guard lets the transaction continue; if not, it reverts the transaction. The “context” parameter passed can include additional data; for example, if you’re using a cosigner policy, the context might contain a cosigner’s signature that the policy will verify.
Consider a DAO treasury Safe that decides to use the Policy Engine. They define:
Policy 1: TreasuryTransferPolicy – allows up to $100k per day to known addresses (exchange accounts, payroll, etc.), enforces daily aggregate limits and per-transaction max. Blocks anything over limits.
Policy 2: InvestmentsPolicy – allows interactions with whitelisted DeFi contracts like specific yield farms or staking contracts, perhaps with some limits on amounts and withdrawal targets.
Policy 3: CatchAllPolicy – basically a cosigner-required policy for anything not covered by 1 or 2. They might use their security team’s server as the cosigner that only approves truly unusual transactions after manual review, or even a custodian service if they prefer that way.
They configure the engine so that known token transfer selectors point to Policy 1, known DeFi function selectors to Policy 2, and fallback to Policy 3. Now, whenever someone tries to use the Safe:
Paying monthly expenses (known address, under limit) -> Policy 1 approves if within limit, transaction goes through immediately.
Moving funds into a new yield farm (not on whitelist) -> to fallback Policy 3 -> requires cosigner. The cosigner reviews, sees it’s a valid new investment approved by governance, and provides signature -> transaction goes through. If cosigner doesn’t sign, transaction blocked. Alternatively, they could update Policy 2 to add that new yield farm to allowed list after proper review, then reattempt transaction.
Any weird transaction (like someone trying to call selfdestruct or send to an arbitrary address not in any list) -> falls to Policy 3 -> cosigner will certainly not sign something bizarre -> blocked entirely.
This way, the DAO has layered security: hard rules for routine stuff, and an intelligent gatekeeper for the rest. All automated and enforced by the blockchain. All this just works with the Safe Wallet interface (except for some caveats for the cosigner policy). So, after proper configuration, the account should function like it does today for anything that isn’t out of the ordinary.
The Policy Engine is extensible. If a new kind of check is needed, you can write a new Policy contract and plug it in. For example, a policy could require that any transaction must be initiated during certain hours of the day (maybe your org only allows executing transactions during business hours – the policy could check block timestamp). Virtually any condition that a smart contract can check could become a policy. This modularity means the community could develop a library of open-source policies: a “DailyLimitPolicy,” “AllowlistPolicy,” “TimeLockPolicy,” “MultisigPolicy” (requiring another Safe’s signature perhaps), etc. Users then compose their security by picking policies that suit their needs.
The approach also helps with auditing: the SafePolicyGuard is a generic piece that can be formally verified to ensure it correctly enforces the policy logic (like always requiring a valid magic value, no way to bypass). Each policy can be audited on its own (which is simpler since each is smaller and focused). This is mentioned as an advantage over giant config-based systems.
Keep in Mind: With great power comes some complexity. The Policy Engine is likely one of the most complex to configure of the guard systems. It’s aimed at advanced users. Non-technical folks might not directly tinker with policy contracts, but they benefit from the outcomes (i.e., a super secure wallet). Over time, friendly interfaces might appear – perhaps a Safe app where you can drag-and-drop policies or select from templates (“Enable token daily limit of X”, etc., which under the hood deploys/ configures the appropriate policy contracts). This is where Fiducia could be considered as the best of both worlds in terms of lesser complexity than Policy Engine, but higher security than Guardrail.
The introduction of Guardrail, Fiducia, and the Policy Engine represents a shift in self-custody from purely key-based security (“N out of M must sign”) to policy-based security (“transactions must meet these conditions”). This layered approach can protect against pitfalls like social engineering, software bugs, or mistakes that multi-sig alone might not catch. We started simple: Guardrail showed how a guard can protect against a specific technical risk (delegatecalls). Fiducia expanded that with multiple features and the innovative cosigner concept, striking a balance between strict rules and practical usability. The Policy Engine then took it to the logical extreme: a fully configurable security framework where every aspect of what a Safe does can be governed by policies. Depending on your needs, you might choose one of these levels. Some will be content with a basic guard, others will want the whole nine yards.
One thing is certain – Safe Guards (in all forms) can add a lot of security for those who use them wisely. And importantly, they do so without compromising self-sovereignty. You’re not handing your keys to a bank or relying on a centralized exchange’s promises. You still hold the keys, but now you have smart contracts watching your back, following the rules you set. It’s like having a programmable security team for your crypto wallet. How cool is that?
Before we end, let’s lighten up with a quick analogy: If a Safe multi-sig wallet is a spaceship that only launches when enough crew members turn their keys, then Guards are the mission control protocols ensuring that the launch only happens under safe conditions. Guardrail is like a basic “weather check” – it won’t launch if the weather (delegatecall context) is unsafe. Fiducia is a detailed pre-flight checklist – every system (destination, payload, cosigner co-pilot) must be green-lit or we hold the countdown. And the Policy Engine is a full AI flight control system – monitoring every gauge and parameter, ready to auto-abort or correct course as needed. The result? A much lower chance of catastrophic launch failures. 🌟
As self-custody becomes more prevalent, these guard mechanisms could become standard practice. Today it might seem “advanced,” but tomorrow even newcomers might be using wallets where a lot of these protections run quietly in the background – making crypto safer for everyone. After all, the goal of Safe (and Safe Research) is to “empower users with advanced, transparent security” without sacrificing the ethos of decentralization. Safe Guards are a big leap in that direction.
Happy (and safe) transacting!
Safe Research is the applied R&D arm of Safe, dedicated to advancing the self custody stack. Our work is grounded in the cypherpunk principles of security, censorship resistance, and privacy, and we focus on building trustless, user centric infrastructure for smart accounts and wallets.
Discuss about cosigners in our forum
Learn more about Safe Research
Read our manifesto
Let’s make Ethereum’s original cypherpunk vision real, one commit at a time.