Gnosis Safe: Multi-Sig Transactions
Gnosis Safe (now "Safe") is the most widely used multi-signature wallet on Ethereum. Nethereum's Nethereum.GnosisSafe package provides three levels of integration: SafeAccount (transparent — any contract service auto-routes through Safe), ChangeContractHandlerToSafeExecTransaction (per-service switching), and manual transaction building with EIP-712 signatures.
NuGet: Nethereum.GnosisSafe
dotnet add package Nethereum.GnosisSafe
SafeAccount (Recommended)
The simplest approach — create a SafeAccount and every contract service created from that web3 instance automatically executes through the Safe:
var safeAccount = new SafeAccount(safeAddress, chainId, privateKey);
var web3 = new Web3(safeAccount, rpcUrl);
// This ERC-20 transfer executes through the Safe
var erc20 = new ERC20ContractService(web3.Eth, tokenAddress);
var receipt = await erc20.TransferRequestAndWaitForReceiptAsync(to, amount);
SafeAccount implements IContractServiceConfigurableAccount, which swaps the contract handler to SafeExecTransactionContractHandler. The call data gets packed into a Safe execTransaction, signed with EIP-712, and submitted.
Per-Service Handler Swap
Switch a specific contract service to Safe execution without changing the entire web3:
var erc20 = new ERC20ContractService(web3.Eth, contractAddress);
erc20.ChangeContractHandlerToSafeExecTransaction(safeAddress, privateKey);
// Only this service goes through Safe; others execute directly
This is useful when some operations should go through the Safe and others shouldn't.
Manual Transaction Building
For full control or multi-owner signature collection:
var safeService = new GnosisSafeService(web3, safeAddress);
var transactionData = new EncodeTransactionDataFunction
{
To = targetAddress,
Value = 0,
Data = encodedCallData,
Operation = 0, // 0 = Call, 1 = DelegateCall
SafeTxGas = 0,
BaseGas = 0,
SafeGasPrice = 0,
GasToken = AddressUtil.ZERO_ADDRESS,
RefundReceiver = AddressUtil.ZERO_ADDRESS
};
var execTx = await safeService.BuildTransactionAsync(
transactionData, chainId, false, privateKey1, privateKey2);
var receipt = await safeService.ExecTransactionRequestAndWaitForReceiptAsync(execTx);
BuildTransactionAsync fetches the nonce, hashes the data with EIP-712, signs with each key, orders signatures by signer address (required by Safe), and returns a ready-to-submit function.
Off-Chain Signature Collection
When owners can't sign in the same session, compute the hash and collect signatures separately:
// Compute hash
transactionData.SafeNonce = await safeService.NonceQueryAsync();
var safeHashes = GnosisSafeService.GetSafeHashes(transactionData, chainId, safeAddress);
// Each owner signs independently
var signature = await safeService.SignEncodedTransactionDataAsync(
transactionData, chainId, convertToSafeVFormat: true);
Safe uses a custom V format (V+4 from standard Ethereum). The convertToSafeVFormat: true parameter handles this automatically.
Combine and Execute
var signatures = new List<SafeSignature>
{
new SafeSignature { Address = owner1Address, Signature = signature1 },
new SafeSignature { Address = owner2Address, Signature = signature2 }
};
var combinedSignatures = safeService.GetCombinedSignaturesInOrder(signatures);
Safe requires signatures ordered by signer address (ascending). GetCombinedSignaturesInOrder handles sorting and concatenation.
MultiSend
Batch multiple actions in a single Safe transaction using typed MultiSendFunctionInput<T>:
var input1 = new MultiSendFunctionInput<TransferFunction>(
new TransferFunction { To = recipient1, Value = amount1 }, tokenAddress);
var input2 = new MultiSendFunctionInput<TransferFunction>(
new TransferFunction { To = recipient2, Value = amount2 }, tokenAddress);
var execTx = await safeService.BuildMultiSendTransactionAsync(
transactionData, chainId, privateKey, false, input1, input2);
var receipt = await safeService.ExecTransactionRequestAndWaitForReceiptAsync(execTx);
MultiSendFunctionInput<T> implements IMultiSendInput and encodes call data automatically. BuildMultiSendTransactionAsync sets Operation to DelegateCall (required by MultiSend).
Query Safe Configuration
var owners = await safeService.GetOwnersQueryAsync();
var threshold = await safeService.GetThresholdQueryAsync();
var nonce = await safeService.NonceQueryAsync();
var version = await safeService.VersionQueryAsync();
var isOwner = await safeService.IsOwnerQueryAsync(address);
Console.WriteLine($"Safe v{version}: {owners.Count} owners, threshold {threshold}, nonce {nonce}");
Gas Parameters
| Parameter | Purpose |
|---|---|
SafeTxGas |
Gas for the internal Safe transaction. Set 0 to let the Safe estimate. |
BaseGas |
Gas overhead independent of execution. Usually 0. |
SafeGasPrice |
Gas price for refund calculation. Set 0 for no refund. |
GasToken |
Token to pay gas refunds in. address(0) = ETH. |
RefundReceiver |
Address receiving the gas refund. address(0) = tx.origin. |
For most use cases, setting all gas parameters to 0 is correct — the Safe uses Ethereum's standard gas mechanism.
When to Use Which Approach
| Approach | When |
|---|---|
SafeAccount |
All contract calls should go through Safe — simplest |
ChangeContractHandlerToSafeExecTransaction |
Only specific services should use Safe |
Manual BuildTransactionAsync |
Need multi-owner signature collection or custom gas params |
BuildMultiSendTransactionAsync |
Batch multiple actions in one Safe tx |
For full documentation, see: https://docs.nethereum.com/docs/defi/guide-gnosis-safe