Warning
This fork is unaudited and is not deployed.
It has diverged substantially from upstream cowprotocol/composable-cow: the order generation entry point, error signalling, polling interface, proof payload location and handler discovery surface have all changed, and several of those changes are breaking. The audits below cover the upstream contracts as they stood before this work and do not carry over to this tree.
Do not use in production. See Divergence from upstream.
This repository is the next in evolution of the conditional-smart-orders, providing a unified interface for stateless, composable conditional orders. ComposableCow is designed to be used with the ExtensibleFallbackHandler, a powerful extensible fallback handler that allows for significant customisation of a Safe, while preserving strong security guarantees.
This fork is not a drop-in replacement for cowprotocol/composable-cow. The settlement path is shape-compatible; the changes concentrate on what an off-chain monitoring service can learn from the contracts, and on making that answer machine-readable rather than something to be parsed out of a revert.
- Order generation.
getTradeableOrderis nowgenerateOrder, declared onIConditionalOrderGenerator. - Typed errors.
string reasonis replaced bybytes4error selectors throughout, so a consumer can classify a failure without reading prose. - Structured polling. Polling returns a verdict rather than reverting for the caller to decode: a result code (
POST,WAIT_TIMESTAMP,WAIT_BLOCK,TRY_NEXT_BLOCK,INVALID,NEEDS_INPUT), the block or timestamp to retry at, and the handler's reason selector. A missingoffchainInputis reported asNEEDS_INPUTrather than a timed retry, and a swap-guard restriction is reported separately from the handler's own verdict. - Fill state. The registry composes
GPv2Settlement.filledAmountinto the poll result, so a partially filled order reports its fill rather than appearing tradeable at full size. The overlay is orthogonal to the verdict. - Order manifest.
IOrderManifest.getManifestPageenumerates the discrete orders a conditional order will produce, paginated, with aManifestStatuscarrying the same verdict polling would return. An empty page therefore distinguishes "not yet" from "never". - Handler discovery.
IOrderDescriptorandIOrderModuledescribe a handler and its off-chain extension, each committing to content by digest (PackageKind: Swarm BMT manifest or sha256). - Proof payload locations.
Proofis now{uris, blobVersionedHashes}rather than an opaque{location, data}. Blob hashes are verified attached to the setting transaction, binding publication to authorisation. - Handler validation. Degenerate zero-amount orders are rejected at validation rather than being emitted for settlement to refuse.
- Housekeeping. Solidity 0.8.30 with a pinned toolchain, require-with-custom-error style, build artifacts untracked,
ComposableCoWrenamed toComposableCow, and all deployment state cleared as nothing in this tree is deployed.
Upstream has since added ComposableCowPoller, a registry for on-chain polling schedules and just-in-time order funding. That work is not present here.
docs/architecture.mdcovers the dual-path design: the gas-sensitive settlement path and the gas-irrelevant polling path.docs/discovery.mdspecifies the discovery surface, including commitments, handler descriptors, order modules and merkle payload documents.docs/design/holds the design notes behind individual decisions.
For the purposes of outlining the methodologies, it is assumed that:
- The
Safehas already had its fallback handler set toExtensibleFallbackHandler. - The
Safehas set thedomainVerifierfor theGPv2Settlement.domainSeparator()toComposableCow
A conditional order is a struct ConditionalOrderParams, consisting of:
- The address of handler, ie. type of conditional order (such as
TWAP). - A unique salt.
- Implementation specific
staticInput- data that is known at the creation time of the conditional order.
- From the context of the Safe that is placing the order, call
ComposableCow.createwith theConditionalOrderParamsstruct. Optionally setdispatch = trueto have events emitted that are picked up by an off-chain monitoring service.
- Collect all the conditional orders, which are multiple structs of
ConditionalOrderParams. - Populate a merkle tree with the leaves from (1), where each leaf is a double hashed of the ABI-encoded struct.
- Determine the merkle root of the tree and set this as the root, calling
ComposableCow.setRoot. Theproofdeclares where the payload document (the complete leaf set) is published. Both channels are optional and orthogonal: a.uris: mirrors for the payload document, all referencing the same bytes. Never interpreted on-chain. b.blobVersionedHashes: EIP-4844 blobs carrying the payload, each verified to be attached to the transaction setting the root. c. Both empty: the root is private and no discovery is expected.
Conditional orders may generate one or many discrete orders depending on their implementation. To retrieve a discrete order that is valid at the current block:
- Call
ComposableCow.getTradeableOrderWithSignature(address owner, ConditionalOrderParams params, bytes offchainInput, bytes32[] proof)where:owner: smart contract /Safeparams: mentioned above.offchainInputis any implementation specific offchain input for discrete order generation / validation.proof: a zero length array if a single order, otherwise the merkle proof for the merkle root that's set forowner.
- The call returns a
PollResultand a signature. Whenresult.generator.codeisPOST, useresult.generator.orderto populate aPOSTto the CoW Protocol API to create an order. Set thesigningSchemetoeip1271and thesignatureto that returned from the call in (1). Any other verdict returns an empty signature. - Review the order on CoW Explorer.
- The call does not revert for order conditions.
result.generatorcarries the verdict, the block or timestamp to retry at, and the handler's error selector;result.fillcarries the observed fill state. This provides feedback for monitoring services to modify their internal state. It reverts only for authorisation and handler-interface failures.
- Determine the digest for the conditional order, ie.
H(Params). - Call
ComposableCow.remove(H(Params))
- Prune the leaf from the merkle tree.
- Determine the new root.
- Call
ComposableCow.setRootwith the new root, which will invalidate any orders that have been pruned from the tree.
A simple time-weighted average price trade may be thought of as n smaller trades happening every t time interval, commencing at time t0. Additionally, it is possible to limit a part's validity of the order to a certain span of time interval t.
struct Data {
IERC20 sellToken;
IERC20 buyToken;
address receiver; // address(0) if the safe
uint256 partSellAmount; // amount to sell in each part
uint256 minPartLimit; // minimum buy amount in each part (limit)
uint256 t0;
uint256 n;
uint256 t;
uint256 span;
bytes32 appData;
}NOTE: No direction of trade is specified, as for TWAP it is assumed to be a sell order
Example: Alice wants to sell 12,000,000 DAI for at least 7500 WETH. She wants to do this using a TWAP, executing a part each day over a period of 30 days.
sellToken= DAIbuyToken= WETHreceiver=address(0)partSellAmount= 12000000 / 30 = 400000 DAIminPartLimit= 7500 / 30 = 250 WETHt0= Nominated start time (unix epoch seconds)n= 30 (number of parts)t= 86400 (duration of each part, in seconds)span= 0 (duration ofspan, in seconds, or0for entire interval)appData= the CoW Protocol app data hash applied to every part
If Alice also wanted to restrict the duration in which each part traded in each day, she may set span to a non-zero duration. For example, if Alice wanted to execute the TWAP, each day for 30 days, however only wanted to trade for the first 12 hours of each day, she would set span to 43200 (i.e. 60 * 60 * 12).
Using span allows for use cases such as weekend or week-day only trading.
To create a TWAP order:
- ABI-Encode the
IConditionalOrder.ConditionalOrderParamsstruct with:handler: set to theTWAPsmart contract deployment.salt: set to a unique value.staticInput: the ABI-encodedTWAP.Datastruct.
- Use the
structfrom (1) as either a Merkle leaf, or withComposableCow.createto create a single conditional order. - Approve
GPv2VaultRelayerto traden x partSellAmountof the safe'ssellTokentokens (in the example above,GPv2VaultRelayerwould receive approval for spending 12,000,000 DAI tokens).
NOTE: When calling ComposableCow.create, setting dispatch = true will cause ComposableCow to emit event logs that are indexed by monitoring services automatically. If you wish to maintain a private order (and will submit to the CoW Protocol API through your own infrastructure, you may set dispatch to false).
When using Safe, it is possible to batch together all the above calls to perform this step atomically, and optimise gas consumption / UX.
NOTE: For cancelling a TWAP order, follow the instructions at Conditional order cancellation.
forge(Foundry)
None. This fork has no deployments on any network.
The legacy networks.json recorded upstream's addresses, which belong to contracts this tree no longer matches, so it has been removed rather than left to be mistaken for the fork's own deployments. When this fork does deploy, the canonical machine-readable record is deployments/networks.json.
The broadcast/ directory holds upstream's deployment records and verification inputs. They describe contracts this tree no longer matches and are retained only as history.
The following audits cover the upstream contracts before the changes in this fork. They do not cover the current tree:
- Ackee Blockchain: CoW Protocol -
ComposableCowandExtensibleFallbackHandler - Gnosis internal audit: ComposableCow - May/July 2023
- Gnosis internal audit (August 2024): ComposableCow - Diff between May/July 2023 and August 2024
Copy .env.example to .env. Every script reads PRIVATE_KEY; SETTLEMENT is needed to deploy ComposableCow, COMPOSABLE_COW to deploy order types against it, and SAFE plus TWAP to submit a single order. Contract verification reads ETHERSCAN_API_KEY. The RPC endpoint is passed per command with --rpc-url.
Effort has been made to adhere as close as possible to best practices, with unit and fuzzing tests being implemented. The fuzz tests include test_simulate_fuzz, which runs end-to-end integration testing including settlement of conditional orders.
forge test -vvv --no-match-test "[fF]uzz" # Unit tests only
forge test -vvv # Unit and fuzz testsFuzz tests are seeded in CI (--fuzz-seed) so that a run is reproducible.
forge coverage -vvv --report summaryDeployment is handled by solidity scripts in forge. The network being deployed to is determined by the --rpc-url passed to each command.
To deploy all contracts in a single run:
source .env
forge script script/deploy_ProdStack.s.sol:DeployProdStack --rpc-url $ETH_RPC_URL --broadcast -vvvv --verifyTo deploy individual contracts:
# Deploy ComposableCow
forge script script/deploy_ComposableCow.s.sol:DeployComposableCow --rpc-url $ETH_RPC_URL --broadcast -vvvv --verify
# Deploy order types
forge script script/deploy_OrderTypes.s.sol:DeployOrderTypes --rpc-url $ETH_RPC_URL --broadcast -vvvv --verifyscript/ also carries deploy_ExtensibleFallbackHandler.s.sol and deploy_ValueFactories.s.sol for the supporting contracts.
Each run writes its record under broadcast/, keyed by script and chain id.
There's a dedicated script to verify all contracts at the same time once they have been deployed on a new chain:
export ETHERSCAN_API_KEY="your API key here"
chain_id=1337
dev/verify-contracts.sh "$chain_id"If this doesn't work, check out broadcast/StandardJsonInput/README.md.
For local integration testing, including running an off-chain monitoring service against the deployment, it may be useful deploying to a forked mainnet environment. This can be done with anvil.
-
Open a terminal and run
anvil:anvil --code-size-limit 50000 --block-time 5
NOTE: When deploying the full stack on
anvil, the balancer vault may exceed contract code size limits necessitating the use of--code-size-limit. -
Follow the previous deployment directions, with this time specifying
anvilas the RPC-URL:source .env forge script script/deploy_AnvilStack.s.sol:DeployAnvilStack --rpc-url http://127.0.0.1:8545 --broadcast -vvvvNOTE: Within the output of the above command, there will be an address for a
Safethat was deployed toanvil. This is needed for the next step.NOTE:
--verifyis omitted as with local deployments, these should not be submitted to Etherscan for verification. -
To then simulate the creation of a single order:
source .env SAFE="address here" forge script script/submit_SingleOrder.s.sol:SubmitSingleOrder --rpc-url http://127.0.0.1:8545 --broadcast