Skip to main content

Overview

Value Factories provide a mechanism for storing on-chain values in the ComposableCoW “cabinet” at the time of order creation or root setting. These values can be used by conditional order handlers to make decisions based on chain state captured at a specific moment.
Value Factories are typically used with the createWithContext or setRootWithContext functions to store snapshots of on-chain data alongside conditional orders.

IValueFactory Interface

The IValueFactory interface is simple and flexible:
interface IValueFactory {
    /**
     * Return a value at the time of the call
     * @param data Implementation specific off-chain data
     * @return value The value at the time of the call
     */
    function getValue(bytes calldata data) external view returns (bytes32 value);
}
The factory reads on-chain state and returns a bytes32 value that gets stored in the cabinet. The data parameter allows for implementation-specific configuration.

The Cabinet Storage

ComposableCoW maintains a “cabinet” - a nested mapping that stores contextual values:
// From ComposableCoW.sol
mapping(address => mapping(bytes32 => bytes32)) public cabinet;
Values are stored with:
  • First key: Owner address (your Safe)
  • Second key: Context identifier (order hash or bytes32(0) for merkle roots)
  • Value: The bytes32 returned by the value factory

CurrentBlockTimestampFactory

The most common value factory implementation stores the current block timestamp:
contract CurrentBlockTimestampFactory is IValueFactory {
    function getValue(bytes calldata) external view override returns (bytes32) {
        return bytes32(block.timestamp);
    }
}
This factory ignores the data parameter and simply returns the current block.timestamp as a bytes32.

Use Case: Time-based Orders

Storing the creation timestamp allows conditional orders to:
  • Calculate time elapsed since order creation
  • Implement vesting schedules
  • Create time-locked trading strategies
  • Set expiration times relative to creation

Creating Orders with Context

1

Deploy a Value Factory

Deploy a value factory contract (or use an existing one like CurrentBlockTimestampFactory).
2

Call createWithContext

Create your conditional order with a stored value:
IConditionalOrder.ConditionalOrderParams memory params = IConditionalOrder.ConditionalOrderParams({
    handler: IConditionalOrder(handlerAddress),
    salt: bytes32(uint256(1)),
    staticInput: abi.encode(orderData)
});

composableCow.createWithContext(
    params,
    IValueFactory(timestampFactoryAddress),
    "", // empty data for CurrentBlockTimestampFactory
    true // dispatch event
);
3

Access the value in your handler

Your conditional order handler can read the stored value:
bytes32 creationTime = composableCow.cabinet(owner, ctx);

Setting Roots with Context

For merkle tree-based order management, use setRootWithContext:
composableCow.setRootWithContext(
    merkleRoot,
    proof,
    IValueFactory(timestampFactoryAddress),
    "" // factory data
);
This stores the value at cabinet[msg.sender][bytes32(0)] - the default slot for merkle root contexts.
For merkle roots, the cabinet slot defaults to bytes32(0) to save gas on calldata and enable a common pattern for root-level context.

Custom Value Factory Examples

Token Price Factory

Capture a token price at order creation time using a Chainlink oracle:
import {IAggregatorV3Interface} from "./interfaces/IAggregatorV3Interface.sol";
import {IValueFactory} from "./interfaces/IValueFactory.sol";

contract ChainlinkPriceFactory is IValueFactory {
    function getValue(bytes calldata data) external view override returns (bytes32) {
        // Decode the oracle address from data
        address oracleAddress = abi.decode(data, (address));
        
        // Get the latest price
        IAggregatorV3Interface oracle = IAggregatorV3Interface(oracleAddress);
        (, int256 price,,,) = oracle.latestRoundData();
        
        // Return as bytes32
        return bytes32(uint256(price));
    }
}
Usage:
composableCow.createWithContext(
    params,
    IValueFactory(priceFactoryAddress),
    abi.encode(chainlinkOracleAddress),
    true
);

Token Balance Factory

Capture the current balance of a token:
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IValueFactory} from "./interfaces/IValueFactory.sol";

contract TokenBalanceFactory is IValueFactory {
    function getValue(bytes calldata data) external view override returns (bytes32) {
        // Decode token and account addresses
        (address token, address account) = abi.decode(data, (address, address));
        
        // Get current balance
        uint256 balance = IERC20(token).balanceOf(account);
        
        return bytes32(balance);
    }
}

Composite Value Factory

Store multiple values packed into a single bytes32:
contract CompositeFactory is IValueFactory {
    function getValue(bytes calldata data) external view override returns (bytes32) {
        // Example: Store timestamp (32 bits) + block number (32 bits) + custom data (192 bits)
        uint256 timestamp = block.timestamp & 0xFFFFFFFF;
        uint256 blockNumber = block.number & 0xFFFFFFFF;
        
        // Decode custom data
        uint256 customData = abi.decode(data, (uint256));
        
        // Pack into bytes32
        bytes32 packed = bytes32(
            (timestamp << 224) |
            (blockNumber << 192) |
            (customData & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
        );
        
        return packed;
    }
}

Reading Cabinet Values in Handlers

Your conditional order handler can access cabinet values during order generation:
contract TimeLockOrder is BaseConditionalOrder {
    struct Data {
        IERC20 sellToken;
        IERC20 buyToken;
        uint256 sellAmount;
        uint256 buyAmount;
        uint256 lockDuration; // seconds
        bytes32 appData;
    }
    
    function getTradeableOrder(
        address owner,
        address,
        bytes32 ctx,
        bytes calldata staticInput,
        bytes calldata
    ) public view override returns (GPv2Order.Data memory) {
        Data memory data = abi.decode(staticInput, (Data));
        
        // Read the creation timestamp from cabinet
        bytes32 creationTime = ComposableCoW(msg.sender).cabinet(owner, ctx);
        uint256 unlockTime = uint256(creationTime) + data.lockDuration;
        
        // Revert if still locked
        if (block.timestamp < unlockTime) {
            revert IConditionalOrder.PollTryAtEpoch(
                unlockTime,
                "Order still locked"
            );
        }
        
        // Order is unlocked, return tradeable order
        return GPv2Order.Data({
            sellToken: data.sellToken,
            buyToken: data.buyToken,
            receiver: address(0),
            sellAmount: data.sellAmount,
            buyAmount: data.buyAmount,
            validTo: uint32(block.timestamp + 600),
            appData: data.appData,
            feeAmount: 0,
            kind: GPv2Order.KIND_SELL,
            partiallyFillable: false,
            sellTokenBalance: GPv2Order.BALANCE_ERC20,
            buyTokenBalance: GPv2Order.BALANCE_ERC20
        });
    }
}

Best Practices

Gas Considerations: Value factory getValue calls happen during transaction execution (not view calls). Keep computations lightweight.

Design Tips

  • Use the data parameter for flexible, reusable factories
  • Return deterministic values based on block state
  • Consider packing multiple values into a single bytes32 when needed
  • Document what your factory returns and how to decode it

Context Keys

The context key determines where values are stored in the cabinet:
Order TypeContext KeyDescription
Merkle root ordersbytes32(0)Default slot for root-level context
Single ordersH(params)Hash of the conditional order params
When creating a single order with createWithContext, the context is tied to that specific order. When setting a root with setRootWithContext, all orders in the tree share the same context at bytes32(0).

Advanced: Dynamic Context Updates

While cabinet values are typically set at creation time, you can update them:
// Remove an order and its context
composableCow.remove(orderHash); // Clears both singleOrders and cabinet

// Recreate with new context
composableCow.createWithContext(params, newFactory, newData, true);
For merkle roots, set a new root with updated context:
composableCow.setRootWithContext(newRoot, proof, factory, data);

Next Steps

Custom Orders

Learn how to implement conditional order handlers that use cabinet values

Swap Guards

Add restrictions to your conditional orders

Build docs developers (and LLMs) love