Overview
Merkle root orders allow you to authorize multiple conditional orders with a single on-chain transaction by setting a merkle root. This is significantly more gas-efficient than creating orders individually when managing many conditional orders.
Instead of calling create() for each order (N transactions), you set one merkle root that authorizes all orders simultaneously (1 transaction).
How It Works
Collect Orders
Gather all conditional orders as ConditionalOrderParams structs
Build Merkle Tree
Create merkle tree where each leaf is a double hash: keccak256(bytes.concat(keccak256(abi.encode(params))))
Set Root On-Chain
Call ComposableCoW.setRoot() with the computed merkle root
Verify with Proofs
Each order is validated by providing its merkle proof along with the order parameters
Creating a Merkle Tree
Step 1: Prepare Order Leaves
Create multiple conditional order parameters:
import { IConditionalOrder } from "composable-cow/interfaces/IConditionalOrder.sol" ;
import { TWAP , TWAPOrder } from "composable-cow/types/twap/TWAP.sol" ;
// Create multiple TWAP orders
IConditionalOrder.ConditionalOrderParams[] memory leaves =
new IConditionalOrder.ConditionalOrderParams[]( 10 );
for ( uint256 i = 0 ; i < 10 ; i ++ ) {
TWAPOrder.Data memory twapData = TWAPOrder. Data ({
sellToken : token0,
buyToken : token1,
receiver : address ( 0 ),
partSellAmount : 1e18 ,
minPartLimit : 1e17 ,
t0 : block .timestamp,
n : 5 ,
t : 3600 , // 1 hour intervals
span : 0 ,
appData : keccak256 ( abi . encode ( "twap" , i))
});
leaves[i] = IConditionalOrder. ConditionalOrderParams ({
handler : IConditionalOrder (TWAP_ADDRESS),
salt : keccak256 ( abi . encode (i)),
staticInput : abi . encode (twapData)
});
}
Step 2: Calculate Leaf Hashes
Each leaf must be double-hashed:
// Double hash function used by ComposableCoW
function hashLeaf (
IConditionalOrder . ConditionalOrderParams memory params
) internal pure returns ( bytes32 ) {
return keccak256 ( bytes . concat ( keccak256 ( abi . encode (params))));
}
// Calculate all leaf hashes
bytes32 [] memory leafHashes = new bytes32 []( leaves . length );
for ( uint256 i = 0 ; i < leaves.length; i ++ ) {
leafHashes[i] = hashLeaf (leaves[i]);
}
ComposableCoW uses double hashing to prevent second preimage attacks. Always use keccak256(bytes.concat(keccak256(abi.encode(params)))) for leaf hashes.
Step 3: Build the Merkle Tree
You can use a library like Murky or OpenZeppelin:
import { Merkle } from "murky/Merkle.sol" ;
Merkle merkle = new Merkle ();
// Sort the hashes (required for deterministic proofs)
bytes32 [] memory sortedHashes = sortHashes (leafHashes);
// Calculate the merkle root
bytes32 root = merkle. getRoot (sortedHashes);
Step 4: Set the Root
import { ComposableCoW } from "composable-cow/ComposableCoW.sol" ;
ComposableCoW composableCow = ComposableCoW ( 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 );
// Proof struct for event emission
ComposableCoW.Proof memory proof = ComposableCoW. Proof ({
location : 0 , // 0 = no proofs emitted, 1 = proofs in data
data : bytes ( "" ) // Empty for location 0
});
// Set the root from your Safe
safe. execute (
address (composableCow),
0 ,
abi . encodeWithSelector (
ComposableCoW.setRoot.selector,
root,
proof
),
Enum.Operation.Call,
signers
);
Proof Location Parameter
The location field in the Proof struct controls proof emission:
Location Behavior 0No proofs emitted in events (default) 1proof.data contains ABI-encoded array of proofs that watchtowers should index
Emitting Proofs for Watchtowers
// Generate all proofs for watchtowers
bytes32 [][] memory allProofs = new bytes32 [][](leaves.length);
for ( uint256 i = 0 ; i < leaves.length; i ++ ) {
allProofs[i] = merkle. getProof (sortedHashes, i);
}
// Emit proofs in the event
ComposableCoW.Proof memory proofWithData = ComposableCoW. Proof ({
location : 1 ,
data : abi . encode (allProofs)
});
composableCow. setRoot (root, proofWithData);
Setting Root with Context
Store additional on-chain data (like start time) with the merkle root:
import { IValueFactory } from "composable-cow/interfaces/IValueFactory.sol" ;
import { CurrentBlockTimestampFactory } from "composable-cow/value_factories/CurrentBlockTimestampFactory.sol" ;
// Use timestamp factory to store current time
IValueFactory timestampFactory = IValueFactory ( 0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc );
composableCow. setRootWithContext (
root,
proof,
timestampFactory,
abi . encode ( block .timestamp)
);
// The timestamp is stored at cabinet[owner][bytes32(0)]
// TWAP orders with t0=0 will read this value automatically
For merkle root orders, context is typically stored at bytes32(0) in the cabinet. This is useful for TWAP orders where t0 = 0 means “read start time from context.”
Verifying Orders with Proofs
Generate Merkle Proof
For a specific order in the tree:
// Get the proof for the 3rd order (index 2)
bytes32 [] memory proof = merkle. getProof (sortedHashes, 2 );
// Get the corresponding order params
IConditionalOrder.ConditionalOrderParams memory params = leaves[ 2 ];
Retrieve Tradeable Order
Include the proof when calling getTradeableOrderWithSignature:
(
GPv2Order.Data memory order,
bytes memory signature
) = composableCow. getTradeableOrderWithSignature (
address (safe),
params,
bytes ( "" ), // offchainInput
proof // merkle proof (NOT empty!)
);
For merkle root orders, you MUST provide the merkle proof. An empty proof array new bytes32[](0) indicates a single order, not a merkle order.
Verification Logic
ComposableCoW verifies merkle proofs in the _auth function:
function _auth (
address owner ,
IConditionalOrder . ConditionalOrderParams memory params ,
bytes32 [] memory proof
) internal view returns ( bytes32 ctx ) {
if (proof.length != 0 ) {
// Merkle root verification
bytes32 leaf = keccak256 ( bytes . concat ( hash (params)));
if ( ! MerkleProof. verify (proof, roots[owner], leaf)) {
revert ProofNotAuthed ();
}
// Context is bytes32(0) for merkle orders
return bytes32 ( 0 );
} else {
// Single order verification
ctx = hash (params);
if ( ! singleOrders[owner][ctx]) {
revert SingleOrderNotAuthed ();
}
}
}
For merkle root orders, the context (ctx) is always bytes32(0). For single orders, it’s H(params).
Updating Merkle Root Orders
Adding Orders
To add new orders:
Add new leaves to your tree
Recalculate the merkle root
Call setRoot() with the new root
// Add 5 more orders to existing 10
IConditionalOrder.ConditionalOrderParams[] memory newLeaves =
new IConditionalOrder.ConditionalOrderParams[]( 15 );
// Copy existing + add new
for ( uint i = 0 ; i < 10 ; i ++ ) {
newLeaves[i] = existingLeaves[i];
}
for ( uint i = 10 ; i < 15 ; i ++ ) {
newLeaves[i] = createNewOrder (i);
}
// Rebuild tree and set new root
bytes32 newRoot = calculateRoot (newLeaves);
composableCow. setRoot (newRoot, proof);
Removing Orders
To remove orders:
Remove leaves from your tree
Recalculate the merkle root
Call setRoot() with the new root
// Remove 3rd order by excluding it
IConditionalOrder.ConditionalOrderParams[] memory prunedLeaves =
new IConditionalOrder.ConditionalOrderParams[]( 9 );
uint j = 0 ;
for ( uint i = 0 ; i < 10 ; i ++ ) {
if (i != 2 ) { // Skip index 2
prunedLeaves[j] = existingLeaves[i];
j ++ ;
}
}
// Rebuild tree and set new root
bytes32 newRoot = calculateRoot (prunedLeaves);
composableCow. setRoot (newRoot, proof);
Changing the merkle root invalidates all previous proofs. Orders removed from the tree can no longer be validated, even if someone has an old proof.
Clearing All Orders
Set the root to bytes32(0) to invalidate all orders:
composableCow. setRoot (
bytes32 ( 0 ),
ComposableCoW. Proof ({location : 0 , data : bytes ( "" )})
);
Complete Example
Here’s a full example from the test suite:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 < 0.9 . 0 ;
import { Merkle } from "murky/Merkle.sol" ;
import { Safe } from "safe/Safe.sol" ;
import { Enum } from "safe/common/Enum.sol" ;
import { IConditionalOrder , ComposableCoW } from "composable-cow/ComposableCoW.sol" ;
import { TWAP , TWAPOrder } from "composable-cow/types/twap/TWAP.sol" ;
contract MerkleRootExample is Merkle {
ComposableCoW composableCow;
Safe safe;
TWAP twap;
function createMerkleRoot () external {
// 1. Create conditional orders
IConditionalOrder.ConditionalOrderParams[] memory leaves =
new IConditionalOrder.ConditionalOrderParams[]( 50 );
for ( uint256 i = 0 ; i < 50 ; i ++ ) {
TWAPOrder.Data memory twapData = TWAPOrder. Data ({
sellToken : IERC20 ( address ( 1 )),
buyToken : IERC20 ( address ( 2 )),
receiver : address ( 0 ),
partSellAmount : 1e18 ,
minPartLimit : 1e17 ,
t0 : block .timestamp,
n : 2 ,
t : 3600 ,
span : 0 ,
appData : keccak256 ( abi . encode ( "test.twap" , i))
});
leaves[i] = IConditionalOrder. ConditionalOrderParams ({
handler : IConditionalOrder ( address (twap)),
salt : keccak256 ( abi . encode (i)),
staticInput : abi . encode (twapData)
});
}
// 2. Hash all leaves (double hash)
bytes32 [] memory hashes = new bytes32 []( 50 );
for ( uint256 i = 0 ; i < 50 ; i ++ ) {
hashes[i] = keccak256 (
bytes . concat ( keccak256 ( abi . encode (leaves[i])))
);
}
// 3. Sort hashes
bytes32 [] memory sortedHashes = sort (hashes);
// 4. Get root
bytes32 root = getRoot (sortedHashes);
// 5. Set root on-chain
safe. execTransaction (
address (composableCow),
0 ,
abi . encodeWithSelector (
ComposableCoW.setRoot.selector,
root,
ComposableCoW. Proof ({location : 0 , data : bytes ( "" )})
),
Enum.Operation.Call,
0 , 0 , 0 , address ( 0 ), payable ( 0 ),
signatures
);
// 6. Get tradeable order for first leaf
bytes32 [] memory proof = getProof (sortedHashes, 0 );
// Find which original leaf is at sorted position 0
bytes32 targetHash = sortedHashes[ 0 ];
IConditionalOrder.ConditionalOrderParams memory targetParams;
for ( uint i = 0 ; i < 50 ; i ++ ) {
if (hashes[i] == targetHash) {
targetParams = leaves[i];
break ;
}
}
// 7. Retrieve order with proof
(
GPv2Order.Data memory order,
bytes memory signature
) = composableCow. getTradeableOrderWithSignature (
address (safe),
targetParams,
bytes ( "" ),
proof
);
// 8. Submit to CoW Protocol API
// ... submit order and signature
}
function sort ( bytes32 [] memory data )
internal pure returns ( bytes32 [] memory )
{
// Simple bubble sort (use better algorithm in production)
for ( uint i = 0 ; i < data.length; i ++ ) {
for ( uint j = i + 1 ; j < data.length; j ++ ) {
if (data[i] > data[j]) {
bytes32 temp = data[i];
data[i] = data[j];
data[j] = temp;
}
}
}
return data;
}
}
Gas Efficiency Comparison
Operation Single Orders Merkle Root Create 10 orders ~10 × 50k gas = 500k gas ~50k gas (1 setRoot) Create 50 orders ~50 × 50k gas = 2.5M gas ~55k gas (1 setRoot) Create 100 orders ~100 × 50k gas = 5M gas ~60k gas (1 setRoot)
Merkle roots become more efficient at scale. The gas cost of setRoot is roughly constant regardless of tree size, while create scales linearly with order count.
Helper Library
From the test suite (test/libraries/ComposableCoWLib.t.sol):
library ComposableCoWLib {
// Double hash for merkle leaf
function hash (
IConditionalOrder . ConditionalOrderParams memory params
) internal pure returns ( bytes32 ) {
return keccak256 ( bytes . concat ( keccak256 ( abi . encode (params))));
}
// Helper to get root and proof for nth leaf
function getRootAndProof (
IConditionalOrder . ConditionalOrderParams [] memory leaves ,
uint256 n ,
mapping ( bytes32 => IConditionalOrder . ConditionalOrderParams ) storage m ,
function ( bytes32 [] memory ) internal pure returns ( bytes32 ) getRoot ,
function ( bytes32 [] memory , uint256 ) internal pure returns ( bytes32 [] memory ) getProof
) internal returns (
bytes32 ,
bytes32 [] memory ,
IConditionalOrder . ConditionalOrderParams memory
) {
// Hash and sort leaves
bytes32 [] memory hashes = new bytes32 []( leaves . length );
for ( uint256 i = 0 ; i < leaves.length; i ++ ) {
hashes[i] = hash (leaves[i]);
m[hashes[i]] = leaves[i];
}
bytes32 [] memory sortedHashes = sort (hashes);
bytes32 root = getRoot (sortedHashes);
bytes32 [] memory proof = getProof (sortedHashes, n);
IConditionalOrder.ConditionalOrderParams memory leaf = m[sortedHashes[n]];
return (root, proof, leaf);
}
}
Best Practices
Use Double Hashing Always use keccak256(bytes.concat(keccak256(abi.encode(params)))) for leaves
Sort Hashes Sort leaf hashes before building tree for deterministic proofs
Store Off-Chain Keep the full tree and proofs off-chain; only store root on-chain
Emit Proofs Use location: 1 to emit proofs in events for watchtower indexing
When to Use Merkle Roots
Use merkle roots when:
Managing 5+ conditional orders
Need to authorize many orders atomically
Orders are relatively static (not frequently updated)
Gas optimization is important
Use single orders when:
Managing 1-4 orders
Orders need individual context values in cabinet
Orders are created/removed frequently
Simplicity is preferred over gas optimization
Next Steps