Skip to main content
Placing orders from a Safe with multiple signers (multi-sig) requires coordinating signatures from threshold owners before the order can be submitted. This tutorial covers the complete flow using both PreSign and ERC-1271 approaches.

Overview

Safe multi-sig wallets can’t sign messages with a single key. Instead, you need to:
  1. Propose the order and collect the required number of owner signatures
  2. Submit the signed transaction to register the order on-chain (PreSign) or off-chain (ERC-1271)
ApproachHow it worksGas costWhen to use
PreSignExecute a transaction calling setPreSignature on the settlement contract~60k gasSimple, reliable, works with any Safe configuration
ERC-1271Submit an off-chain signature that the settlement contract verifies via isValidSignature on the SafeNo gas (off-chain)Gas-efficient, but requires Safe’s ERC-1271 support
Most multi-sig integrations use PreSign because it’s simpler and doesn’t require handling Safe’s internal signature encoding. Use ERC-1271 when gas costs matter (e.g., frequent trading).

Prerequisites

  • A Safe deployed with 2+ owners and a threshold ≥ 2
  • The sell token approved for the CoW Protocol Vault Relayer from the Safe
  • Safe Protocol Kit installed: npm install @safe-global/protocol-kit
PreSign is the simplest approach — you create the order, then execute a Safe transaction to call setPreSignature on the settlement contract.
1

Create the order with PRESIGN signing scheme

import {
  SupportedChainId,
  OrderKind,
  TradingSdk,
  TradeParameters,
  SwapAdvancedSettings,
  SigningScheme,
} from '@cowprotocol/sdk-trading'
import { ViemAdapter } from '@cowprotocol/sdk-viem-adapter'
import { createPublicClient, http, privateKeyToAccount } from 'viem'
import { mainnet } from 'viem/chains'

const SAFE_ADDRESS = '0xYourSafeAddress'

const adapter = new ViemAdapter({
  provider: createPublicClient({
    chain: mainnet,
    transport: http('YOUR_RPC_URL'),
  }),
  // Use any Safe owner's key — this is for the SDK to create quotes
  signer: privateKeyToAccount('OWNER_1_PRIVATE_KEY' as `0x${string}`),
})

const sdk = new TradingSdk({
  chainId: SupportedChainId.MAINNET,
  appCode: 'my-multisig-app',
}, {}, adapter)

const parameters: TradeParameters = {
  kind: OrderKind.SELL,
  sellToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
  sellTokenDecimals: 6,
  buyToken: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',  // WETH
  buyTokenDecimals: 18,
  amount: '10000000000', // 10,000 USDC
}

const advancedSettings: SwapAdvancedSettings = {
  quoteRequest: {
    from: SAFE_ADDRESS,                  // Quote from the Safe
    signingScheme: SigningScheme.PRESIGN, // Use PreSign for multi-sig
  },
}

const { orderId } = await sdk.postSwapOrder(parameters, advancedSettings)
console.log('Order created (pending PreSign):', orderId)
The order is now in the orderbook with status presignaturePending.
2

Build the PreSign Safe transaction

import Safe from '@safe-global/protocol-kit'
import { encodeFunctionData } from 'viem'

// Settlement contract ABI (just the setPreSignature function)
const SETTLEMENT_ABI = [{
  name: 'setPreSignature',
  type: 'function',
  inputs: [
    { name: 'orderUid', type: 'bytes' },
    { name: 'signed', type: 'bool' },
  ],
  outputs: [],
}]

const SETTLEMENT_CONTRACT = '0x9008D19f58AAbD9eD0D60971565AA8510560ab41'

// Encode the setPreSignature call
const callData = encodeFunctionData({
  abi: SETTLEMENT_ABI,
  functionName: 'setPreSignature',
  args: [orderId, true],
})

// Initialize Safe SDK (owner 1)
const safe = await Safe.init({
  provider: 'YOUR_RPC_URL',
  signer: 'OWNER_1_PRIVATE_KEY',
  safeAddress: SAFE_ADDRESS,
})

// Create the Safe transaction
const safeTx = await safe.createTransaction({
  transactions: [{
    to: SETTLEMENT_CONTRACT,
    value: '0',
    data: callData,
  }],
})

// Owner 1 signs
const signedTx = await safe.signTransaction(safeTx)
console.log('Owner 1 signed. Collecting more signatures...')
3

Collect additional owner signatures

Each additional owner signs the same transaction:
// Owner 2 initializes their Safe SDK instance
const safeOwner2 = await Safe.init({
  provider: 'YOUR_RPC_URL',
  signer: 'OWNER_2_PRIVATE_KEY',
  safeAddress: SAFE_ADDRESS,
})

// Owner 2 signs the same transaction
const fullySignedTx = await safeOwner2.signTransaction(signedTx)
console.log('Owner 2 signed. Threshold reached.')
In practice, Safe owners typically sign via the Safe Transaction Service or Safe UI rather than sharing private keys. The Safe SDK supports both approaches.
4

Execute the transaction

Once enough signatures are collected (≥ threshold), any owner can execute:
const result = await safe.executeTransaction(fullySignedTx)
console.log('PreSign tx hash:', result.hash)
console.log('Order is now Open and ready for execution!')

Using Safe Transaction Service

For production multi-sig flows, use the Safe Transaction Service to propose and collect signatures asynchronously:
import SafeApiKit from '@safe-global/api-kit'

const apiKit = new SafeApiKit({
  chainId: 1n,
})

// Owner 1: Propose the transaction
const safeTxHash = await safe.getTransactionHash(safeTx)
const signature = await safe.signHash(safeTxHash)

await apiKit.proposeTransaction({
  safeAddress: SAFE_ADDRESS,
  safeTransactionData: safeTx.data,
  safeTxHash,
  senderAddress: owner1Address,
  senderSignature: signature.data,
})

console.log('Transaction proposed. Other owners can sign via Safe UI.')

// Owner 2 (later): Confirm the transaction
await apiKit.confirmTransaction(safeTxHash, owner2Signature)

// Any owner: Execute when threshold is met
const pendingTxs = await apiKit.getPendingTransactions(SAFE_ADDRESS)
// Find the transaction and execute it

Method 2: ERC-1271 (Gas-Efficient)

ERC-1271 allows off-chain signing — no gas cost for the signature itself. The settlement contract calls isValidSignature on the Safe to verify.
import {
  SupportedChainId,
  OrderKind,
  TradingSdk,
  SigningScheme,
} from '@cowprotocol/sdk-trading'

const advancedSettings: SwapAdvancedSettings = {
  quoteRequest: {
    from: SAFE_ADDRESS,
    signingScheme: SigningScheme.EIP1271,
  },
}

const { orderId } = await sdk.postSwapOrder(parameters, advancedSettings)
ERC-1271 with Safe multi-sig requires encoding the combined Safe signatures in a specific format. This is more complex than PreSign. See the ERC-1271 Signing Guide for details on signature encoding.

Python SDK

The Python SDK’s swap_tokens function supports Safe multi-sig orders with the safe_address parameter:
from web3 import Account, Web3
from web3.types import Wei
from cowdao_cowpy.cow.swap import swap_tokens
from cowdao_cowpy.common.chains import Chain

account = Account.from_key("OWNER_PRIVATE_KEY")
safe_address = Web3.to_checksum_address("0xYourSafeAddress")

# This creates a PreSign order and returns the order UID
# You still need to execute the setPreSignature transaction from the Safe
completed_order = await swap_tokens(
    amount=Wei(10000 * 10**6),   # 10,000 USDC
    account=account,
    chain=Chain.MAINNET,
    sell_token=USDC_ADDRESS,
    buy_token=WETH_ADDRESS,
    safe_address=safe_address,   # Triggers PreSign signing scheme
)

print(f"Order UID: {completed_order.uid}")
print(f"Now execute setPreSignature from the Safe to activate the order")

Transaction Batching

For efficiency, you can batch the token approval and PreSign into a single Safe transaction using MultiSend:
// Batch: approve + setPreSignature in one Safe tx
const transactions = [
  {
    to: SELL_TOKEN,
    value: '0',
    data: encodeFunctionData({
      abi: ERC20_ABI,
      functionName: 'approve',
      args: [VAULT_RELAYER, sellAmount],
    }),
  },
  {
    to: SETTLEMENT_CONTRACT,
    value: '0',
    data: encodeFunctionData({
      abi: SETTLEMENT_ABI,
      functionName: 'setPreSignature',
      args: [orderId, true],
    }),
  },
]

const safeTx = await safe.createTransaction({ transactions })
// Sign and execute as above...

Order Lifecycle

1. Create order (signingScheme: PRESIGN)
   └── Status: presignaturePending

2. Propose Safe tx (setPreSignature)
   └── Owners sign via Safe UI or API

3. Execute Safe tx (threshold met)
   └── Status: open

4. Solver fills the order
   └── Status: fulfilled

Troubleshooting

IssueCauseFix
Order stays presignaturePendingsetPreSignature tx not executedEnsure the Safe tx has enough signatures and execute it
WrongOwner errorfrom in quote doesn’t match Safe addressSet from: SAFE_ADDRESS in the quote request
Order expires before signingValidity too short for multi-sig coordinationUse a longer validTo (e.g., 7 days)
Insufficient allowanceVault Relayer not approvedApprove from the Safe before or batch with the PreSign tx

Next Steps

Last modified on March 12, 2026