Smart Contract Wallets
Smart contract wallets (like Safe/Gnosis Safe) require a different signing method than regular EOA (Externally Owned Account) wallets. The SDK supports smart contract wallets using the PRESIGN signing scheme.
Overview
Smart contract wallets cannot sign EIP-712 messages directly. Instead, they use on-chain signatures through the PRESIGN method:
- Create an order with
SigningScheme.PRESIGN
- Get a pre-sign transaction from the SDK
- Execute the transaction to “sign” the order on-chain
PRESIGN is more gas-efficient for smart contract wallets compared to EIP-1271 signatures.
The PRESIGN Signing Scheme
When you use SigningScheme.PRESIGN, the SDK creates an order in a “pre-sign” state. The order is only valid after you execute an on-chain transaction that registers your approval.
How it Works
- Create order with PRESIGN scheme - The order is created in the order book with a special “pre-sign” status
- Get pre-sign transaction - Use
getPreSignTransaction to get the transaction data
- Execute transaction - Send the transaction from your smart contract wallet
- Order becomes valid - Once the transaction is confirmed, the order is ready for execution
Creating Swap Orders with Smart Contract Wallets
Example
import {
SupportedChainId,
OrderKind,
TradeParameters,
SwapAdvancedSettings,
SigningScheme,
TradingSdk,
} from '@cowprotocol/sdk-trading'
import { ViemAdapter } from '@cowprotocol/sdk-viem-adapter'
import { createPublicClient, http, privateKeyToAccount } from 'viem'
import { sepolia } from 'viem/chains'
const adapter = new ViemAdapter({
provider: createPublicClient({
chain: sepolia,
transport: http('YOUR_RPC_URL')
}),
signer: privateKeyToAccount('YOUR_PRIVATE_KEY' as `0x${string}`)
})
const sdk = new TradingSdk({
chainId: SupportedChainId.SEPOLIA,
appCode: 'YOUR_APP_CODE',
}, {}, adapter)
const smartContractWalletAddress = '0x1234567890123456789012345678901234567890'
const parameters: TradeParameters = {
kind: OrderKind.BUY,
sellToken: '0xfff9976782d46cc05630d1f6ebab18b2324d6b14',
sellTokenDecimals: 18,
buyToken: '0x0625afb445c3b6b7b929342a04a22599fd5dbb59',
buyTokenDecimals: 18,
amount: '120000000000000000',
}
const advancedParameters: SwapAdvancedSettings = {
quoteRequest: {
// Specify the PRESIGN signing scheme
signingScheme: SigningScheme.PRESIGN,
},
}
// Step 1: Create the order
const { orderId } = await sdk.postSwapOrder(parameters, advancedParameters)
console.log('Order created with "pre-sign" state, id: ', orderId)
// Step 2: Get the pre-sign transaction
const preSignTransaction = await sdk.getPreSignTransaction({
orderId,
account: smartContractWalletAddress
})
console.log('Execute this transaction to sign the order:', preSignTransaction)
// Step 3: Execute the transaction from your smart contract wallet
// (implementation depends on your wallet SDK)
Creating Limit Orders with Smart Contract Wallets
Example
import {
SupportedChainId,
OrderKind,
LimitTradeParameters,
LimitOrderAdvancedSettings,
SigningScheme,
TradingSdk,
} from '@cowprotocol/sdk-trading'
import { ViemAdapter } from '@cowprotocol/sdk-viem-adapter'
import { createPublicClient, http, privateKeyToAccount } from 'viem'
import { sepolia } from 'viem/chains'
const adapter = new ViemAdapter({
provider: createPublicClient({
chain: sepolia,
transport: http('YOUR_RPC_URL')
}),
signer: privateKeyToAccount('YOUR_PRIVATE_KEY' as `0x${string}`)
})
const sdk = new TradingSdk({
chainId: SupportedChainId.SEPOLIA,
appCode: 'YOUR_APP_CODE',
}, {}, adapter)
const smartContractWalletAddress = '0x1234567890123456789012345678901234567890'
const limitOrderParameters: LimitTradeParameters = {
kind: OrderKind.BUY,
sellToken: '0xfff9976782d46cc05630d1f6ebab18b2324d6b14',
sellTokenDecimals: 18,
buyToken: '0x0625afb445c3b6b7b929342a04a22599fd5dbb59',
buyTokenDecimals: 18,
sellAmount: '120000000000000000',
buyAmount: '66600000000000000000',
owner: smartContractWalletAddress, // Important: specify the owner
}
const advancedParameters: LimitOrderAdvancedSettings = {
additionalParams: {
signingScheme: SigningScheme.PRESIGN,
},
}
const { orderId } = await sdk.postLimitOrder(
limitOrderParameters,
advancedParameters
)
const preSignTransaction = await sdk.getPreSignTransaction({
orderId,
account: smartContractWalletAddress
})
console.log('Order created with "pre-sign" state, id: ', orderId)
console.log('Execute the transaction to sign the order', preSignTransaction)
When creating limit orders with smart contract wallets, you must specify the owner parameter if it differs from the signer. CoW Protocol uses the owner to check balance, allowance, and other permissions.
Getting Pre-Sign Quotes
For more accurate gas estimation, specify the signing scheme when getting quotes:
import {
SupportedChainId,
OrderKind,
TradeParameters,
SwapAdvancedSettings,
SigningScheme,
TradingSdk,
} from '@cowprotocol/sdk-trading'
const parameters: TradeParameters = {
kind: OrderKind.BUY,
sellToken: '0xfff9976782d46cc05630d1f6ebab18b2324d6b14',
sellTokenDecimals: 18,
buyToken: '0x0625afb445c3b6b7b929342a04a22599fd5dbb59',
buyTokenDecimals: 18,
amount: '120000000000000000',
}
const advancedParameters: SwapAdvancedSettings = {
quoteRequest: {
// Specify the signing scheme for accurate quotes
signingScheme: SigningScheme.PRESIGN,
},
}
const { quoteResults } = await sdk.getQuote(parameters, advancedParameters)
console.log('Quote:', quoteResults)
The getPreSignTransaction Method
The getPreSignTransaction method returns transaction parameters ready to be executed.
Parameters
orderId - The unique identifier of the order to sign
account - The smart contract wallet address
Returns
A TransactionParams object with:
to - The contract address (Settlement contract)
data - The encoded function call
gasLimit - Estimated gas limit
value - ETH value to send (usually ‘0’)
Implementation Details
The method calls setPreSignature(orderId, true) on the CoW Protocol Settlement contract:
// From the SDK source code (getPreSignTransaction.ts)
export async function getPreSignTransaction(
signer: Signer,
chainId: SupportedChainId,
orderId: string,
): Promise<TransactionParams> {
const contract = getSettlementContract(chainId, signer)
const preSignatureCall = contract.interface.encodeFunctionData(
'setPreSignature',
[orderId, true]
)
const gas = await contract.estimateGas
.setPreSignature?.(orderId, true)
.catch(() => GAS_LIMIT_DEFAULT)
return {
data: preSignatureCall,
gasLimit: '0x' + calculateGasMargin(gas).toString(16),
to: contract.address,
value: '0',
}
}
Executing Pre-Sign Transactions
With Safe SDK
import Safe from '@safe-global/protocol-kit'
const safe = await Safe.create({
safeAddress: smartContractWalletAddress,
ethAdapter,
})
const safeTransaction = await safe.createTransaction({
transactions: [preSignTransaction]
})
const txResponse = await safe.executeTransaction(safeTransaction)
const receipt = await txResponse.transactionResponse?.wait()
console.log('Order signed on-chain:', receipt.hash)
With Viem Wallet Client
import { createWalletClient, http } from 'viem'
import { mainnet } from 'viem/chains'
const walletClient = createWalletClient({
chain: mainnet,
transport: http(),
})
const hash = await walletClient.sendTransaction({
to: preSignTransaction.to,
data: preSignTransaction.data,
gas: BigInt(preSignTransaction.gasLimit),
value: BigInt(preSignTransaction.value),
})
console.log('Transaction hash:', hash)
Gas Considerations
PRESIGN orders require an on-chain transaction, which costs gas:
- Gas cost: ~50,000-100,000 gas depending on the wallet implementation
- When to use: Best for larger orders where gas cost is negligible compared to order value
- Alternative: For small orders, consider using an EOA wallet instead
The pre-sign transaction gas cost is separate from the order execution cost. The execution cost is covered by the protocol’s fee mechanism.
Common Issues and Solutions
Order Not Found
Problem: The order hasn’t been created yet or the order ID is incorrect.
Solution: Ensure you wait for postSwapOrder or postLimitOrder to complete before calling getPreSignTransaction.
Insufficient Allowance
Problem: The smart contract wallet hasn’t approved tokens.
Solution: Approve tokens before creating orders (see Token Approvals guide).
Transaction Reverts
Problem: The pre-sign transaction reverts on-chain.
Solution:
- Verify the order ID is correct
- Ensure the wallet has enough ETH for gas
- Check that you’re using the correct chain ID
Best Practices
- Always specify the owner: For limit orders, explicitly set the
owner parameter
- Get quotes with PRESIGN: Include the signing scheme in quote requests for accurate estimates
- Wait for confirmation: Ensure the pre-sign transaction is confirmed before expecting order execution
- Handle transaction errors: Implement proper error handling for the on-chain transaction
- Check order status: Verify the order status after signing to ensure it’s ready
Next Steps