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.
A detailed explanation on the architecture is available here.
For the purposes of outlining the methodologies, it is assumed that:
- The
Safe
has already had its fallback handler set toExtensibleFallbackHandler
. - The
Safe
has set thedomainVerifier
for 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.create
with theConditionalOrderParams
struct. Optionally setdispatch = true
to have events emitted that are picked up by a watch tower.
- 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
. Theproof
must be set, and currently: a. Set alocation
of0
for no proofs emitted. b. Otherwise, set alocation
of1
at which case the payload in the proof will be interpreted as an array of proofs and indexed by the watch tower.
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 /Safe
params
: mentioned above.offchainInput
is 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
.
- Decoding the
GPv2Order
, use this data to populate aPOST
to the CoW Protocol API to create an order. Set thesigningScheme
toeip1271
and thesignature
to that returned from the call in (1). - Review the order on CoW Explorer.
getTradeableOrderWithSignature(address,ConditionalOrderParams,bytes,bytes32[])
may revert with one of the custom errors. This provides feedback for watch towers to modify their internal state.
- 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.setRoot
with 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;
}
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, or0
for entire interval)
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.ConditionalOrderParams
struct with:handler
: set to theTWAP
smart contract deployment.salt
: set to a unique value.staticInput
: the ABI-encodedTWAP.Data
struct.
- Use the
struct
from (1) as either a Merkle leaf, or withComposableCoW.create
to create a single conditional order. - Approve
GPv2VaultRelayer
to traden x partSellAmount
of the safe'ssellToken
tokens (in the example above,GPv2VaultRelayer
would 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 the watch tower 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
).
Fortunately, when using Safe, it is possible to batch together all the above calls to perform this step atomically, and optimise gas consumption / UX. For code examples on how to do this, please refer to the CLI.
TODO NOTE: For canceling a TWAP order, follow the instructions at Conditional order cancellation.
forge
(Foundry)
The above deployed contracts have been audited by:
- Ackee Blockchain: CoW Protocol -
ComposableCoW
andExtensibleFallbackHandler
- Gnosis internal audit: ComposableCoW - May/July 2023
- Gnosis internal audit (August 2024): ComposableCoW - Diff between May/July 2023 and August 2024
Copy the .env.example
to .env
and set the applicable configuration variables for the testing / deployment environment.
Effort has been made to adhere as close as possible to best practices, with unit, fuzzing and fork tests being implemented.
NOTE: Fuzz tests also include a simulate
that runs full end-to-end integration testing, including the ability to settle conditional orders. Fork testing simulates end-to-end against production ethereum mainnet contracts, and as such requires ETH_RPC_URL
to be defined (this should correspond to an archive node).
forge test -vvv --no-match-test "fork|[fF]uzz" # Basic unit testing only
forge test -vvv --no-match-test "fork" # Unit and fuzz testing
forge test -vvv # Unit, fuzz, and fork testing
forge coverage -vvv --no-match-test "fork" --report summary
Deployment is handled by solidity scripts in forge
. The network being deployed to is dependent on the ETH_RPC_URL
.
To deploy all contracts in a single run, run:
source .env
forge script script/deploy_ProdStack.s.sol:DeployProdStack --rpc-url $ETH_RPC_URL --broadcast -vvvv --verify
To 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 --verify
The broadcast
directory collects the latest run of the deployment script by network and is updated manually.
When the script is ran, the corresponding files can be found in the folder broadcast/deploy_OrderTypes.s.sol/
.
Because of the issue #39, in order to achieve deterministic deployment it is needed to:
- Go to a deployed contract in another network, open the creation TX (e.g. ExtensibleFallbackHandler in mainnet)
- Go to
Click to show more
and copy theInput Data
in Original format, also copy theto
address - Use your favourite tool to make a transaction (e.g., swiss-knife)
- Use the corresponding
Input Data
andto
and send the tx - A new contract will be deployed using
CREATE2
to the same deterministic address
How to verify the contracts:
- Some contracts will auto-verify themselves (rare), because they exist in other networks
- Some contracts can be verified with forge, e.g.,
forge verify-contract --etherscan-api-key $BASESCAN_API_KEY --rpc-url $RPC_URL 0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5 lib/safe/contracts/handler/ExtensibleFallbackHandler.sol:ExtensibleFallbackHandler
- For the contracts which can't be verified either way, the standard json input has to be generated with forge:
forge verify-contract --verifier sourcify --show-standard-json-input --etherscan-api-key $BASESCAN_API_KEY --rpc-url $RPC_URL 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 src/ComposableCoW.sol:ComposableCoW > ComposableCoW.json
, and submit the json to the corresponding block explorer through the "standard-json" option on its web interface
For local integration testing, including the use of Watch Tower, 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
anvil
as the RPC-URL:source .env forge script script/deploy_AnvilStack.s.sol:DeployAnvilStack --rpc-url http://127.0.0.1:8545 --broadcast -vvvv
NOTE: Within the output of the above command, there will be an address for a
Safe
that was deployed toanvil
. This is needed for the next step.NOTE:
--verify
is 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