Imagine you and your friends are building an NFT marketplace. You are the CEO and your friend works as a Solidity engineer who writes the smart contract. The NFT marketplace becomes popular, and your revenue builds from the market fee of every NFT sale transaction. You store your profit inside a smart contract, and boast to the media about your company that has enough money to buy a private island. Then, the Solidity engineer disappears and withdraws all the funds from the treasury. You watch in horror.
Now, you vow not to fall into the same trap again. From now on, every sensitive transaction in a smart contract needs approval from a certain number of people. For example, withdrawing funds from your treasury requires at least 60 percent approval from certain key people. If there are five key people, at least three approvals are needed.
Luckily, you don’t need to build this mechanism from scratch; you can use Gnosis Safe to interact with your NFT marketplace. You put the funds inside the Gnosis Safe smart contracts, and withdrawing the funds now requires at least a certain number of signatures. A rogue agent cannot steal the funds anymore, and you’re back to saving up for a private island!
Gnosis Safe is a project from Gnosis. Gnosis started as a prediction markets platform where people can trade information freely. As part of the project, the team behind Gnosis created Gnosis Safe to secure funds for multiple participants. Today, it’s the most popular multisig wallet smart contract on Ethereum. Search “multisig wallet Ethereum” on Google and you will find Gnosis in the top results.
In this article, you will learn how to set up a treasury wallet with Gnosis Safe, so you can protect your funds on the Ethereum blockchain.
You can clone the Gnosis Safe smart contract from their GitHub repo like so:
$ git clone https://github.com/gnosis/safe-contracts/
Use a specific version so you can follow this tutorial:
$ cd safe-contracts $ git checkout v1.3.0-libs.0
With this specific version, the deployed addresses of Gnosis Safe will be deterministic.
Let’s deploy Gnosis Safe to the Hardhat development network. But first, you must install Hardhat inside the safe-contracts
directory:
$ yarn add hardhat
Then, run the Hardhat development network and deploy the Gnosis Safe smart contracts:
$ npx hardhat node Nothing to compile sending eth to create2 contract deployer address (0x3fab184622dc19b6109349b94811493bf2a45362) (tx: 0x076c3e6eb9678931c92e0322885f48ebdc064226483a9bae4866f99c7f8aa8bb)... deploying create2 deployer contract (at 0x4e59b44847b379578588920ca78fbf26c0b4956c) using deterministic deployment (https://github.com/Arachnid/deterministic-deployment-proxy) (tx: 0xeddf9e61fb9d8f5111840daef55e5fde0041f5702856532cdbb5a02998033d26)... deploying "SimulateTxAccessor" (tx: 0xfc6d7c491688840e79ed7d8f0fc73494be305250f0d5f62d04c41bc4467e8603)...: deployed at 0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da with 237871 gas deploying "GnosisSafeProxyFactory" (tx: 0x6fff529768b3c5660234fcd53d5d04918aadc935a90ec05aca1796649bf4f699)...: deployed at 0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2 with 867594 gas deploying "DefaultCallbackHandler" (tx: 0x406498f13d684b2db11ac78d1b06c2b38657f02e729ecebc677b7ea28a30e712)...: deployed at 0x1AC114C2099aFAf5261731655Dc6c306bFcd4Dbd with 542473 gas deploying "CompatibilityFallbackHandler" (tx: 0xe7426790ce3fed5ba2083b5e5b911b561a306d3f26fbd5c0d0d6c0c1d5847e3f)...: deployed at 0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4 with 1238095 gas deploying "CreateCall" (tx: 0xa602d00962fa8de99f84dbefd62f831f179d12e549863bd305607bbb775f5c81)...: deployed at 0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4 with 294718 gas deploying "MultiSend" (tx: 0x8790b4413d0b4336586897f0bf40a72cdcfcb8fd06aed8a164fac5ecf662e0f6)...: deployed at 0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761 with 190004 gas deploying "MultiSendCallOnly" (tx: 0xb4ccc0ce8099412d505d0ab131ce9fffb1915a5053906875fc301528ebe79f1a)...: deployed at 0x40A2aCCbd92BCA938b02010E17A5b8929b49130D with 142122 gas deploying "SignMessageLib" (tx: 0xdf0d113415ea15354de8e816b793ca89e5a9a7d4ad7b48e1344872d0f4aacdbf)...: deployed at 0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2 with 262353 gas deploying "GnosisSafeL2" (tx: 0x83b42dd66a2e282b3e76cb10fb4ab93da970b0454010faef142ab8c6a5c4233d)...: deployed at 0x3E5c63644E683549055b9Be8653de26E0B4CD36E with 5200241 gas deploying "GnosisSafe" (tx: 0xea94214f16af5e66646518db2403a6e24b17973d6bbb0208fc40f01343b0225f)...: deployed at 0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552 with 5017833 gas Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/
The Gnosis Safe smart contracts are not just one smart contract; they are many. But you need to pay attention to three smart contracts in particular: MultiSend
, GnosisSafe
, and GnosisSafeProxyFactory
. You need their addresses when you use the Gnosis Safe SDK later.
So what are these three smart contracts?
GnosisSafe
is the core safe smart contract, and everyone only needs one. Your startup and your business competitors can use the same deployed GnosisSafe
, so you don’t need to deploy it separately if it has been deployed already.
You don’t interact directly with GnosisSafe
. You use a proxy, called GnosisSafeProxy
. Because of this, your startup and your business competitor need to use different GnosisSafeProxy
s. To create your own GnosisSafeProxy
, you can use GnosisSafeProxyFactory
.
MultiSend
is a helper smart contract to batch multiple transactions into one. You may want to buy a yacht as your startup office, pay salary to a meme artist, and pay taxes to the country where your startup resides. Instead of executing these transactions one by one, you can batch them into one with this helper smart contract, then execute them in one go.
There are other smart contracts as part of the Gnosis Safe, but you don’t touch them directly. You only deal with the three smart contracts mentioned previously. That’s why the Gnosis Safe SDK requires you to provide their addresses.
If you use Gnosis Safe in other networks like Ethereum mainnet or Rinkeby, you don’t need to deploy the Gnosis Safe smart contracts because the team behind Gnosis Safe already deployed these core ones. You just need to find their addresses and write them down. But since you are using the Hardhat development network, you need to do this step.
Let the process run peacefully. You can open a new terminal and create your project that interacts with these smart contracts in the new terminal.
The Gnosis Safe SDK libraries are Node.js libraries. To use them, you need to create a Node project. Let’s create one by creating an empty directory and initialize it with yarn:
$ mkdir our-treasury $ cd our-treasury $ yarn init -y yarn init v1.22.11 warning The yes flag has been set. This will automatically answer yes to all questions, which may have security implications. success Saved package.json Done in 0.02s. $ ls package.json
To interact with Ethereum in Node, you have two choices: web3.js
and ethers.js
. In this tutorial, you’ll use the ethers.js
library. Install the library with yarn like so:
$ yarn add ethers
You will put the Ethereum addresses on the .env
file instead of hard-coding them, so you need the dotenv
library:
$ yarn add dotenv
Lastly, you need the Gnosis Safe SDK libraries:
$ yarn add @gnosis.pm/safe-core-sdk @gnosis.pm/safe-ethers-lib
These are the core libraries that you’re going to learn how to use in this tutorial to interface with the Gnosis Safe smart contracts.
Instead of hard-coding the Ethereum addresses, you can store them as environment variables. But setting up environment variables in a terminal before executing a script is a hassle:
$ export ACCOUNT_1=0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 $ export ACCOUNT_2=0x70997970c51812dc3a010c7d01b50e0d17dc79c8 ... $ node index.js
It’s better if you use the .env
file. Basically, you use the dotenv
library to load the environment variables from the .env
file. That way, you only need to set up the environment variables once.
Create the .env
file with the following content:
ACCOUNT_1="0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" ACCOUNT_2="0x70997970c51812dc3a010c7d01b50e0d17dc79c8" ACCOUNT_3="0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc" ACCOUNT_4="0x90f79bf6eb2c4f870365e785982e1f101e93b906" ACCOUNT_5="0x15d34aaf54267db7d7c367839aaf71a00a2c6a65" ACCOUNT_6="0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc" ACCOUNT_7="0x976ea74026e726554db657fa54763abd0c3a0aa9" MULTI_SEND_ADDRESS="0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761" SAFE_MASTER_COPY_ADDRESS="0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552" SAFE_PROXY_FACTORY_ADDRESS="0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2"
In the code above, you can see that the addresses for MULTI_SEND_ADDRESS
, SAFE_MASTER_COPY_ADDRESS
, SAFE_PROXY_FACTORY_ADDRESS
are the same as the ones in the first terminal. As a reminder, the first terminal is the terminal where you’ve deployed the Gnosis Safe smart contracts on the Hardhat development network. Later, in your client code, you will load these smart contracts’ addresses from the .env
file to interact with the deployed smart contracts on the Hardhat development network.
The ACCOUNT_1
and other accounts are the sample Ethereum addresses provided by the Hardhat development node. If you run npx hardhat node
in a new Hardhat project, you will get 20 sample Ethereum addresses with their private keys for development. Each account has 10,000ETH. In this .env
file, you just took the first seven addresses.
Let’s create the index.js
file. This is where you will write the code to build the treasury with Gnosis Safe:
$ edit index.js # replace edit with vim or code or your favorite editor
First things first, you want to be able to read the variables you put in the .env
file. So add this line:
require('dotenv').config();
Then, you import the Gnosis Safe SDK library:
const { SafeFactory, SafeAccountConfig, ContractNetworksConfig } = require('@gnosis.pm/safe-core-sdk'); const Safe = require('@gnosis.pm/safe-core-sdk')["default"];
To make things clearer, imagine that you’re creating a web3 startup to disrupt traditional banks. There are five people who are building this startup, with you acting as the CEO. The other people on the team are the CTO, a Solidity engineer, a meme artist, and an advisor.
An angel investor sends money to your startup. The money is put inside the safe smart contract. It takes three of five signatures from your team to approve any transactions related to this smart contract. You, as the CEO, decide to buy a yacht for your startup office. You will need two other signatures from your team to approve this transaction. Let’s do it!
To protect your company’s treasury from being emptied by a single member, you have to make sure that three out of your five team members approve of the yacht purchase. Let’s add this functionality now.
Still in the same file, index.js
, add these lines below the const Safe = require…
line:
const ceo = process.env.ACCOUNT_1; const cto = process.env.ACCOUNT_2; const meme_artist = process.env.ACCOUNT_3; const solidity_engineer = process.env.ACCOUNT_4; const advisor = process.env.ACCOUNT_5; const investor = process.env.ACCOUNT_6; const yacht_shop = process.env.ACCOUNT_7;
Here, you load up the addresses you stored on the .env
file in index.js
. This way, to change the addresses, you don’t need to change the code.
Then, set up a provider from the ethers.js
library:
const { ethers } = require('ethers'); const provider = new ethers.providers.JsonRpcProvider();
A provider is an Ethereum connection object. Here, you create an abstraction of a JSON-RPC connection to an Ethereum node.
From this provider, you create three signers from three addresses. Remember, you only need three signatures to approve a transaction in the safe:
const ceo_signer = provider.getSigner(ceo); const cto_signer = provider.getSigner(cto); const advisor_signer = provider.getSigner(advisor);
The Gnosis Safe smart contracts work with the ethers.js
library and the web3.js
library. In this tutorial, you are using ethers.js
. So you need the adapter that works with ethers.js
:
const EthersAdapter = require('@gnosis.pm/safe-ethers-lib')["default"]; const ethAdapter_ceo = new EthersAdapter({ ethers, signer: ceo_signer }); const ethAdapter_cto = new EthersAdapter({ ethers, signer: cto_signer }); const ethAdapter_advisor = new EthersAdapter({ ethers, signer: advisor_signer });
In the code above, you interacted with the Gnosis Safe SDK using these adapters. Note that each address needs its own adapter.
Next, create the main
, asynchronous function. You will use this pattern to handle async/await
and handle errors in the code properly:
async function main() { // FROM NOW ON, YOUR CODE IS PUT HERE } main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); });
As explained in the beginning of the tutorial, the only way to create a safe is from the safe factory that is shared with everyone. So first, you need to create a safe factory object connecting to the safe factory smart contract, GnosisSafeProxyFactory
:
const id = await ethAdapter_ceo.getChainId(); const contractNetworks = { [id]: { multiSendAddress: process.env.MULTI_SEND_ADDRESS, safeMasterCopyAddress: process.env.SAFE_MASTER_COPY_ADDRESS, safeProxyFactoryAddress: process.env.SAFE_PROXY_FACTORY_ADDRESS } } const safeFactory = await SafeFactory.create({ ethAdapter: ethAdapter_ceo, contractNetworks: contractNetworks });
In the code above, you first received the chain ID. Then, you created an object containing three smart contracts with which you safe will interact. Finally, you created a safe factory.
Next, create a safe from this safe factory like so:
const owners = [ceo, cto, meme_artist, solidity_engineer, advisor]; const threshold = 3; const safeAccountConfig = { owners: owners, threshold: threshold}; const safeSdk_ceo = await safeFactory.deploySafe({safeAccountConfig});
The safe needs the addresses of the members and the minimum amount of signatures required to approve transactions for this safe. In the code above, you put all members of the startup and 3
as the threshold. To deploy a safe, you can use the deploySafe
method from the safe factory.
Now that you have a safe already, an investor sends money to your startup, which would look like this:
const treasury = safeSdk_ceo.getAddress(); const ten_ethers = ethers.utils.parseUnits("10", 'ether').toHexString(); const params = [{ from: investor, to: treasury, value: ten_ethers }]; await provider.send("eth_sendTransaction", params); console.log("Fundraising.");
The safe holds your treasury. The investor sent 10ETH using the eth_sendTransaction
RPC method.
To make sure it works, you can check the balance of the treasury with the following:
const balance = await safeSdk_ceo.getBalance(); console.log(`Initial balance of the treasury: ${ethers.utils.formatUnits(balance, "ether")} ETH`);
Once the investor’s money is in, you can move fast. Create a transaction to buy a yacht like so:
const three_ethers = ethers.utils.parseUnits("3", 'ether').toHexString(); const transaction = { to: yacht_shop, data: '0x', value: three_ethers }; const safeTransaction = await safeSdk_ceo.createTransaction(transaction); const hash = await safeSdk_ceo.getTransactionHash(safeTransaction);
The transaction is sending 3ETH to the yacht shop. Since the transaction is transferring ETH, you can fill empty data, 0x
, in the data field of the transaction. However, if you create a smart contract transaction, such as minting NFTs or selling tokens, you will need to fill the data field.
After doing that, create the safe transaction using the createTransaction
method. Then, get the hash with the getTransactionHash
method.
Finally, your job as CEO is to approve the transaction:
const txResponse = await safeSdk_ceo.approveTransactionHash(hash); await txResponse.transactionResponse?.wait();
But your job is not done yet. You call your co-founder, the CTO of your startup, and persuade her to approve the transaction of buying a yacht. “Wouldn’t it be nice if you could code in the vast ocean?”
Your CTO agrees to approve the transaction:
const safeSdk_cto = await Safe.create({ ethAdapter: ethAdapter_cto, safeAddress: treasury, contractNetworks: contractNetworks }); const safeTransactionCTO = await safeSdk_cto.createTransaction(transaction); const hashCTO = await safeSdk_ceo.getTransactionHash(safeTransaction); const txResponse_cto = await safeSdk_cto.approveTransactionHash(hashCTO); await txResponse_cto.transactionResponse?.wait();
The CTO needs a different Safe object. But you don’t need to create it with the safe factory; you can create it with the create
method of the Safe object. Because your safe smart contract is live already on the blockchain, you just passed the treasury address when you created the Safe object.
Next, you pass the transaction to your CTO either by chatting or via email. What the transaction means in this context is the transaction
object in the code. Remember, you’ve already created this object:
const transaction = { to: yacht_shop, data: '0x', value: three_ethers };
Your CTO created a safe transaction from this one and got its hash. Then, she approves the transaction using the approveTransactionHash
method, which accepts the hash argument.
Your job is still not done yet; you need another signature. But this time, you don’t need to convince your advisor because he gives you full support to buy a yacht. He approves the transaction:
const safeSdk_advisor = await Safe.create({ ethAdapter: ethAdapter_advisor, safeAddress: treasury, contractNetworks: contractNetworks }); const safeTransactionAdvisor = await safeSdk_advisor.createTransaction(transaction); const txResponse_advisor = await safeSdk_advisor.executeTransaction(safeTransactionAdvisor); await txResponse_advisor.transactionResponse?.wait(); console.log("Buying a yacht.");
The code is the same as the CTO’s approval transaction, but instead of the approveTransactionHash
method, the advisor used the executeTransaction
method. This method approves the transaction as well behind the scenes. But most importantly, this method executes the safe transaction, which is buying a yacht!
Finally, let’s check your treasury balance:
const afterBalance = await safeSdk_ceo.getBalance(); console.log(`The final balance of the treasury: ${ethers.utils.formatUnits(afterBalance, "ether")} ETH`);
The script is finished. You can execute the script like so:
$ node index.js Fundraising. Initial balance of the treasury: 10.0 ETH Buying a yacht. The final balance of the treasury: 7.0 ETH
Now, you can work in a yacht with your team building a DAO to disrupt banks!
In this article, you learned how to create a Gnosis Safe that can be configured to require multiple signatures to approve transactions. You launched the Gnosis Safe smart contracts in the Hardhat development network, then, using the Gnosis safe SDK, created a safe to hold the treasury. Using multiple addresses, you created and approved the transaction of sending ETH.
This article only explains the SDK of interacting with the Gnosis Safe smart contracts. If you want to learn the ins and outs of the smart contract themselves, you can check their GitHub repository! The SDK also has other methods like signing a transaction off-chain. Check their GitHub repository to learn more. The code for this article is available on this GitHub repository.
LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app or site. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.
Modernize how you debug web and mobile apps — Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.