(Ex-Gnosis) Safe Wallet Manual Transaction Execution
Introduction
Gnosis Safe is one of the most popular choices when it comes to multi-signature wallets.
Though it provides great security over your assets/permissions and a smooth overall experience, you still might get in a situation where you must execute a transaction without the help of their UI.
In this article, we will explain how to do so, step by step :)
Data Preparation
In order to execute a transaction through your Gnosis Safe you must first gather and prepare the mandatory data for the action you want to take.
In this tutorial, we will work through an example of a simple ERC20 token transfer with the Safe owned by two wallets, where the authorization threshold is two.
This implies that each transaction made by the Safe needs to be authorized by both of the Safe owners.
In our example two of the following wallets will represent the Safe owners:
#1: 0xc91153c7121732b61dEC1a261CdF46B53D0Fdbb6
#2: 0xfC1a463181aFd4Bc7cC431CC32Fa5a85321FA691
Values to Retrieve
- `to` — address of the contract which we want to interact with
in our case this is an ERC20 token address
- `value` — message value of the transaction
zero, as transfers usually do not require additional base coin spendings
- `data ` — call data to be sent to the contract
describing transfer call with the recipient address and the amount of tokens
- `operation` — type of the call we are willing to perform
zero, as zero represents ‘call’ while one represents ‘delegatecall’
- `signatures ` — owner signatures
structures structure which consists of concatenated signatures sorted in ascending order by the value of the owner address|
- `_nonce` — Safe transaction nonce
retrievable via the ‘nonce()’ getter and necessary in order to generate the hash
Other parameters — baseGas, safeTxGas, gasPrice, gasToken, refundRecipient can be set to zero or zero address for the sake of simplicity of this example as they’re optional.
Generating the Call Data
In order to execute the call we need to provide precise data to the contract describing the action that we’d like to make.
This can be easily accomplished in a multitude of ways, of which the easiest option is finding a calldata encoder online (ex. abi.hashex.org).
Or just by using the following Solidity code snippet 👇
function getCallData() external pure returns (bytes memory) {
return abi.encodeWithSignature(
"transfer(address,uint256)", // Function signature (name + arg types)
0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552, // Receiver address
1000000000000000000000 // One thousand tokens in wei
);
}
Return value of the function call with example data — hex string:
0xa9059cbb000000000000000000000000d9db270c1b5e3bd161e8c8503c55ceabee70955200000000000000000000000000000000000000000000003635c9adc5dea00000
Using the described schema you can create call data for any call you like.
Never forget to properly validate the encoded data in whichever way you prefer, otherwise, you might end up making an unwanted action such as burning tokens.
Retrieving the Data Hash
Data hash is retrieved by filling in the collected data to the ‘view’ function ‘getTransactionHash()’ through a blockchain explorer or other preferred way.
This data is the same data that we will use for the ‘execTransaction()’ call and the interfaces of these two functions are almost the same, except that execution requires the owner signatures and does not take the nonce as an argument.
Remember that ‘_nonce’ is being retrieved via the contract call of the ‘nonce()’ getter.
Return value of the function call — 32 byte hash:
0xd27ede4f2ed9c85105ef5d15a5906268a6c056378d578d40c873e610d0c381b3
Generated hash should then get approved by the owner(s).
Approving the Hash
Once the hash has been retrieved, it should be approved by all of the owners except for the one who is making the call as the owner in the role of ‘msg.sender’ implicitly approves the action.
If the caller is not one of the owners then each one of the owners needs to approve the hash.
In order to approve the hash, the owner should call ‘approveHash’ function, with computed hash as argument:
After calling for approval, make sure each time that transaction is properly executed through the blockchain explorer.
Alternative Solution
Retrieve signatures via wallet extension by provoking a pop-up modal through the browser console.
This can be done in order to avoid hash approvals.
The way to safely generate signatures through the wallet extension which does not have native support for that will be explained in one of the next articles.
Once you have the signatures, they should be concatenated in the ascending order and filled in the ‘signatures’ field of ‘execTransaction()’ the same way that you would do this with ‘contract signatures’.
Computing Signatures Structure
Once hashes are approved we just need to compute a signature structure which explains that we are using ‘contract signatures’ to validate the execution data.
Signatures can be represented either as literal signatures that consist of ‘r’, ’s’, and ‘v’ (but then you do not need to approve the hashes) or ‘contract signatures’ which are structured like this for interaction with the Safe wallet:
#1:
0x000000000000000000000000c91153c7121732b61dec1a261cdf46b53d0fdbb6000000000000000000000000000000000000000000000000000000000000000001
#2:
0x000000000000000000000000fc1a463181afd4bc7cc431cc32fa5a85321fa691000000000000000000000000000000000000000000000000000000000000000001
In the place of ‘r’ there is the address of an owner who either approved the hash or is the ‘execTransaction()’ function caller. At the end, there is the value of ‘01’ as the last byte in the place of ‘v’ which represents the type of verification (‘contract signature’ type). Bytes of parameter ‘s’ are empty.
So we can manually make the signature we need using the following schema:
‘0x’ + 24 zeroes + owner address (without 0x in front) + 64 zeroes + ‘01’
Once signatures are created they should be concatenated in the ascending order by the values of owner addresses.
‘Signatures’ structure (hex string):
0x000000000000000000000000c91153c7121732b61dec1a261cdf46b53d0fdbb6000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000fc1a463181afd4bc7cc431cc32fa5a85321fa691000000000000000000000000000000000000000000000000000000000000000001
Simulation (Extra Step)
Depending on the importance of the action you’re willing to perform and the complexity of the flow, you might want to simulate the call first — to ensure the wanted outcome of your call.
There are multiple parties online offering this as a service (Blocksec Phalcon / Tenderly), and you can also do it yourself manually using an EVM toolkit of your choice (Hardhat / Foundry).
Transaction Execution
Now, since we’ve gathered all of the data and approvals/signatures, we can finally execute the transaction by calling the ‘execTransaction()’ function on our gnosis safe through the explorer with all the data that we’ve gathered.
Here’s an example of how final input to the ‘execTransaction()’ function would look with our example data 👇
to (address) - 0xdAC17F958D2ee523a2206206994597C13D831ec7
value (uint256) - 0
data (bytes) - 0xa9059cbb000000000000000000000000d9db270c1b5e3bd161e8c8503c55ceabee70955200000000000000000000000000000000000000000000003635c9adc5dea00000
operation (uint8) - 0
safeTxGas (uint256) - 0
baseGas (uint256) - 0
gasPrice (uint256) - 0
gasToken (address) - 0x0000000000000000000000000000000000000000
refundReceiver (address) - 0x0000000000000000000000000000000000000000
signatures (bytes) - 0x000000000000000000000000c91153c7121732b61dec1a261cdf46b53d0fdbb6000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000fc1a463181afd4bc7cc431cc32fa5a85321fa691000000000000000000000000000000000000000000000000000000000000000001
Now you’re ready to make an action of your own! Good luck :)
About Diligence
DcentraLab Diligence provides a multitude of services that can help you ensure the safety of your project — security audits, consultations, deployment assists, on-chain forensics, and much more!