Skip to main content

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

1

Collect Orders

Gather all conditional orders as ConditionalOrderParams structs
2

Build Merkle Tree

Create merkle tree where each leaf is a double hash: keccak256(bytes.concat(keccak256(abi.encode(params))))
3

Set Root On-Chain

Call ComposableCoW.setRoot() with the computed merkle root
4

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:
LocationBehavior
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:
  1. Add new leaves to your tree
  2. Recalculate the merkle root
  3. 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:
  1. Remove leaves from your tree
  2. Recalculate the merkle root
  3. 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

OperationSingle OrdersMerkle 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

Build docs developers (and LLMs) love