A secure multi-signature wallet is one of the key items in the Ethereum ecosystem. It’s evolving, but it’s different from 2017. it develops It goes from a relatively simple signature verification to a flexible, modular system.
After auditing Safe version 1.4.0, we realized the importance of Safe in the Ethereum ecosystem and decided to continue researching it. We’ve collected some tips and potential pitfalls to help safe users and developers build projects around them. Here are our findings for some valuable resources for those who decide to study further:
Alternative handler msg.sender
use msg.sender
This is very common in smart contracts. You can use this for access control as a mapping key, or encode it as another parameter to link data to a specific address. However, some design patterns can work in a less straightforward way. One of them is Safe. FallbackHandler
contract.
To expand the functionality of your safe, you will need to deploy new devices. separated It’s a contract called Fallback Handler. why? Safe is a battle-tested singleton contract, so changing it may not be the best idea. Instead, we can extend Safe’s functionality in any way we want by deploying proxies that point to Safe contracts and handlers.
How does it work? Initially FallbackHandler
Put the contract address in the safe. Then call the safe address using the following function: FallbackHandler
. Since that functionality is not implemented within the Safe contract, the call would be: fallback
The call is then forwarded to: FallbackHandler
.
During this call tracking msg.sender
in FallbackHandler
The function passes the address of the vault, not the original sender. It’s important to remember that anyone can call you. FalbackHandler
On behalf of safety. For example, a simple access control condition that only allows Safe to call a contract would be useless. To use the original sender’s address, use the following function: _msgSender()
Instead, it is implemented internally. Contract HandlerContext.
As you can see, this function extracts certain parts of the call data, including the original sender’s address. The process of storing the address inside the call data can be seen in the logic of the internal fallback function. contract FallbackManager
.
broken guard
Guards are smart contracts that implement specific data validation logic. In general, it includes two main features (Safe version 1.5.0 is Guard for module transactions):
checkTxBeforeExecution
checkTxAfterExecution
These functions work as hooks. Guard can implement any data validation to be performed before or after transaction execution. The following code fragment is execTransaction
function:
function setGuard()
You have to call in guards to raise them. This feature is implemented in the contract. GuardManager
, Safe inherits. This feature is protected by: authorized
A modifier that only allows calling Safe itself, i.e. Safe executes a transaction that calls Safe itself.
Problems may appear if recovery mechanisms are not considered or implemented before setting up the guard. If a guard is set and the transaction is reverted because the code contains a bug, The safe is covered with bricks. All setup functions are called via Safe self-call and cannot be performed if the guard code is broken and always reverted.
Mitigation for these scenarios can be modular. A module is a separate contract that has the power to execute secure transactions through its functionality. executeTxFromModule()
. To set up a module, perform the following function: enableModule()
in the contract ModuleManager
must be called. When the module is set up before Broken guard, module transaction can call setGuard()
To the address of the new duty guard. However, assuming your module is: ~ no set before, the guard is broken. In this case there is no way to add new modules. enableModule()
Protected by modifiers. recognized.
In the new version 1.5.0, guard calls are also performed in module transactions. Relieving the broken guard became more difficult and almost impossible.
powerful module
Modules are separate contracts that can perform transactions on behalf of Safe. The power of module transactions lies in the fact that no additional signature from the owner is required. What modules can do CALL
As well as DELEGATECALL
To a random address via function ExecTransactionFromModule()
.
If the logic of the called address includes a state change function, the state of the Safe contract will change. In extreme situations, we enter into agreements such as: selfdestruct
may be called. In a less extreme (but no less risky) scenario, the contract can be written into Safe’s save slot. For example, critical slots can be overwritten by owner address, module address, or threshold number. This leads to some simple advice: Before adding a module, always make sure you have properly audited the module and trust its owner..
Trusted Distributor
One scenario is when the safety owner does not notice that the module is connected to the safety. Inside the safety settings function, you will find the following features: setupModules()
is called, which does the following: DELEGATECALL
.
If a third-party distributor of an untrusted Safe contract decides to do something malicious, they can easily do so by: DELEGATECALL
. Because the owner does not sign setup calls, the Deployer has unrestricted ability to perform any safety operations during this initial setup phase. For example, a distributor can change a vault’s storage slots, accept tokens, or set up modules that have unrestricted permissions for future vaults. You can learn more about this issue here. OpenZeppelin Posts.
To prevent this scenario, the safety owner should double-check the values of the safety, configuration of connected modules, and storage slots, and ideally, a call trace of the configuration process.
tx.origin == msg.sender
This pattern is still used by many NFT projects to protect against bots for NFT issuance. However, the pattern is not compatible with account abstraction. Smart accounts have the ability to create transactions. This means: tx.origin
It is a contract, not an EOA. 2024 could be the year of account abstraction, so you should avoid using this pattern to avoid slowing down the adoption process.
Extract signatures from call data
Anyone with the signature can execute the transaction. The vault owner’s signature for a specific transaction is produced off-chain and passed to the function as an input parameter. If there are enough signatures to pass the threshold, a secure transaction is executed. Who is calling the function? That’s not important.
All data, including signatures, can be read from: calldata
Transactions in mempool. So if anyone reads this, there are no restrictions. calldata
And you decide to execute the trade. For value extraction (MEV), separate transaction sequences are managed with the goal of profit. Since there are no access controls (the presence of signatures and transaction data is access control), safe transactions can be selected from the mempool and included in an atomic transaction (Execution is performed in a smart contract.). These value extraction methods are much more powerful than traditional methods because the extractor has more control over the state.
How can these potential risks be mitigated? use OnlyOwnerGuard
Allows only the owner to call the executable function.
personal safe
Try your own 1-of-1 multi-signature secure wallet. There are many security advantages over cold wallets. You can:
- Rotate your private key
- Create your own recovery mechanism
- For added security, add new owners later.
- Update Check Mechanism
- Prepare for the future
And most importantly your address same.
Step 2 Threshold Increase
Safety contract (OwnerManager
Safe (which Safe inherited) includes the following features: addOwnerWithThreshold()
. This feature adds a new owner address while also increasing the threshold. There is no problem as long as you don’t make small mistakes.
Let’s say you have a personal 1/1 safe.
After some time, I decided to increase security by adding a second owner (e.g. a secondary cold wallet address) and upgrading the threshold to 2/2. The mentioned feature is more effective as it allows you to perform both steps simultaneously. But what happens if you accidentally enter the wrong address and increase the threshold? The vault will be covered with bricks. forever.
A simpler and error-free way is to add owners by calling a function with the same threshold. Then increase the threshold using the newly added owner address. You can see that no mistakes appear in this flow.
Transaction scanning and simulation
we can find simulate()
Functions inside the Safe codebase (SimulateTxAccessor
contract). This feature simulates safe transaction execution. From simulated transactions, you can extract call traces, contract state changes, events raised, balance changes, gas used, and more. All this information can give you more confidence before executing the actual transaction. This feature is integrated into Safe UX through: affectionatelyYou can simulate trades with one simple click.
This feature has been further enhanced with the introduction of: Turn off firewall By redefining. This cryptocurrency “firewall” scans transactions and checks for potential risks before they are signed. As a simple scenario, imagine a hacker taking control of the website of your favorite DeFi project. Once your wallet shows the transaction data to be signed, everything will look as usual.
Unfortunately, the data is rarely readable and we are used to signing it without verifying it. Here’s when DeFirewall does its work: Simulate transactions and use pop-up windows to highlight events or state changes that appear malicious. As an example:
- If the asset balance becomes 0 after the transaction
- The recipient’s address is a well-known hacker’s address.
Phishing has resulted in hundreds of millions of dollars in losses in the blockchain world, and spotting a well-crafted phishing operation can be difficult even for cybersecurity experts.