Skip to content
Shebin John edited this page Apr 17, 2025 · 3 revisions

Safe Onchain Policy Engine

Overview

This document covers the design, features, risks, monetization strategies, and implementation details of the Safe Onchain Policy Engine. The Policy Engine is an integral part of the Safe platform that allows organizations to define and enforce fine-grained access controls on transactions made by a Safe.


Introduction

The Policy Engine is a smart contract feature that provides on-chain mandatory access control (MAC) for transactions executed by a Safe account. It allows Safe owners to define and enforce transaction policies based on various criteria, ensuring secure and compliant operations.


Core Features

The Policy Engine supports a variety of features to provide security, flexibility, and scalability for Safe accounts, including:

  • Fine-Grained Transaction Policies: Policies can be configured at a granular level, specifying what actions are allowed or denied.
  • Extensibility: New policy types can be added as needed.
  • Security Focus: Policies are designed to minimize the attack surface and improve security.
  • Auditability: Policies are written in Solidity and can be formally audited.

Policy Types and Configuration

The Policy Engine supports multiple types of policies, each with its own specific conditions and requirements. Some key components include:

  • Access Selectors: These are used to map policy conditions to specific transaction data, such as target address, selector and operation.
  • Policy Engine Interface: Custom policies implement the IPolicy interface to define conditions and actions for transactions.
  • Configuration: Allows configuring multiple policies at once using a Merkle root or root hash, ensuring both security and efficiency.

Technical Risks and Considerations

Key risks and technical considerations include:

  • Gas Overhead: Each transaction requires additional gas for policy checks. Optimizations are required to minimize this overhead.
  • Policy Complexity & Interactions: Multiple policies might conflict, requiring careful management and testing.
  • Smart Contract Bugs: Extensive audits are needed to ensure the security of policy contracts.
  • Timelock Inflexibility: Policy updates might be delayed, affecting urgent business needs.
  • Off-Chain Data Limitations: Policies are limited to on-chain data unless oracles are used.
  • User Experience Complexity: Ensuring users can configure policies correctly and intuitively is a priority.

Comparison to Zodiac Roles Modifier

The Safe Onchain Policy Engine offers several advantages over the Zodiac Roles Modifier:

  • Native Integration vs. Module: By implementing as a Guard, the policy checks are built into the Safe's execution flow (pre-transaction) rather than as a module that itself initiates or filters transactions. Guards are somewhat more straightforward in that they simply prevent execution, whereas a module-based approach requires routing transactions through the module. The guard approach can be more seamless and transparent to the user.
  • Expressiveness and Flexibility: The Roles Modifier uses a declarative JSON DSL to specify allowed calls, which can become complex for anything beyond simple whitelists. Our approach uses actual Solidity code for policies, which means any logic can be written and audited in a familiar language. This can handle more complex conditions than the Roles mod easily could, and if a new type of policy is needed, a developer can create a new policy contract for it.
  • Auditability of Logic: Many auditing firms (and internal auditors) are more comfortable reading Solidity code than interpreting a custom DSL or JSON configuration. Policies as Solidity contracts can undergo the same security audits as any smart contract. The Roles mod's configurations, while powerful, might be harder to analyze for security holes or unintended interactions, especially as they grow in size.
  • User Interface & UX: The new Policy Engine is being designed with a first-class UI in mind (integrated into Safe{Wallet}). In contrast, Zodiac's Roles mod, while it has a reference interface, is not as widely integrated into the main Safe UI. By controlling the UX, we can ensure policy management is user-friendly (despite the inherent complexity). In short, we aim to deliver a polished experience for managing on-chain policies, something that was not fully realized with the generic Roles mod.
  • Performance: A custom-tailored guard can be optimized for the Safe's context. The Roles mod is generic and might have overhead to cover all its use cases. By focusing on common policy patterns and encoding them efficiently on-chain, we might achieve lower gas impact per check than a one-size-fits-all roles interpreter would.
  • Mandatory access control: In the default Roles modifier setup, the owners still have discretionary access to the Safe, meaning they can be phished into executing malicious transactions. This can be avoided in the Roles modifier by removing all Safe owners (similar to the Gnosis Pay setup), but this comes at a steep usability cost:
    • It no longer works natively with the Safe transaction service
    • Roles mod authorization is obfuscated compared to Safe transaction signatures

Implementation Details

Safe{Policies} Contract

The core contract for enforcing policies in Safe transactions is the SafePolicyGuard. This contract interacts with the PolicyEngine to validate transactions before they are executed.

Policy Interface

The policy interface (IPolicy) defines the standard for all policy contracts, requiring them to implement a checkTransaction function that returns a magic value if the transaction is allowed.

Access Selectors

Policies are selected based on access selectors that encode the function selector, operation type, and target address, ensuring efficient and transparent policy application.


WIP: Changes to Current Implementation

Changes to be made based on discussion:

  • Merkle Root/Root Hash for Multiple Policy Configuration: Simplifies policy setup and validation.
  • Guard Removal Process: Implementing the removal of guards via the Allow List Policy with time delays.
  • Policy Registry and Decoding: A registry of pre-approved policies to simplify configuration, along with a decoding function to inspect transactions.
  • Formal Verification Template: A standardized template for new policy contracts to ensure they are formally verified.

Future Improvements and Features

Future features under consideration include:

  • Chainlink for Transaction Value Limits: Using Chainlink data to impose limits based on USD values.
  • Cosigner Fallback: Providing a mechanism for cosigners to execute immediate transactions.

Conclusion

The Safe Onchain Policy Engine is a powerful and flexible tool for enforcing mandatory access control on Safe transactions. With a focus on security, flexibility, and ease of use, it offers a comprehensive solution for organizations needing to define fine-grained policies for Safe accounts.


Meeting Notes (17th April 2025):

  • Using Merkle Root or Hash for multiple policy configurations
    • Parameters include:
      • access
        • Type: uint256
        • Content: target, selector & operation
      • policy
        • Type: address
      • dataHash
        • Type: bytes32
        • Content: data in bytes (Configuration Data)
  • The configuration initially happens with configureImmediately(...)
    • This has to be called by the Safe (though it is independent of Safe contract implementation)
    • This sets up multiple policies immediately
    • No check is required for checking if the guard is enabled, because if the guard is enabled, then this tx won’t be executed.
    • This doesn’t have to be behind a time delay as well, because the guard is not setup yet.
  • Normal flow of configuration
    • Two-step process
      • requestConfigure(root, ...)
        • This sets the time delay for the root which is the hashed data for the configuration to be setup.
      • applyConfigure(configure, ...)
        • This sets the policy after the time delay is passed.
  • Invalidate configuration
    • invalidateConfiguration(root)
    • This is to invalidate any configuration from the normal flow of configuration if a mistake while setup was made.
  • Policy per Tx
    • For a particular Tx, we only use a single policy at a time.
  • Guard Removal
    • Guard removal is not a special case, rather has to be going through the Allow List Policy.
    • When the guard is to be removed, we use the normal flow of configuration to setup the call to safe itself, with the selector as setGuard
    • Once the delay is over, we use the applyConfigure(...) and remove the guard (can be done with a multisend.
  • Cosigner Fallback mechanism
    • We should have a fallback cosigner mechanism for executing a one-time transaction or an immediate transaction.
    • We should use a specific function, say execute in the fallback policy to execute the transaction (Making it easy for the access to be specific in this case)
      • The parameter could include the target, selector, operation, etc.
    • This could be monetized as well.
    • This is low priority at the moment in terms of implementation.
  • Registry of Policies and Decoding functionality
    • Using a registry for the policies which helps to ease the setup.
    • This can be handled by the Safe Team or some collective.
    • Only audited policies should be added to the registry.
    • Decoding of transactions based on the policy can also be included (inspired based on Richard’s Tx decoder)
    • This is low priority at the moment in terms of implementation.
  • Wallet Feature: Tx Value Limit
    • We can use Chainlink data streams to pull the data regarding USD value, if we want to have some limits based on USD value.
  • Formal Verification
    • We should have a template that makes it easier for new policies to write formal verification.
      • Specs
      • Helper function
      • Things to cover (maybe a checklist), etc.