Tl;dr
Safe is at the forefront of modular Smart Account infrastructure, paving the way for developers to create a diverse range of applications and wallets
Integration interfaces to the Safe contracts are a critical part of unlocking new functionalities, while preserving security as a primitive
This article explains how these Safe modular interfaces: Plugins, Hooks, Function Handlers, and Signature Verifiers work together
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 and Account Abstraction. Safe has a set of integrations, which are complementary smart contracts, that greatly augment the functionality of Safe Accounts. This article will explain the different pieces that make up the Safe Modular Smart Account Architecture and how its integration interfaces work.
This content piece is geared towards readers with a basic understanding of programming, smart contracts and decentralised systems. If you’re interested in the technical implementation of our contracts, please directly refer to our GitHub repos.
The Safe Account is known as a proxy contract because it forwards, or “delegates” calls to the singleton contract. The singleton contract is where all the logic needed for a Safe to operate is held. Proxy contracts are particularly useful as they are typically cheaper to deploy and maintain on public blockchains, while also allowing for upgradability. The primary function of the Safe proxy contract is to store the state of the Safe.
We call contract state the variables (or values) that are stored and associated with a particular contract. The functions, hence the code of contracts, is typically stored and executed as bytecode in the EVM (Ethereum Virtual Machine) while variables are stored differently. The variables are the values affected by the functions when they’re called. In the context of Safe, the singleton contract holds the functions which update the state variables inside the proxy. The singleton contract is also known as the master copy of the Safe.
In the context of the Safe proxy contract; owners, threshold, plugin addresses, and hook addresses are all set as the variables of the proxy, hence defining its state. The functions that update those values come from the singleton contract.
Singleton contract management is another important element to understand the design and upgradability of the Safe contract. Singleton contracts are only deployed once on networks where Safe is available. Given that the singleton holds the logic, a new singleton is deployed when new functionalities are added to the Safe. Users are then free to upgrade their Safe to the singleton of their choice, based on their preferences.
To guarantee the integrity of singletons, and to make it easy for developers to verify that a particular singleton was deployed by the Safe team, the Safe team keeps track of singleton addresses under the Safe Deployments repository. Using the Safe Contracts repository, developers can run the deployment code locally to verify that the set of Safe contract addresses (e.g. singleton, proxy, function-handler) matches the one deployed on a particular network. If code changes are made to the deployment code, then the addresses generated locally should be different.
The deployments of both Safe proxies and singletons happen through factories. Factory contracts are smart contracts that are used to deploy other smart contracts. In the context of Safe, these factory contracts use the CREATE2 opcode of the EVM. Opcodes are low level instructions that are understood and executed by the EVM.
CREATE2 allows the deployment of contracts based on parameters such as owners’ addresses, and an arbitrary value provided by the contract deployer. One particular attribute of CREATE2 is that it allows to deterministically predict the address to which the contract will be deployed, without having to actually deploy it. This is particularly useful for complex transactions, security, and savings in gas costs. A contract address can remain undeployed up until it becomes necessary to actually deploy it.
Having explained the base Safe contracts, we can now look into Safe’s modular integration interfaces: Plugins, Hooks, Function Handlers, and Signature Verifiers. For flow diagrams of the interfaces, please refer to this article on the diamond proxy pattern.
The first pillar of Safe integrations is Plugins (also known as modules). To set up a plugin, a transaction must be executed by a Safe owner. Once executed, the transaction will update the state of the Safe with the plugin address as a state variable.
Plugins are added through a function call in the Safe singleton. Plugins are incredibly powerful, as they allow to add any custom logic and new features to the Safe contracts such as:
Payment streamings
Recovery mechanisms
Session keys
Safe Plugins are a bridge between the web2 and web3 world because they can hold any onchain function while interacting with offchain data through oracle systems. Any offchain or real-world data can be fed into the custom logic of a Safe Plugin to meet an array of use cases. Some examples include automation or applications that require heavy offchain computation such as 3D rendering or Machine Learning (ML) based applications. From prediction markets to real world assets, Safe Plugins are capable of connecting any offchain API (Application Programming Interface) with the capabilities of blockchain.
However, this extreme flexibility also involves some risks, as the verification for executing transactions from a Safe where a plugin is enabled solely relies on the plugin itself rather than the Safe. This opens the door to faulty or malicious plugins, which might alter the integrity of the Safe. This is particularly important as it implies that once set, and based on their configuration, Plugins can potentially overwrite basic Safe security features like signature thresholds to the benefit of other types of access control logic.
As plugins are self-containing smart contracts with their dedicated storage and functions, the Safe team has always been careful not to add plugins without diligently reviewing their implementation. Up to date, only the allowance module has been added to our repositories as a reference implementation to help teams understand how to write modules and plugins in a safe way.
A second major integration of the Safe contracts is Safe Hooks (also known as guards). Hooks are an additional security layer users and developers can set up on top of the multi-signature scheme. The Hook is a smart contract which will verify that certain conditions are met before and after executing a transaction.
Running checks on the state changes of the Safe proxy contract before and after a transaction gets executed, the Hook will accept or block transactions based on the requirements set by its user. The term “hook” refers to a pattern used by developers to write functions which will automatically execute after other functions are called. In the case of Safe, hooks are triggered based on the functions specified directly in the hooks contract.
To use Safe Hooks, users must first execute a transaction to add the hook. Hooks can be used to; run before/after checks on balances, owners, recipient addresses and much more. Implemented in conjunction with a threshold signature, they are a particularly useful design pattern to use for individuals, systems, and organisations with high security requirements.
One of the benefits of having hooks directly executed by contracts, is that these rules are enforced onchain. Hence, there is no way for an application to circumvent the rules specified in the hook contract.
Safe-based project Console by Brahma.fi is an example of a project making use of Safe Hooks in a smart and elegant way. Their architecture makes use of hooks to only allow whitelisted protocols in the hook to interact with a given Safe. Console also leverages plugins to enhance the functionalities accessible between the Safe owner and the whitelisted protocols, opening the door to automation and much more.
The Function Handler is an essential piece of the Safe contracts, as it allows for the Safe to theoretically respond to any instructions coming from arbitrary function calls. The Function Handler’s interface is currently defined via the fallback handler contract.
A function call is a set of inputs defined by users when calling functions in smart-contracts. Just like any real world contract, smart contracts do not hold an infinite amount of information. They are often only holding a set of narrow functions, which can only allow users to perform specific actions. Because the Function Handler contract holds logic, it is also considered a singleton.
In the case of Safe, the narrow set of functions is tailored around the multi-signature functionality. It is by design what makes the Safe contracts so secure. The access to the contract and its subsequent functions is only possible if the signature threshold required is met by the user(s). Being so essential to the Safe, these functions can be accessed without the need for a Function Handler.
When a user or a contract calls a function that does not exist inside the Safe (proxy) contract, the Safe contract “defaults” to the fallback function. The fallback function then forwards the function call to an external contract called the Function Handler. The Function Handler can be upgraded, so new functionalities can be added as new standards and functions are being developed. This design is fairly specific to Safe, as the fallback function typically sits within the contract that is called rather than a dedicated external contract.
One example where a Safe Function Handler is very useful is related to the Non-Fungible-Token standard (ERC-721). To ensure that a contract can handle a specific token, it is specified that a call to the receiving contract should be made before a transfer. If the receiving contract does not properly respond to this call, the transfer will be blocked. For this, the onERC721Received callback is used. As the Safe contract does not implement it in its core contract, it is required to enable a Function Handler that implements this function to enable secure NFT transfers to a Safe contract. The advantage is that this can be extended to the point that only specific NFTs are accepted, while for others, the incoming transfer will be blocked.
The final integration that this article will cover is Signature Verifiers. Digital signatures in the blockchain world are as much of a core primitive as consensus mechanisms are.
Digital signatures are rooted in the concept of asymmetric keys. Typically, asymmetric keys involve both a public and private key. While the public key is used to generate a public address and is known by all other users, the private key is kept private to the owner of the address, and is used to sign and verify transactions.
In Ethereum, EOAs are the primary means through which users interact with decentralised applications or dApps. Whenever they wish to interact with a particular contract, they are prompted by the application to sign messages, in order to prove that they own the public address from which they’re interacting. These messages are called digital signatures. Similarly, applications and other system participants can use these signatures to verify the authenticity of messages coming from public addresses. By checking the digital signature, the message, and the public address of the sender, one can prove the ownership and the integrity of the message.
However, smart contract accounts (Smart Accounts) do not consist of a public and private key pair, meaning that they cannot sign or verify messages on their own. Instead, they hold code and logic which can be called by EOAs and other contracts.
In 2018, this discrepancy in design led the Ethereum community to propose EIP1271 — a standard that suggests a way for smart contracts to handle digital signatures. The standard defines a function which can take a message and a signature, and return whether it is valid, according to the smart contract’s own terms. This offers developers greater flexibility to define the verification logic of their choice. While the standard has gained adoption over the past years, many projects and applications have still not implemented it, resulting in compatibility issues between some web3 applications and Smart Accounts like Safe.
Recently, our team has pushed a gasless implementation of EIP1271 via the safe transaction service and the fallback handler to augment the multi-signature use case. This enables Safe users to easily interact with applications such as OpenSea directly from Safe{Wallet}, making the Safe Smart Account standard yet more useful to the community.
While EIP1271 is a popular standard to handle digital signatures for smart contracts, efforts to improve the types of signatures supported by the EVM are constantly being made by the Ethereum community. Indeed, while Ethereum’s default signature scheme relies on the secp256k1 elliptic curve (learn more here), other schemes including secp256r1, are better supported by popular hardware and software solutions (such as Apple and Android). This fuels the need to push for new EIPs, including the recent EIP7212.
At Safe, we believe that the growth of the ecosystem is partly reliant on our ability to make digital signatures more seamless, so we can embed web3 transactions within the existing and future user experiences of non-web3 users. For this reason, we decide to treat signature verifiers as a standalone integration interface.
The Safe Modular Smart Account Architecture allows the flexible development of a wide array of decentralised applications and smart contract wallets using integration interfaces. To summarise, Safe’s architecture comprises components such as the Safe Account (proxy contract which holds the account state) and the singleton (which holds the logic), and is extensible through Plugins, Hooks, Function Handlers, and Signature Verifiers. This architecture allows for extensive and customised web3 architectures, while emphasising on security, composability, and interoperability.
Special thanks to Richard Meissner for his feedback and contributions.