🕵️♂️ Hi! I'm a passionate blockchain enthusiast dedicating my time to discover and address security bugs.
🕵️♂️ Hi! I'm a passionate blockchain enthusiast dedicating my time to discover and address security bugs.

Subscribe to Cizeon

Subscribe to Cizeon
Share Dialog
Share Dialog
<100 subscribers
<100 subscribers


We have seen in part 1 that a Gnosis Safe wallet is only a proxy contract that redirects all transaction to an implementation contract.
The implementation contract source code can be found on github.
contract Safe is
Singleton,
NativeCurrencyPaymentFallback,
ModuleManager,
GuardManager,
OwnerManager,
SignatureDecoder,
SecuredTokenTransfer,
ISignatureValidatorConstants,
FallbackManager,
StorageAccessible,
ISafe
{
The Safe contracts inherit from multiple other contracts, each containing different wallet functionalities. Today, we will focus on the OwnerManager.
The Gnosis Safe wallet, often referred to as a multi-signature (or multi-sig) wallet, allows multiple owners to manage a single wallet. Transactions can only be executed once a predefined number of owners have approved them. This significantly reduces the risk of a compromised owner, as a quorum of owners must approve transactions.
The source code for the OwnerManager module is is located here.
Several storage variables are declared.
mapping(address => address) internal owners;
uint256 internal ownerCount;
uint256 internal threshold;
First, a mapping from address to address stores the list of owners. If you’re not familiar with Solidity's mappings, think of them as a HashMap or a dictionary in Python, where both the key and the value are addresses here.
The second variable, ownerCount, simply represents the number of owners.
Lastly, the threshold variable defines the number of owners required to approve a transaction. If set to 1, any owner can execute transactions. If set to 2, at least two owners must approve, regardless of the total number of owners.
This was simple. Let’s see how we can retrieve these variables on-chain.
On line 120, there’s a public view function that returns the threshold value. We’ll call it using the cast command from Foundry. If you haven’t installed Foundry yet, refer to the first article for instructions.
Pay close attention here. A solid understanding of this is essential for part 3 of the series.
On Ethereum, a transaction consists of several parameters. Here are the most important ones:
from: The sender’s address
to: The recipient’s address
value: The amount of ETH to send, 0 if none.
input data: Optional field to include data.
Addresses can also belong to smart contracts. For example, when sending ERC-20 tokens, you’re actually sending a transaction to the ERC-20 smart contract, which then calls the transfer() function with two parameters: the recipient (to) and the amount of tokens to send.
The function to call and its parameters are recorded in the input data field, known as calldata. This is the data passed along by our proxy contract.
To encode the calldata, we need the function’s signature. This consists of the function’s name and the data types of its parameters, separated by commas, no spaces. For the transfer() function of an ERC-20 token, the signature would be:
transfer(address,uint256)
The function’s signature is then hashed using keccak256 and only the first 4 bytes of the hash are taken as function selector.
$ cast keccak256 "transfer(address,uint256)"
0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
The transfer function selector is: 0xa9059cbb. We would then append an address and an amount. Luckily we can use the cast command to pack this for us, and even send the transaction.
$ cast calldata "transfer(address,uint256)" 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D 10000000
0xa9059cbb000000000000000000000000e27f243cd5cb7364bbae758bb05aa62ec2a5fb7d0000000000000000000000000000000000000000000000000000000000989680
If this transaction is sent to the USDC smart contract, the calldata would be decoded as a call to the transfer() function, instructing it to send 10 USDC to the address 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D.
The observant reader might have guessed that, with just the calldata, it’s impossible to retrieve the function’s signature from the function selector. This is because there’s no way to reverse a hash back to its original data.
However, there are several databases of function selectors, and the most commonly used ones are stored. The transfer() function is on of them.
$ cast 4byte 0xa9059cbb
transfer(address,uint256)
We now have all the pieces needed to retrieve the threshold value from a Safe wallet. We’ll send a transaction to the wallet’s address (the proxy contract) which will borrow the code from the implementation contract containing the OwnerManager module. Are you following so far? 🙂
We can retrieve the function’s signature from the source code on line 120. There are no parameters.
getThreshold()
We don’t need to manually compute the calldata; we’ll simply ask the cast command to send the transaction for us. Since it’s a public view function, no gas is required.
$ export ETH_RPC_URL=https://rpc.gnosischain.com
$ cast call 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D "getThreshold()"
0x0000000000000000000000000000000000000000000000000000000000000001
For this Gnosis Safe wallet, only one owner or signer is needed to meet the threshold.
Similarly, on line 134, there’s a public view function called getOwners(), which returns an array of addresses. To help cast understand the return value, we can specify its type.
$ cast call 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D "getOwners() returns (address[])"
[0x6D5904167423e36A07427340d14804798039D4e6, 0xffF4aA9E37a3661db92d550936E5e27442aa5fa6]
There are two owners, but only one is needed to execute a transaction from the wallet.
This is all we need to know to retrieve information on-chain from a wallet.
Since we’re here to learn and understand how things work, let’s take a look at the OwnerManager source code.
The list of owners is stored in an address to address mapping and unlike an array, it is not possible to simply loop through each elements of the array.
mapping(address => address) internal owners;
The Gnosis Safe developers have done it in a very similar way that a linked list. A linked list is a data structure where each element contains a value and a reference to the next element in the sequence.

Let’s observe the getOwners() funtion.
address internal constant SENTINEL_OWNERS = address(0x1);
function getOwners() public view override returns (address[] memory) {
address[] memory array = new addressUnsupported embed;
// populate return array
uint256 index = 0;
address currentOwner = owners[SENTINEL_OWNERS];
while (currentOwner != SENTINEL_OWNERS) {
array[index] = currentOwner;
currentOwner = owners[currentOwner];
++index;
}
return array;
}
First, it reads the address stored in the mapping for address(0x1), which gives the address of the first owner.
Then, it reads the address stored at the first owner’s address, revealing the second owner’s address.
This process continues until the last owner points back to address(0x1).
In this way, it’s possible to loop through each owner in the mapping while remaining gas-efficient.

As we saw in our first article, the cast command allows us to retrieve on-chain storage, even if the value is private. Remember, with the EVM, nothing is truly private. But before we dive in, we need to understand how mappings are stored in storage.
The EVM storage is essentially a large array of 256-bit elements, starting at index 0 up to 2²⁵⁶. Each storage variables are stored one after each others. We can simply count each variables to find its slot number. This is why, to get the address of the singleton contract, we instructed cast to fetch storage at index 0.
$ cast storage 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D 0
0x00000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c762
If the source code of the smart-contract was also published to Etherscan or Gnosisscan here, cast can count the slots number for us.
$ export ETHERSCAN_API_KEY=**********************************
$ cast storage 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D --proxy 0x29fcb43b46531bca003ddc8fcb67ffe91900c762
╭-----------------------+------------------------------------------+------+
| Name | Type | Slot |
+==========================================================================
| singleton | address | 0 |
|-----------------------+------------------------------------------+------+
| modules | mapping(address => address) | 1 |
|-----------------------+------------------------------------------+------+
| owners | mapping(address => address) | 2 |
|-----------------------+------------------------------------------+------+
| ownerCount | uint256 | 3 |
|-----------------------+------------------------------------------+------+
| threshold | uint256 | 4 |
|-----------------------+------------------------------------------+------+
| nonce | uint256 | 5 |
|-----------------------+------------------------------------------+------+
| _deprecatedDomainSeparator | bytes32 | 6 |
|-----------------------+------------------------------------------+------+
| signedMessages | mapping(bytes32 => uint256) | 7 |
|-----------------------+------------------------------------------+------+
| approvedHashe | mapping(address => mapping(bytes32 => uint256)) | 8 |
╰-----------------------+------------------------------------------+------+
If the proxy argument isn’t available in your version of Foundry, simply enter the address of the singleton contract instead. The command output here is truncated for clarity.
We now know that the owner’s mapping is stored in the second slot. However, the entire mapping can’t fit in 256 bits. In fact, this variable serves only as a placeholder, and what’s actually stored inside isn’t relevant.
$ cast storage 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D 2
0x0000000000000000000000000000000000000000000000000000000000000000
Rather, the mapping’s values are stored in different slot numbers that we need to compute. For that we need to concatenate the key value with the mapping’s slot number, two in this case. The hash of it is used as the slot number. Let’s use the chisel command also from Foundry. Chisel is a Solidity REPL (Read-Eval-Print-Loop) tool. It evaluates Solidity code and returns the result.
The first key value for our mapping is address(0x1), the SENTINEL_OWNERS constant. We need to concatenate it with the mapping’s slot number.
$ chisel
Welcome to Chisel! Type `!help` to show available commands.
➜ abi.encode(1,2)
Type: dynamic bytes
├ Hex (Memory):
├─ Length ([0x00:0x20]): 0x0000000000000000000000000000000000000000000000000000000000000040
├─ Contents ([0x20:..]): 0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002
├ Hex (Tuple Encoded):
├─ Pointer ([0x00:0x20]): 0x0000000000000000000000000000000000000000000000000000000000000020
├─ Length ([0x20:0x40]): 0x0000000000000000000000000000000000000000000000000000000000000040
└─ Contents ([0x40:..]): 0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002
Let’s compute the hash. This is our slot number.
➜ keccak256(abi.encode(1,2))
Type: bytes32
└ Data: 0xe90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0
If correct that value at this slot number should be our first owner.
$ cast storage 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D 0xe90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0
0x0000000000000000000000006d5904167423e36a07427340d14804798039d4e6
If we want to retrieve the address of the second owner, we need to compute the slot number.
$ chisel
keccak256(abi.encode(0x6d5904167423e36a07427340d14804798039d4e6,2))
Type: bytes32
└ Data: 0x9af88d12773e5e18abf0cac977452a5c1dbfefcdef018154daf5409a67cd4bfe
Let’s retrieve the second owner’s address from the contract’s storage.
$ cast storage 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D 0x9af88d12773e5e18abf0cac977452a5c1dbfefcdef018154daf5409a67cd4bfe
0x000000000000000000000000fff4aa9e37a3661db92d550936e5e27442aa5fa6
We already know there are only two owners for this wallet, the address of the third mapping slot should be address(0x1).
$ chisel
➜ keccak256(abi.encode(0xfff4aa9e37a3661db92d550936e5e27442aa5fa6,2))
Type: bytes32
└ Data: 0x7e0181350b9666b922b76c7918cbfa0c8b34d0de78f3a35620d03967f396f283
$ cast storage 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D 0x7e0181350b9666b922b76c7918cbfa0c8b34d0de78f3a35620d03967f396f283
0x0000000000000000000000000000000000000000000000000000000000000001
The loop is closed :-).
Lastly, we can see there are a couple of internal functions to setup owners.
function setupOwners(address[] memory _owners, uint256 _threshold) internal {
function addOwnerWithThreshold(address owner, uint256 _threshold) public override authorized {
function removeOwner(address prevOwner, address owner, uint256 _threshold) public override authorized {
function swapOwner(address prevOwner, address oldOwner, address newOwner) public override authorized {
They are all protected by the authorized modifier.
function requireSelfCall() private view {
if (msg.sender != address(this)) revertWithError("GS031");
}
modifier authorized() {
// Modifiers are copied around during compilation. This is a function call as it minimized the bytecode size
requireSelfCall();
_;
}
Only the Gnosis Safe wallet itself has the ability to change the owners.
Several other protections are also implemented:
// Owner address cannot be null, the sentinel or the Safe itself.
if (newOwner == address(0) || newOwner == SENTINEL_OWNERS || newOwner == address(this)) revertWithError("GS203");
An owner cannot be address(0x0) or address(0x1), nor can it be the wallet itself.
// No duplicate owners allowed.
if (owners[owner] != address(0)) revertWithError("GS204");
Duplicates owners are also not permitted.
If the owner’s list needs to be updated, the transaction must go through the transaction signing process and reach the quorum to be executed on-chain.
This is what we need to explore next: how transactions that must be executed by the smart contract are encoded, signed, and then executed.
We’ll dive into this in the next article of the series. Stay tuned!
We have seen in part 1 that a Gnosis Safe wallet is only a proxy contract that redirects all transaction to an implementation contract.
The implementation contract source code can be found on github.
contract Safe is
Singleton,
NativeCurrencyPaymentFallback,
ModuleManager,
GuardManager,
OwnerManager,
SignatureDecoder,
SecuredTokenTransfer,
ISignatureValidatorConstants,
FallbackManager,
StorageAccessible,
ISafe
{
The Safe contracts inherit from multiple other contracts, each containing different wallet functionalities. Today, we will focus on the OwnerManager.
The Gnosis Safe wallet, often referred to as a multi-signature (or multi-sig) wallet, allows multiple owners to manage a single wallet. Transactions can only be executed once a predefined number of owners have approved them. This significantly reduces the risk of a compromised owner, as a quorum of owners must approve transactions.
The source code for the OwnerManager module is is located here.
Several storage variables are declared.
mapping(address => address) internal owners;
uint256 internal ownerCount;
uint256 internal threshold;
First, a mapping from address to address stores the list of owners. If you’re not familiar with Solidity's mappings, think of them as a HashMap or a dictionary in Python, where both the key and the value are addresses here.
The second variable, ownerCount, simply represents the number of owners.
Lastly, the threshold variable defines the number of owners required to approve a transaction. If set to 1, any owner can execute transactions. If set to 2, at least two owners must approve, regardless of the total number of owners.
This was simple. Let’s see how we can retrieve these variables on-chain.
On line 120, there’s a public view function that returns the threshold value. We’ll call it using the cast command from Foundry. If you haven’t installed Foundry yet, refer to the first article for instructions.
Pay close attention here. A solid understanding of this is essential for part 3 of the series.
On Ethereum, a transaction consists of several parameters. Here are the most important ones:
from: The sender’s address
to: The recipient’s address
value: The amount of ETH to send, 0 if none.
input data: Optional field to include data.
Addresses can also belong to smart contracts. For example, when sending ERC-20 tokens, you’re actually sending a transaction to the ERC-20 smart contract, which then calls the transfer() function with two parameters: the recipient (to) and the amount of tokens to send.
The function to call and its parameters are recorded in the input data field, known as calldata. This is the data passed along by our proxy contract.
To encode the calldata, we need the function’s signature. This consists of the function’s name and the data types of its parameters, separated by commas, no spaces. For the transfer() function of an ERC-20 token, the signature would be:
transfer(address,uint256)
The function’s signature is then hashed using keccak256 and only the first 4 bytes of the hash are taken as function selector.
$ cast keccak256 "transfer(address,uint256)"
0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
The transfer function selector is: 0xa9059cbb. We would then append an address and an amount. Luckily we can use the cast command to pack this for us, and even send the transaction.
$ cast calldata "transfer(address,uint256)" 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D 10000000
0xa9059cbb000000000000000000000000e27f243cd5cb7364bbae758bb05aa62ec2a5fb7d0000000000000000000000000000000000000000000000000000000000989680
If this transaction is sent to the USDC smart contract, the calldata would be decoded as a call to the transfer() function, instructing it to send 10 USDC to the address 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D.
The observant reader might have guessed that, with just the calldata, it’s impossible to retrieve the function’s signature from the function selector. This is because there’s no way to reverse a hash back to its original data.
However, there are several databases of function selectors, and the most commonly used ones are stored. The transfer() function is on of them.
$ cast 4byte 0xa9059cbb
transfer(address,uint256)
We now have all the pieces needed to retrieve the threshold value from a Safe wallet. We’ll send a transaction to the wallet’s address (the proxy contract) which will borrow the code from the implementation contract containing the OwnerManager module. Are you following so far? 🙂
We can retrieve the function’s signature from the source code on line 120. There are no parameters.
getThreshold()
We don’t need to manually compute the calldata; we’ll simply ask the cast command to send the transaction for us. Since it’s a public view function, no gas is required.
$ export ETH_RPC_URL=https://rpc.gnosischain.com
$ cast call 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D "getThreshold()"
0x0000000000000000000000000000000000000000000000000000000000000001
For this Gnosis Safe wallet, only one owner or signer is needed to meet the threshold.
Similarly, on line 134, there’s a public view function called getOwners(), which returns an array of addresses. To help cast understand the return value, we can specify its type.
$ cast call 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D "getOwners() returns (address[])"
[0x6D5904167423e36A07427340d14804798039D4e6, 0xffF4aA9E37a3661db92d550936E5e27442aa5fa6]
There are two owners, but only one is needed to execute a transaction from the wallet.
This is all we need to know to retrieve information on-chain from a wallet.
Since we’re here to learn and understand how things work, let’s take a look at the OwnerManager source code.
The list of owners is stored in an address to address mapping and unlike an array, it is not possible to simply loop through each elements of the array.
mapping(address => address) internal owners;
The Gnosis Safe developers have done it in a very similar way that a linked list. A linked list is a data structure where each element contains a value and a reference to the next element in the sequence.

Let’s observe the getOwners() funtion.
address internal constant SENTINEL_OWNERS = address(0x1);
function getOwners() public view override returns (address[] memory) {
address[] memory array = new addressUnsupported embed;
// populate return array
uint256 index = 0;
address currentOwner = owners[SENTINEL_OWNERS];
while (currentOwner != SENTINEL_OWNERS) {
array[index] = currentOwner;
currentOwner = owners[currentOwner];
++index;
}
return array;
}
First, it reads the address stored in the mapping for address(0x1), which gives the address of the first owner.
Then, it reads the address stored at the first owner’s address, revealing the second owner’s address.
This process continues until the last owner points back to address(0x1).
In this way, it’s possible to loop through each owner in the mapping while remaining gas-efficient.

As we saw in our first article, the cast command allows us to retrieve on-chain storage, even if the value is private. Remember, with the EVM, nothing is truly private. But before we dive in, we need to understand how mappings are stored in storage.
The EVM storage is essentially a large array of 256-bit elements, starting at index 0 up to 2²⁵⁶. Each storage variables are stored one after each others. We can simply count each variables to find its slot number. This is why, to get the address of the singleton contract, we instructed cast to fetch storage at index 0.
$ cast storage 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D 0
0x00000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c762
If the source code of the smart-contract was also published to Etherscan or Gnosisscan here, cast can count the slots number for us.
$ export ETHERSCAN_API_KEY=**********************************
$ cast storage 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D --proxy 0x29fcb43b46531bca003ddc8fcb67ffe91900c762
╭-----------------------+------------------------------------------+------+
| Name | Type | Slot |
+==========================================================================
| singleton | address | 0 |
|-----------------------+------------------------------------------+------+
| modules | mapping(address => address) | 1 |
|-----------------------+------------------------------------------+------+
| owners | mapping(address => address) | 2 |
|-----------------------+------------------------------------------+------+
| ownerCount | uint256 | 3 |
|-----------------------+------------------------------------------+------+
| threshold | uint256 | 4 |
|-----------------------+------------------------------------------+------+
| nonce | uint256 | 5 |
|-----------------------+------------------------------------------+------+
| _deprecatedDomainSeparator | bytes32 | 6 |
|-----------------------+------------------------------------------+------+
| signedMessages | mapping(bytes32 => uint256) | 7 |
|-----------------------+------------------------------------------+------+
| approvedHashe | mapping(address => mapping(bytes32 => uint256)) | 8 |
╰-----------------------+------------------------------------------+------+
If the proxy argument isn’t available in your version of Foundry, simply enter the address of the singleton contract instead. The command output here is truncated for clarity.
We now know that the owner’s mapping is stored in the second slot. However, the entire mapping can’t fit in 256 bits. In fact, this variable serves only as a placeholder, and what’s actually stored inside isn’t relevant.
$ cast storage 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D 2
0x0000000000000000000000000000000000000000000000000000000000000000
Rather, the mapping’s values are stored in different slot numbers that we need to compute. For that we need to concatenate the key value with the mapping’s slot number, two in this case. The hash of it is used as the slot number. Let’s use the chisel command also from Foundry. Chisel is a Solidity REPL (Read-Eval-Print-Loop) tool. It evaluates Solidity code and returns the result.
The first key value for our mapping is address(0x1), the SENTINEL_OWNERS constant. We need to concatenate it with the mapping’s slot number.
$ chisel
Welcome to Chisel! Type `!help` to show available commands.
➜ abi.encode(1,2)
Type: dynamic bytes
├ Hex (Memory):
├─ Length ([0x00:0x20]): 0x0000000000000000000000000000000000000000000000000000000000000040
├─ Contents ([0x20:..]): 0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002
├ Hex (Tuple Encoded):
├─ Pointer ([0x00:0x20]): 0x0000000000000000000000000000000000000000000000000000000000000020
├─ Length ([0x20:0x40]): 0x0000000000000000000000000000000000000000000000000000000000000040
└─ Contents ([0x40:..]): 0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002
Let’s compute the hash. This is our slot number.
➜ keccak256(abi.encode(1,2))
Type: bytes32
└ Data: 0xe90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0
If correct that value at this slot number should be our first owner.
$ cast storage 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D 0xe90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0
0x0000000000000000000000006d5904167423e36a07427340d14804798039d4e6
If we want to retrieve the address of the second owner, we need to compute the slot number.
$ chisel
keccak256(abi.encode(0x6d5904167423e36a07427340d14804798039d4e6,2))
Type: bytes32
└ Data: 0x9af88d12773e5e18abf0cac977452a5c1dbfefcdef018154daf5409a67cd4bfe
Let’s retrieve the second owner’s address from the contract’s storage.
$ cast storage 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D 0x9af88d12773e5e18abf0cac977452a5c1dbfefcdef018154daf5409a67cd4bfe
0x000000000000000000000000fff4aa9e37a3661db92d550936e5e27442aa5fa6
We already know there are only two owners for this wallet, the address of the third mapping slot should be address(0x1).
$ chisel
➜ keccak256(abi.encode(0xfff4aa9e37a3661db92d550936e5e27442aa5fa6,2))
Type: bytes32
└ Data: 0x7e0181350b9666b922b76c7918cbfa0c8b34d0de78f3a35620d03967f396f283
$ cast storage 0xE27f243CD5CB7364Bbae758Bb05AA62ec2a5Fb7D 0x7e0181350b9666b922b76c7918cbfa0c8b34d0de78f3a35620d03967f396f283
0x0000000000000000000000000000000000000000000000000000000000000001
The loop is closed :-).
Lastly, we can see there are a couple of internal functions to setup owners.
function setupOwners(address[] memory _owners, uint256 _threshold) internal {
function addOwnerWithThreshold(address owner, uint256 _threshold) public override authorized {
function removeOwner(address prevOwner, address owner, uint256 _threshold) public override authorized {
function swapOwner(address prevOwner, address oldOwner, address newOwner) public override authorized {
They are all protected by the authorized modifier.
function requireSelfCall() private view {
if (msg.sender != address(this)) revertWithError("GS031");
}
modifier authorized() {
// Modifiers are copied around during compilation. This is a function call as it minimized the bytecode size
requireSelfCall();
_;
}
Only the Gnosis Safe wallet itself has the ability to change the owners.
Several other protections are also implemented:
// Owner address cannot be null, the sentinel or the Safe itself.
if (newOwner == address(0) || newOwner == SENTINEL_OWNERS || newOwner == address(this)) revertWithError("GS203");
An owner cannot be address(0x0) or address(0x1), nor can it be the wallet itself.
// No duplicate owners allowed.
if (owners[owner] != address(0)) revertWithError("GS204");
Duplicates owners are also not permitted.
If the owner’s list needs to be updated, the transaction must go through the transaction signing process and reach the quorum to be executed on-chain.
This is what we need to explore next: how transactions that must be executed by the smart contract are encoded, signed, and then executed.
We’ll dive into this in the next article of the series. Stay tuned!
No activity yet