cowswap-twap-orders
cowswap-twap-orders copied to clipboard
🐄🐮 Time-weighted average price orders for CoW Protocol
🚨 Repository deprecated
This repository was the formative work of what has grown to become the ComposableCoW conditional order framework. This includes the repository for the work having been moved to the cowprotocol GitHub organisation.
If you've come here curious about Cow Swap's recent launch of TWAP, check their article here.
CoW Protocol Conditional Orders
This repository extends conditional-smart-orders, providing tight integration with Safe. It use's Safe's SignMessageLib to sign conditional orders. In doing so we:
- Reduce conditional order creation gas costs by ~75% (for TWAPs).
- Reduce
isValidSignaturegas costs on each settlement.
The reasoning behind using Conditional Orders with Safe is covered well on the conditional-smart-orders repository, however this implementation of ConditionalOrders provides added benefits.
Notably, the CoWTWAPFallbackHandler can be set on any existing Safe with no loss of stock Safe functionality. This is achieved by CoWTWAPFallbackHandler inheriting from CompatibilityFallbackHandler, providing all the existing Safe functionality, and only selectively overriding and extending isValidSignature to achieve conditional order capabilities with CoW Protocol. isValidSignature still even allows for verifying signatures not related to ConditionalOrders.
Architecture
All state (excluding external on-chain data requirements as may be used by the conditional order) is stored off-chain, and passed as bytes (calldata) to handling functions.
Functions:
dispatch(bytes payload)getTradeableOrder(bytes payload)
Event:
ConditionalOrderCreated(address indexed, bytes)
Errors:
OrderNotValid()OrderNotSigned()OrderExpired()OrderCancelled()
EIP-712 Types:
ConditionalOrder(bytes payload)CancelOrder(bytes32 order)
Assumptions
- CoW Protocol enforces single-use orders, ie. no
GPv2Ordercan be filled more than once.
Methodology
For the purposes of outlining the methodologies, it is assumed that the Safe has already had it's fallback handler set to that required for the implementation specific conditional order.
Conditional order creation
- The conditional order is ABI-encoded to a bytes payload.
- The payload from (1) is used as the input to generate the EIP-712 digest of
ConditionalOrder(bytes payload). - The digest from (2) is signed by the safe using a
DELEGATECALLtoSignMessageLib. - A call is made to the safe's
dispatch(bytes payload), passing in the payload from (1). dispatchtriggers eventConditionalOrderCreatedthat is indexed, containing the safe's address, and the payload from (1) to be used.
CAUTION: It is required to call dispatch after the order has been signed by the safe, otherwise the call will revert with OrderNotSigned().
Get Tradeable Order
Conditional orders may generate one or many orders depending on their implementation. To retrieve an order that is valid at the current block:
- Call
getTradeableOrder(bytes payload)using the implementation specific ABI-encoded payload to get aGPv2Order. - Decoding the
GPv2Order, use this data to populate aPOSTto the CoW Protocol API to create an order. Set thesigningSchemetoeip1271and thesignatureto the implementation specific ABI-encoded payload (ie.payload). - Review the order on CoW Explorer.
getTradeableOrder(bytes payload)may revert with one of the custom errors. This provides feedback for watch towers to modify their internal state.
Conditional order cancellation
- Determine the digest for the conditional order, as discussed in Conditional order creation.
- Generate the EIP-712 digest of
CancelOrder(bytes32 order)where order is the digest from (1). - Sign the digest from (2) with the safe by using a
DELEGATECALLTOSignMessageLib.
Signing
All signatures / hashes are EIP-712. The EIP712Domain for determing digests is that returned by GPv2Settlement.domainSeparator() on the relevant chain.
Time-weighted average price (TWAP)
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.
Data Structure
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, or0for 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 (ie. 60 * 60 * 12).
Using span allows for use cases such as weekend or week-day only trading.
Methodology
To create a TWAP order:
- ABI-Encode the above
Datastruct and sign it with the safe as outlined in Conditional Order Creation - Approve
GPv2VaultRelayerto traden x partSellAmountof the safe'ssellTokentokens (in the example above,GPv2VaultRelayerwould receive approval for spending 12,000,000 DAI tokens). - Call
dispatchto announce the TWAP order to the watch tower.
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.
NOTE: For cancelling a TWAP order, follow the instructions at Conditional order cancellation.
CLI
The CLI utility provided contains help functions to see all the options / configurability available for each subcommand.
CAUTION: This utility handles private keys for proposing transactions to Safes. Standard safety precautions associated with private key handling applies. It is recommended to NEVER pass private keys directly via command line as this may expose sensitive keys to those who have access to list processes running on your machine.
Enviroment setup
Copy .env.example to .env, setting at least the PRIVATE_KEY and ETH_RPC_URL. Then build the project, in the root directory of the repository:
yarn build
Usage
Usage: conditional-orders [options] [command]
Dispatch or cancel conditional orders on Safe using CoW Protocol
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
create-twap [options] Create a TWAP order
set-fallback-handler [options] Set the fallback handler of the Safe
cancel-order [options] Cancel an order
help [options] [command] display help for command
-
Setting a safe's fallback handler
yarn ts-node cli.ts set-fallback-handler -s 0xdc8c452D81DC5E26A1A73999D84f2885E04E9AC3 --handler 0x87b52ed635df746ca29651581b4d87517aaa9a9fCheck your safe's transaction queue and you should see the newly created transaction.
-
Creating a TWAP order
The CLI utility will automatically do some math for you. All order creation is from the perspective of totals. By specifying the
--sell-token,--buy-token,--total-sell-amount, and--total-min-buyamount, the CLI will automatically determine the number of decimals, parse the values, and divide the totals by the number of parts (-n), using the results as the basis for the TWAP order.yarn ts-node cli.ts create-twap -s 0xdc8c452D81DC5E26A1A73999D84f2885E04E9AC3 --sell-token 0x91056D4A53E1faa1A84306D4deAEc71085394bC8 --buy-token 0x02ABBDbAaa7b1BB64B5c878f7ac17f8DDa169532 --total-sell-amount 1000 --total-min-buy-amount 1 -n 6 -t 600Check your safe' transaction queue, and you should see a newly created transaction that batches together the signing of the conditional order, approving
GPv2VaultRelayeronsellTokenfortotal-sell-amount, and emits the order viadispatch.NOTE: When creating TWAP orders, the
--total-sell-amountand--total-min-buy-amountare specified in whole units of the respective ERC20 token. For example, if wanting to buy a total amount of 1 WETH, specify--total-min-buy-amount 1. The CLI will automatically determine decimals and specify these appropriately. -
Cancelling a conditional order
To cancel an order, you must know it's order hash, which is the EIP-712 digest of
ConditionalOrder(bytes payload).yarn ts-node cli.ts cancel-order -s 0xdc8c452D81DC5E26A1A73999D84f2885E04E9AC3 --order-hash 0x6070b52cef3c1a6dd0070bd7382b32418b66dc333bf36b1e7ae28f6d7b287f07Check your safe's transaction queue, and you should see a newly created transaction to cancel the conditional order.
Tenderly Actions
A watchdog has been implementing using Tenderly Actions. By means of emitted Event and new block monitoring, conditional orders can run autonomously.
Notably, with the CondtionalOrderCreated event, multiple conditional orders can be created for one safe - in doing so, the actions maintain a registry of:
- Safes that have created at least one conditional order.
- All payloads for conditional orders by safe that have not expired or been cancelled.
- All part orders by
orderUidcontaining their status (SUBMITTED,FILLED) - theTradeonGPv2Settlementis monitored to determine if an order isFILLED.
As orders expire, or are cancelled, they are removed from the registry to conserve storage space.
TODO: Improvements to flag an orderUid as SUBMITTED if the API returns an error due to duplicate order submission. This would limit queries to the CoW Protocol API to the total number of watchtowers being run.
Local testing
From the root directory of the repository:
yarn build
yarn test:actions
If for some reason the watch tower hasn't picked up a conditional order, this can be simulated by calling a local version directly:
yarn build
ETH_RPC_URL=http://rpc-url-here.com:8545 yarn ts-node ./actions/test/run_local.ts <safeAddress> <payload>
When subsituting in the safeAddress and payload, this will simulate the watch tower, and allow for order submission if the watch tower is down.
Deployment
If running your own watch tower, or deploying for production:
tenderly actions deploy
Developers
Requirements
forge(Foundry)node(>= v16.18.0)yarnnpmtenderly
Deployed Contracts
Contracts within have been audited by Group0. See their audit report here.
| Contact Name | Ethereum Mainnet | Goerli | Gnosis Chain |
|---|---|---|---|
CoWTWAPFallbackHandler |
0x87b52ed635df746ca29651581b4d87517aaa9a9f |
0x87b52ed635df746ca29651581b4d87517aaa9a9f |
0x87b52ed635df746ca29651581b4d87517aaa9a9f |
NOTE: Due to some issues between forge and gnosisscan, contracts are verified on sourcify, and therefore viewabled on here on blockscout for Gnosis Chain. All other deployments are verified on their respective Etherscan-derivative block explorer.
Environment setup
Copy the .env.example to .env and set the applicable configuration variables for the testing / deployment environment.
Testing
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
Coverage
forge coverage -vvv --no-match-test "fork" --report summary
Deployment
Deployment is handled by solidity scripts in forge. The network being deployed to is dependent on the ETH_RPC_URL.
source .env
forge script script/deploy_CoWTWAPFallbackHandler.s.sol:DeployCoWTWAPFallbackHandler --rpc-url $ETH_RPC_URL --broadcast -vvvv --verify
Local deployment
For local integration testing, including the use of Tenderly Actions, it may be useful deploying to a forked mainnet environment. This can be done with anvil.
-
Open a terminal and run
anvil:anvil --fork-url http://erigon.dappnode:8545 -
Follow the previous deployment directions, with this time specifying
anvilas the RPC-URL:source .env forge script script/deploy_CoWTWAPFallbackHandler.s.sol:DeployCoWTWAPFallbackHandler --rpc-url http://127.0.0.1:8545 --broadcast -vvvvNOTE:
--verifyis omitted as with local deployments, these should not be submitted to Etherscan for verification.