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
isValidSignature
gas 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 ConditionalOrder
s 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 ConditionalOrder
s.
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)
- CoW Protocol enforces single-use orders, ie. no
GPv2Order
can be filled more than once.
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.
- 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
DELEGATECALL
toSignMessageLib
. - A call is made to the safe's
dispatch(bytes payload)
, passing in the payload from (1). dispatch
triggers eventConditionalOrderCreated
that 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()
.
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 aPOST
to the CoW Protocol API to create an order. Set thesigningScheme
toeip1271
and thesignature
to 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.
- 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
DELEGATECALL
TOSignMessageLib
.
All signatures / hashes are EIP-712. The EIP712Domain
for determing digests is that returned by GPv2Settlement.domainSeparator()
on the relevant chain.
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
(ie. 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 above
Data
struct and sign it with the safe as outlined in Conditional Order Creation - 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). - Call
dispatch
to 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.
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.
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: 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 0x87b52ed635df746ca29651581b4d87517aaa9a9f
Check 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 600
Check your safe' transaction queue, and you should see a newly created transaction that batches together the signing of the conditional order, approving
GPv2VaultRelayer
onsellToken
fortotal-sell-amount
, and emits the order viadispatch
.NOTE: When creating TWAP orders, the
--total-sell-amount
and--total-min-buy-amount
are 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 0x6070b52cef3c1a6dd0070bd7382b32418b66dc333bf36b1e7ae28f6d7b287f07
Check your safe's transaction queue, and you should see a newly created transaction to cancel the conditional order.
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
orderUid
containing their status (SUBMITTED
,FILLED
) - theTrade
onGPv2Settlement
is 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.
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.
If running your own watch tower, or deploying for production:
tenderly actions deploy
forge
(Foundry)node
(>= v16.18.0
)yarn
npm
tenderly
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.
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
.
source .env
forge script script/deploy_CoWTWAPFallbackHandler.s.sol:DeployCoWTWAPFallbackHandler --rpc-url $ETH_RPC_URL --broadcast -vvvv --verify
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
anvil
as the RPC-URL:source .env forge script script/deploy_CoWTWAPFallbackHandler.s.sol:DeployCoWTWAPFallbackHandler --rpc-url http://127.0.0.1:8545 --broadcast -vvvv
NOTE:
--verify
is omitted as with local deployments, these should not be submitted to Etherscan for verification.