Safe is a modular Smart Account protocol that uses Account Abstraction to build a wide range of wallets and other solutions through a shared plugin interface
Some community members, including in this recent ERC-6900, advocate for the use of Diamond Proxies for modular Smart Accounts like Safe
Over the past weeks, the Safe Core team evaluated changing the smart contract architecture to a Diamond Proxy Pattern for its upcoming v2 smart contracts
Due to security concerns, a Diamond Proxy setup was discarded and we generally recommend against using it for Smart Accounts
One of the goals of the Safe contracts is to be modular and extensible. We believe this is essential to fully harness the power of Smart Accounts (Account Abstraction). This has been a top priority since the first version of the Safe in 2018, and it is consistently reevaluated. Another crucial aspect of the Safe contracts is security. We aim to provide a high level of modularity without compromising security.
One of the biggest security challenges for modular Smart Accounts is managing the risks associated with complex smart contract setups. One way to do this is by clearly separating concerns [1, 2]. In the current Safe contracts, this separation has been implemented with different, distinct, plugin types:
Modules - Whitelisted addresses that can execute transactions in the name of the Safe Smart Account.
Guard - A contract that can be set to perform additional checks on transactions to be executed
Fallback Handler - A contract that can be set to handle arbitrary incoming (read) calls
A Safe Module extends the logic of the core Safe contracts by creating additional access logic to execute transactions. For example, the Allowance Module allows granting spending limits to separate accounts, which then do not require confirmation from all users. Figure 1 shows that Safe Modules are independent smart contracts, ensuring a clear separation of concerns. The only "touch points" are the methods that enable the execution of transactions by Safe Modules (i.e.,
execTransactionFromModule). This enforces strict security assumptions on the module contracts, which is necessary since modules can execute any arbitrary transaction via the Safe.
A Safe Guard is a contract that can be set for a Safe to perform additional security checks (also known as “hooks”). Figure 2 outlines the general flow when a Guard is registered. Before the execution of a Safe transaction, the Guard is called with all the transaction parameters. If the Guard does not revert, the Safe Smart Account will execute the Safe transaction and call the Guard again afterward. This can, for example, be used to only allow interaction with specific contracts or to check the state of the Safe once a transaction has been executed.
A Fallback Handler processes incoming calls and allows the implementation of logic for novel standards/assets that require different types of callbacks. Figure 3 outlines how this is used with the Safe to enable support for contract signatures (EIP-1271) as well as different token standards (i.e. ERC-721, ERC-1155, and ERC-777).
These three plugin types make Safe contracts extremely flexible while clearly separating the different concerns to limit complexity. Plugins are completely independent of the core Safe contracts and maintain their own storage. Interaction occurs only through well-defined interfaces, allowing for strong security guidelines and automated tests.
As the Ethereum landscape evolves and new patterns for smart contracts are developed, it's necessary to constantly evaluate how to improve Safe contracts. Several topics will influence the development of v2 of the Safe contracts.
One pattern that caught our attention was the Multi-Facet Proxy Pattern (aka Diamonds), which is also favored by a recent proposal draft to standardize modular Smart Accounts.
The Safe Proxy pattern sets exactly one logic contract (aka Singleton or Mastercopy) that defines how the proxy will react to incoming calls and transactions. This is a very common pattern also used by OpenZepplin in their Upgradable Proxy. Diamond Proxies extend this pattern by allowing multiple logic contracts (called Facets) to be set depending on the incoming call. The interesting part is what happens when these incoming calls are processed by the smart contract. Figure 4 shows the general layout of a Diamond contract. For each incoming call, the Diamond base logic (CodeD) checks if a Facet is registered. If this is the case, the code of the Facet (CodeA or CodeB) is executed in the context of the Diamond using a delegate call.
The Diamond and its Facets share the same storage area. This makes it possible for them to share specific storage values (DataABD or DataAB) while each still has its own values (DataD, DataA, and DataB). To avoid issues with conflicts in the storage layout, it is recommended to set a unique "storage area" for each of the Facets by using the “Unstructured Storage” approach.
If any Facet overwrites the storage area of another Facet or the Diamond incorrectly uses the shared storage, it might cause completely unexpected behavior. Therefore, it is very important to clearly define the storage areas used, as storage collisions can break an account, make interaction impossible, or otherwise put its security and integrity at risk.
The Diamond pattern is very flexible and modular, which is especially useful if you have a bigger project that relies on a single contract. The project team can then maintain the Diamond setup with the different Facets and audit any proposed changes to ensure that new Facets don’t break the setup. However, this can be quite complex and requires quite a lot of attention. With Facets influencing each other, full audits and strong security assumptions can only be made when a specific setup including all Facets is known. Trail of Bits provided a nice summary of what to pay attention to when using the Diamond pattern in your project.
While the Diamond pattern may offer some benefits, we believe that it would compromise the separation of concerns and increase the complexity of maintaining a high-security standard for Safe Smart Accounts. While professional teams may be able to evaluate and audit this complexity, it could prove extremely challenging for individual users. Furthermore, each user's setup may slightly differ and result in idiosyncratic effects on different Facets. This increases complexity to a level where even professional auditors would find it challenging to ensure the security of all possible setups.
On the other hand, the benefits of using the Diamond pattern over the current modules approach are limited. The biggest advantages would be that gas usage is slightly lower and it would be possible to expose all the Facets via the Diamond. As our goal is to maximize the security of our contracts, we believe that the Diamond pattern is not the right approach for achieving this. Nevertheless, we will continue to evaluate different patterns to provide the best user experience at the highest security level.
After careful consideration, we have decided against using the Diamond pattern for the next version of the Safe contracts. Our main priority is to provide a high level of security while maintaining flexibility for our users. To achieve this, we have designed Safe Smart Accounts to clearly separate the storage and logic code of the core contract from any plugins, such as modules, guards, or fallback handlers.
Diamond Proxies can be very useful, particularly for bigger projects where every transaction is inspected by multiple people, providing extreme flexibility. However, Diamond Proxies introduce complexity with multiple contracts directly influencing each other. Understanding this complexity and verifying that no transaction breaks anything becomes challenging. In our opinion, this makes Diamond Proxies unsuitable for accounts where transactions are triggered daily.
If you’d like to participate in the discussion for the Safe v2 contracts, feel free to share your thoughts in this Forum thread.