Building a Guard for Safe Smart Account
This tutorial demonstrates how to build a custom Guard contract that adds security restrictions to a Safe Smart Account. You'll learn how to:
- Create a Safe Guard that prevents delegate calls
- Enable the Guard on a Safe Smart Account
- Write comprehensive tests for the Guard
You'll build a NoDelegatecallGuard
that blocks delegatecall
operations through the Safe account. While this is a simple example, the same principles can be used to build more complex Guards for your specific security needs.
A Safe account supports two types of transactions:
- Safe Transaction: Executed through the Safe owners with required signatures
- Module Transaction: Executed through an enabled Safe Module
This tutorial focuses on Safe Transactions, as Guards only apply to those.
Understanding Safe Guards
Before we dive into the code, let's understand what Guards do:
- Guards are contracts that can inspect and validate transactions before and after they are executed by a Safe
- They implement a standard interface with two key functions:
checkTransaction
: Called before execution to validate the transactioncheckAfterExecution
: Called after execution for post-transaction checks
- Guards can block transactions by reverting if validation fails
Important Notice: The smart contract code provided in this tutorial is intended solely for educational purposes and serves only as an illustrative example. This example code has not undergone any security audits or formal verification processes. Safe does not guarantee the reliability, security, or correctness of this example code. Before deploying any smart contract code in a production environment, developers must conduct a thorough security audit and ensure rigorous testing procedures have been performed.
Only enable Guards from trusted and audited code. A malicious Guard could block all transactions and make your Safe unusable.
Prerequisites
Before starting this tutorial, make sure you have:
- Experience with Solidity (opens in a new tab) and Hardhat (opens in a new tab)
- Node.js (opens in a new tab) and npm (opens in a new tab) installed
- Basic understanding of Smart Account concepts
Project Setup
Initialize Project
Create a new project directory and initialize npm:
Configure Dependencies
Add the following to your package.json
:
Install the required dependencies:
Setup Hardhat project
Initialize a TypeScript Hardhat project:
Now, try compiling the contracts to ensure everything is set up correctly.
Select Create a TypeScript project
when prompted.
When compiling Safe contracts with solidity 0.8.x the bytecode size exceeds the limit of 24KB. To overcome this, set allowUnlimitedContractSize
to true
in the hardhat config.
In practise with production networks, use the officially deployed Safe contracts. Also, add dependencyCompiler
to import SafeProxyFactory
and Safe
contracts.
Update your hardhat.config.ts
:
Create a new Solidity contract
Delete the default contracts/Lock.sol
and test file test/Lock.ts
and create a new Solidity contract NoDelegatecallGuard.sol
in the contracts
directory.
Step 1. Create NoDelegatecallGuard contract
Explanation:
BaseGuard.sol
: BaseGuard is an abstract contract that implements ERC-165 and inherits the Guard interface with two functions:checkTransaction
: This function is called before Safe transaction is executed.checkAfterExecution
: This function is called after Safe transaction is executed.
Enum.sol
: Provides EnumOperation
which can have values likeCall
orDelegateCall
.DelegatecallNotAllowed
is a custom error type that will be used to revert the transaction ifdelegatecall
is detected.
Step 2: Implement checkTransaction
function
Explanation:
- The
checkTransaction
function checks if the operation type isDelegateCall
. If it is, the function reverts with a custom errorDelegatecallNotAllowed
.
Step 3: Implement checkAfterExecution
function
Explanation:
- The
checkAfterExecution
function is empty as we do not need to perform any action after the transaction is executed.
Final contract code
Testing the contract
Step 1: Create test/utils/utils.ts file
Create a new file named utils.ts
in the test/utils
directory and include the code below.
Explanation:
- This file contains utility function to execute transaction through the Safe account.
Step 2: Start with a boilerplate test file
Create a new file named NoDelegatecallGuard.test.ts
and include the following basic structure that will be filled in later steps (ignore the warnings about unused imports):
Step 3: Setup contracts and variables in before hook
This step sets up the test environment by deploying and configuring the necessary contracts. Please note that:
- Alice is the only owner of the Safe and a threshold of 1 is set. Thus, only Alice's signature is required to execute transactions.
- Alice as the owner of the Safe is required to set the guard.
- The guard is enabled by calling the
setGuard
function on the Safe contract. - ⚠️ Security Note: Only trusted and audited code should be enabled as a guard, since guard can block transactions. A malicious guard make Safe unusable by blocking all transactions.
Step 4: Add test cases
Final test code
Run the tests
Congratulations! You have successfully created, enabled and tested a Safe Guard.
Do more with Safe and Guard
Did you encounter any difficulties? Let us know by opening an issue (opens in a new tab) or asking a question on Stack Exchange (opens in a new tab) with the safe-core
tag.