Safe Audit Report 1 4 0
Safe Audit Report 1 4 0
Contracts 1.4.0
by Ackee Blockchain
28.3.2023
Contents
1. Document Revisions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2. Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.1. Ackee Blockchain . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.2. Audit Methodology . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.3. Finding classification. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.4. Review team. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.5. Disclaimer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3. Executive Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Revision 1.0. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Revision 1.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
4. Summary of Findings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
5. Report revision 1.0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
5.1. System Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
5.2. Trust model. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
M1: Broken guard can cause DoS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
M2: Lack of contract check . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
L1: Error-prone proxy constructor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
W1: Usage of delegatecalls . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
W2: Fallback handler can be set to address(this) . . . . . . . . . . . . . . . . . . . . . . . . 27
W3: Removed owner's stored hash . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
W4: Singleton address at slot 0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
W5: Call to disableModule can be frontrun . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
W6: Threshold can be set too high . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
I1: Code and comment inconsistency. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
I2: Require should be assert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
6. Report revision 1.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
2 of 46
Appendix A: How to cite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Appendix B: Woke outputs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
B.1. Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3 of 46
1. Document Revisions
0.1 Draft report March 14, 2023
4 of 46
2. Overview
This document presents our findings in reviewed contracts.
3. Manual code review - the code is checked line by line for common
vulnerabilities, code duplication, best practices and the code architecture
is reviewed.
5. Unit and fuzzy testing - run unit tests to ensure that the system works as
expected, potentially write missing unit or fuzzy tests.
5 of 46
2.3. Finding classification
A Severity rating of each finding is determined as a synthesis of two sub-
ratings: Impact and Likelihood. It ranges from Informational to Critical.
Low to High impact issues also have a Likelihood, which measures the
Severity
Likelihood
Warning - - - Warning
Info - - - Info
6 of 46
Impact
• High - Code that activates the issue will lead to undefined or catastrophic
consequences for the system.
• Low - Code that activates the issue will have outcomes on the system that
are either recoverable or don’t jeopardize its regular functioning.
• Warning - The issue cannot be exploited given the current code and/or
configuration (such as deployment scripts, compiler configuration, use of
multi-signature wallets for owners, etc.), but could be a security
vulnerability if these were to change slightly. If we haven’t found a way to
exploit the issue given the time constraints, it might be marked as a
"Warning" or higher, based on our best estimate of whether it is currently
exploitable.
• Info - The issue is on the borderline between code quality and security.
Examples include insufficient logging for critical operations. Another
example is that the issue would be security-related if code or
configuration (see above) was to change.
Likelihood
7 of 46
2.4. Review team
Member’s Name Position
2.5. Disclaimer
We’ve put our best effort to find all vulnerabilities in the system, however our
findings shouldn’t be considered as a complete list of all existing issues. The
statements made in this document should not be interpreted as investment
or legal advice, nor should its authors be held accountable for decisions made
based on them.
8 of 46
3. Executive Summary
Safe is a decentralized custody protocol allowing multi-signature (multi-sig)
wallets to be used as a single account. Businesses and individuals can use
multi-sig wallets for safe collective management, perform sensitive
transactions, and achieve redundancy. The protocol is widely used across the
Ethereum and EVM ecosystems.
Revision 1.0
Safe engaged Ackee Blockchain to perform a security review of the Safe
contracts version 1.4.0 with a total time donation of 10 engineering days in a
period between February 27 and March 10, 2023 and the lead auditor was
Lukáš Böhm.
The audit has been performed on the commit eb93dbb, and the scope was the
following contracts with all imports (recursively):
• SafeL2.sol
• proxies/SafeProxyFactory.sol
• handler/CompatibilityFallbackHandler.sol
• libraries/MultiSendCallOnly.sol
• libraries/SignMessageLib.sol
We began our review using static analysis tools Woke. We then took a deep
dive into the logic of the contracts. For testing and fuzzing, we have involved
Woke testing framework where we simulated deployment of the Safe and
focused on the correctness of signature and owner handling. The appendix
includes parts of the testing source core.
9 of 46
• signature validation,
• modules handling,
• owners handling,
• guard handling,
• access controls,
• data validation.
Our review resulted in 11 findings, ranging from Info to Medium severity. The
quality of the code is exceptional. NatSpec in-code documentation is part of
every contract and function. General documentation still needs to be
created, but Safe team provided a few documents describing the most
crucial part - signatures.
Revision 1.1
The review was done on March 28 on the given commit cb4b2b1.
The status of all reported issues has been updated and can be seen in the
findings table. Issues include client responses, comments, and pull requests
10 of 46
with specific responses, if any.
See Revision 1.1 for the review of the updated codebase and additional
information we consider essential for the current scope.
11 of 46
4. Summary of Findings
The following table summarizes the findings we identified during our review.
Unless overridden for purposes of readability, each finding contains:
• a Description,
• an Exploit scenario,
• a Solution.
There might often be multiple ways to solve or alleviate the issue, with
varying requirements regarding the necessary changes to the codebase. In
that case, we will try to enumerate them all, clarifying which solves the
underlying issue better (albeit possibly only with architectural changes) than
others.
12 of 46
Severity Reported Status
13 of 46
5. Report revision 1.0
5.1. System Overview
This section contains an outline of the audited contracts. Note that this is
meant for understandability purposes and does not replace project
documentation.
Contracts
SafeL2
The main logic of the protocol is in the Safe.sol contract. It allows execution
of the Safe transaction to a specific address, with a data payload, value, and
other parameters. All of these parameters must be signed by the owners of
the Safe. There are four different signatures:
• EOA signature
• EIP-712 signature
• Pre-Validated signature
14 of 46
added. SafeL2 extends the functionality of Safe by emitting events with
additional information.
OwnerManager
The contract manages the owners of Safe. It allows adding, removing, and
swapping owners. It also allows changing the threshold of the Safe. Functions
of the contract are protected by authorized modifier. The modifier allows only
the Safe contract to call the functions, which means a Safe transaction has
to be performed on the Safe contract to call itself.
GuardManager
The contract implements the logic for hooks that are called before and after
a Safe transaction is executed. Only one Guard can be set at the time. Setting
the guard is protected by authorized modifier.
ModuleManager
The contract enables and disables modules. These two functionalities are
protected by authorized modifier. If a module is set, it can execute a Safe
transaction without needing signatures to be passed as a parameter into the
function.
SafeProxyFactory
15 of 46
• Deploy Proxy with a callback and nonce
The new Proxy is created using the CREATE2 function, where the address can
be precalculated with salt. Singleton Safe contract code is used as a logic
contract for the new Proxy.
CompatibilityFallbackHandler
The contract provides compatibility between Safe version < 1.3.0 and 1.3.0 +.
It implements the EIP-1271 interface and other necessary functionalities for
the new Safe version.
Actors
Deployer
The deployer of the new Safe has the privilege to set up owners, threshold,
and fallback handler.
Owners
Owners of the Safe can sign transactions that are then executable by
anyone. The executor of the Safe transaction has to pass all necessary
signatures (defined by threshold) as an input argument to the Safe
transaction.
Modules
Modules are contracts that can execute Safe transactions without the need
for signatures. They can be enabled and disabled by the Safe transaction.
16 of 46
carefully select addresses with such trust and privilege over the system.
17 of 46
M1: Broken guard can cause DoS
Medium severity issue
Description
{
if (guard != address(0)) {
Guard(guard).checkAfterExecution(txHash, success);
}
}
If one of these two functions is broken or just reverts, it can cause DoS for
the whole Safe.
Vulnerability scenario
18 of 46
any reason, there is no way to execute a Safe transaction and change the
guard address.
Recommendation
Guard can work as an additional layer of security for the Safe. Nevertheless, if
the guard functions contain an issue that causes reverting transactions, Safe
should be able to execute transactions without it or have the ability to
change the guard address.
Fix 1.1
Client’s response:
19 of 46
M2: Lack of contract check
Medium severity issue
Description
For transferring tokens from the Safe contract to the payment token receiver
following function used:
20 of 46
It uses a low-level call to a predefined function selector
"transfer(address,uint256)" on the token’s address. However, if the address
The second place where contract check is suitable but not performed is the
execute function in the Executor contract.
if (operation == Enum.Operation.DelegateCall) {
// solhint-disable-next-line no-inline-assembly
assembly {
success := delegatecall(txGas, to, add(data, 0x20), mload(data), 0,
0)
}
} else {
// solhint-disable-next-line no-inline-assembly
assembly {
success := call(txGas, to, value, add(data, 0x20), mload(data), 0,
0)
}
}
The function is called from the Safe execution function, where the check of
whether the address to is a contract is not performed either. Especially for a
delegate call, it is important to check whether the address is a contract.
Vulnerability scenario
Recommendation
Perform the check whether the address is a contract before calling the low-
level call.
21 of 46
Additionally, differ the logic for EOA and contract calls.
Fix 1.1
Client’s response:
22 of 46
L1: Error-prone proxy constructor
Low severity issue
Description
The constructor of the SafeProxy contract does not use robust verification
for singleton address. Only the check for zero address is performed.
Vulnerability scenario
Recommendation
contract SafeProxy{
constructor(address _singleton) {
require(Safe(payable(_singleton)).identifier() == keccak256("safe-
1.4.0"), "Invalid singleton address provided");
singleton = _singleton;
}
...
}
contract Safe{
function identifier() public pure returns (bytes32) {
return keccak256("safe-1.4.0");
...
23 of 46
}
Fix 1.1
Client’s response:
24 of 46
W1: Usage of delegatecalls
Impact: Warning Likelihood: N/A
Description
Delegatecall in setup
The Safe contract uses the setup function to initialize its state. The setup
function can be viewed as a point of centralization as it is called before the
owners are set. What is more, the setup function can also result in a
delegatecall. That increases the possibility for the deployer to set up the
contract dishonestly.
To trust the setup, the owners must verify the code and the inputs to the
setup function.
Delegatecall in execute
can transform the contract’s storage into an inconsistent state. The target
contract might not be audited and might break some important invariants
(like the owner list validity, nonce linearly decreases, the threshold is at most
len(owners) etc.)). If the nonce decreases, transactions might be replayed. If
the threshold exceeds the number of owners, the contract might be locked
forever, etc.
25 of 46
Recommendation
Include the bare minimum of logic in the setup function. If a more delicate
setup is needed, consider moving it to the execute portion of the contract.
The delegatecall may be eventually needed, but splitting the setup into two
parts makes the verification process more transparent.
More generally, consider the usage of delegatecalls. The semantics can often
be easily replicated with a simple call, which is easier to verify and audit.
26 of 46
W2: Fallback handler can be set to address(this)
Impact: Warning Likelihood: N/A
Description
exceptional cases.
The authorized modifier enforces a self-call. The fallback handler contains the
following code:
So even though the first call will fall into the fallback function, the second
one might not.
27 of 46
Recommendation
Ensure the fallback handler cannot be set to address(this). This will not
reduce the functionality of the fallback handler and will ensure that the
handler cannot be set to address(this) by accident.
Fix 1.1
Client’s response:
• Add test
28 of 46
W3: Removed owner's stored hash
Impact: Warning Likelihood: N/A
Description
However, when one of the owners is removed, the hash of the message
he/she approved before remains stored. This fact violates the condition that
only owners can make message approvals.
Recommendation
Even though the pre-approved message hashes are not exploitable, there is
no reason to store hashes of the removed owner. Therefore, the hash of the
removed owner should be removed from the storage.
Fix 1.1
Client’s response:
29 of 46
W4: Singleton address at slot 0
Impact: Warning Likelihood: N/A
Description
The SafeProxy contract uses the proxy pattern to delegate calls to the logic
contract. The address of the logic contract is stored at slot 0.
contract SafeProxy {
// Singleton always needs to be first declared variable, to ensure that
it is at the same location in the contracts to which calls are delegated.
// To reduce deployment costs this variable is internal and needs to be
retrieved via `getStorageAt`
address internal singleton;
This is prone to error as it requires that the singleton variable is always the
first declared variable in the contract. If not, a slot collision can happen, and
the address can get overwritten.
Recommendation
30 of 46
More information can be found in the [OpenZeppelin
documentation](https://docs.openzeppelin.com/upgrades-plugins/1.x/
proxies#unstructured-storage-proxies).
31 of 46
W5: Call to disableModule can be frontrun
Impact: Warning Likelihood: N/A
Description
Modules can be added to the Safe and removed. Removing a module is done
by calling the disableModule function. However, the disabled transaction can
be front-run by a malicious module. Because the module can perform state
changes in the Safe it also can entirely mitigate the effect of the
disableModule call.
Recommendation
This issue cannot be mitigated as it is inherent to the Safe design. The issue
is included to demonstrate further the potential dangers of using modules.
Fix 1.1
Client’s response:
32 of 46
W6: Threshold can be set too high
Impact: Warning Likelihood: N/A
Description
The 5.1.1.2 contract allows adding new owners and changing the threshold.
The threshold can be set to arbitrarily high values if it is lower than the
number of owners.
However, there is an implicit limit for the threshold imposed by the block gas
limit. If the threshold is set too high, supplying enough signatures will not be
possible because of the gas limit.
Recommendation
Consider performing some more thorough calculations and setting a limit for
the threshold.
Fix 1.1
Client’s response:
33 of 46
I1: Code and comment inconsistency
Impact: Info Likelihood: N/A
Description
While declaring new variables in Safe contract, at the line #150 a zero value is
assigned,
uint256 moduleCount = 0;
uint8 v;
Even though the compiler assigns a zero value to the variables, it is a good
practice not to mix the two approaches.
In the contract ModuleManager the code comment at line #160 refers to the
variable currentModule, which does not exist in the code.
Recommendation
Stick with one approach for an assignment and use it consistently across the
codebase.
34 of 46
Fix 1.1
Client’s response:
35 of 46
I2: Require should be assert
Impact: Info Likelihood: N/A
Description
• OwnerManager.sol
◦ setupOwners #31
require(threshold == 0, "GS200");
• ModuleManager.sol
◦ setupModules #32
These invariant conditions should always be true and are not supposed to
happen during regular operations.
It is essential to remember that solidity version < 0.8.0 (allowed version for
Safe contracts) failing asserts are returning invalid opcode, which consumes
all remaining gas. On the other hand, require is returning unused gas.
36 of 46
Recommendation
The asserts provide more information for reviewers and auditors because
they convey that the given condition should always be true. Using requires
may be confusing because it implies that the condition could sometimes
revert.
Fix 1.1
Client’s response:
37 of 46
6. Report revision 1.1
No significant changes were performed in the logic of contracts. Events were
modified in several places in the codebase (PR #542). They are now indexed
for better off-chain access. All other changes address reported issues.
38 of 46
Appendix A: How to cite
Please cite this document as:
39 of 46
Appendix B: Woke outputs
B.1. Tests
The following code shows the functions implemented in Woke testing
framework for building every type of signature payload that is used in the
Safe contract and also create_mutlisig function for creating the final
signature byte payload.
# Static part v == 0
def get_eip_sig(address, offset):
# r - contract address
contract = 12 * b"\x00" + bytes.fromhex(str(address)[2:])
# s - pointing to dynamic data start
data_pointer = int.to_bytes(offset, 32, "big")
# v - type
sig_type = b"\x00"
static_part = contract + data_pointer + sig_type
return static_part
# Dynamic part v == 0
def get_eip_dynamic_data(data):
# 32 bytes - len of following data
data_len = int.to_bytes(len(data), 32, "big")
# len + data
dynamic_part = data_len + data
return dynamic_part
# default sig
40 of 46
def get_classic_sig(address, hash):
signature = address.sign_hash(hash)
return signature
41 of 46
elif tup[0] == '30':
multisig += get_formated_sig(tup[1], hash)
else:
multisig += get_classic_sig(tup[1], hash)
multisig += eip_data
return multisig
42 of 46
Initial deployment code of the Safe by SafeProxyFactory.
43 of 46
Example of guard setup Safe transaction with provided signatures:
# Default sig
a = Account.from_alias("test")
a.balance = 10*(10**18)
# V > 30 sig
b = default_chain.accounts[1]
c = default_chain.accounts[2]
d = default_chain.accounts[3]
e = default_chain.accounts[4]
# V == 0 sig
contract_1 = SignatureValidator.deploy(from_=c)
contract_2 = SignatureValidator.deploy(from_=e)
# 0 < treshold <= len(owners)
owners = [a,b,contract_1,d,contract_2]
treshold = 5
...
guard = DebugTransactionGuard.deploy(from_=c)
tx_abi = Abi.encode_call(Safe.setGuard, [guard.address])
###### SETTING GUARD ######
to = safe.address
value = 0
data = tx_abi
operation = Enum.Operation.Call
safe_tx_gas = 100000
base_gas = 100000
gas_price = 0
nonce = 0
tx_data = safe.encodeTransactionData(
to,
value,
data,
operation,
safe_tx_gas,
base_gas,
gas_price,
payment_token,
payment_receiver,
44 of 46
nonce,
from_=d
)
tx_hash = keccak256(tx_data)
contract.sign(tx_hash, b"\x00",from_=c)
contract_2.sign(tx_hash, b"\x00",from_=e)
tx = safe.execTransaction(
to,
value,
data,
operation,
safe_tx_gas,
base_gas,
gas_price,
payment_token,
payment_receiver,
multisig,
from_=d,
return_tx=True
)
45 of 46
Thank You
Ackee Blockchain a.s.
h ps://twi er.com/AckeeBlockchain