Execute multi-signature transactions through Gnosis Safe (Safe) using Nethereum (.NET/C#). Use this skill whenever the user asks about multi-sig wallets, Gnosis Safe, Safe transactions, SafeAccount, multi-signature signing, MultiSend, or executing contract calls through a Safe with C# or .NET.
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
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.
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.
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.
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.
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.
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).
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}");
| 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.
| 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