Skip to content

About

🐮 🧾 Composable Conditional Orders for CoW Protocol

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

ComposableCoW: Composable Conditional orders

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.

Architecture

A detailed explanation on the architecture is available here.

Methodology

For the purposes of outlining the methodologies, it is assumed that:

  1. The Safe has already had its fallback handler set to ExtensibleFallbackHandler.
  2. The Safe has set the domainVerifier for the GPv2Settlement.domainSeparator() to ComposableCoW

Conditional order creation

A conditional order is a struct ConditionalOrderParams, consisting of:

  1. The address of handler, ie. type of conditional order (such as TWAP).
  2. A unique salt - a fresh random value per order, as it is what keeps two otherwise-identical orders distinct.
  3. Implementation specific staticInput - data that is known at the creation time of the conditional order.
Single Order
  1. From the context of the Safe that is placing the order, call ComposableCoW.create with the ConditionalOrderParams struct. Optionally set dispatch = true to have events emitted that are picked up by a watch tower.
Merkle Root
  1. Collect all the conditional orders, which are multiple structs of ConditionalOrderParams.
  2. Populate a merkle tree with the leaves from (1), where each leaf is a double hashed of the ABI-encoded struct.
  3. Determine the merkle root of the tree and set this as the root, calling ComposableCoW.setRoot. The proof must be set, and currently: a. Set a location of 0 for no proofs emitted. b. Otherwise, set a location of 1 at which case the payload in the proof will be interpreted as an array of proofs and indexed by the watch tower.

Get Tradeable Order With Signature

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:

  1. 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 for owner.
  2. Decoding the GPv2Order, use this data to populate a POST to the CoW Protocol API to create an order. Set the signingScheme to eip1271 and the signature to that returned from the call in (1).
  3. Review the order on CoW Explorer.
  4. getTradeableOrderWithSignature(address,ConditionalOrderParams,bytes,bytes32[]) may revert with one of the custom errors. This provides feedback for watch towers to modify their internal state.

Conditional order cancellation

Single Order
  1. Determine the digest for the conditional order, ie.H(Params).
  2. Call ComposableCoW.remove(H(Params))
Merkle Root
  1. Prune the leaf from the merkle tree.
  2. Determine the new root.
  3. Call ComposableCoW.setRoot with the new root, which will invalidate any orders that have been pruned from the tree.

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 = DAI
  • buyToken = WETH
  • receiver = address(0)
  • partSellAmount = 12000000 / 30 = 400000 DAI
  • minPartLimit = 7500 / 30 = 250 WETH
  • t0 = Nominated start time (unix epoch seconds)
  • n = 30 (number of parts)
  • t = 86400 (duration of each part, in seconds)
  • span = 0 (duration of span, in seconds, or 0 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.

Methodology

To create a TWAP order:

  1. ABI-Encode the IConditionalOrder.ConditionalOrderParams struct with:
    • handler: set to the TWAP smart contract deployment.
    • salt: a fresh random value per order. It is what keeps two otherwise-identical orders distinct, so reusing one makes them indistinguishable.
    • staticInput: the ABI-encoded TWAP.Data struct.
  2. Use the struct from (1) as either a Merkle leaf, or with ComposableCoW.create to create a single conditional order.
  3. Approve GPv2VaultRelayer to trade n x partSellAmount of the safe's sellToken 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.

Just-in-time funding with ComposableCowPoller

ComposableCowPoller enables an EOA to create a ComposableCoW conditional order, such as a TWAP, without moving the full sell amount into the order's smart account up front. The funds remain in the EOA until each part is ready to trade. A pre-hook then calls pollFunds, which verifies that the order is still active and transfers only that part's sell amount to the smart account. Each part can be funded only once.

The funder can register or revoke a funding schedule directly. To avoid a separate transaction, the funder can instead sign an EIP-712 authorization that a relayer submits on-chain. The Poller validates EOA signatures directly and uses ERC-1271 when the funder is a contract. Signed actions include a deadline and the current authorization epoch. Revocation derives the schedule ID from its funder, handler, owner, and salt, so it works before or after registration without allowing one funder to cancel another's schedule. It advances the authorization epoch, invalidating pending signatures from prior epochs while allowing the same schedule ID to be reused.

A funder who already does some operations through a CowShed does not need a second signature to also include the registration. registerFromShed and revokeFromShed allow the funder's shed, proxyOf(funder), to register and revoke schedules on behalf of the funder. Without these methods, the funder would need to sign a second EIP-712 authorization or transaction for the schedule registration.

Removing the conditional order from ComposableCoW stops further funding. Revoking its funding schedule does too, but does not remove the funder's token allowance; revoke that allowance separately when it is no longer needed.

Developers

Requirements

Deployed Contracts

Contract Name Ethereum Mainnet Gnosis Chain Sepolia Arbitrum One Base Bsc Avalanche Optimism Polygon Lens
ExtensibleFallbackHandler 0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5 0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5 0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5 0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5 0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5 0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5 0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5 0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5 0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5 0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5
ComposableCoW 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74
TWAP 0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5 0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5 0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5 0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5 0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5 0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5 0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5 0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5 0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5 0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5
GoodAfterTime 0xdaf33924925e03c9cc3a10d434016d6cfad0add5 0xdaf33924925e03c9cc3a10d434016d6cfad0add5 0xdaf33924925e03c9cc3a10d434016d6cfad0add5 0xdaf33924925e03c9cc3a10d434016d6cfad0add5 0xdaf33924925e03c9cc3a10d434016d6cfad0add5 0xdaf33924925e03c9cc3a10d434016d6cfad0add5 0xdaf33924925e03c9cc3a10d434016d6cfad0add5 0xdaf33924925e03c9cc3a10d434016d6cfad0add5 0xdaf33924925e03c9cc3a10d434016d6cfad0add5 0xdaf33924925e03c9cc3a10d434016d6cfad0add5
PerpetualStableSwap 0x519BA24e959E33b3B6220CA98bd353d8c2D89920 0x519BA24e959E33b3B6220CA98bd353d8c2D89920 0x519BA24e959E33b3B6220CA98bd353d8c2D89920 0x519BA24e959E33b3B6220CA98bd353d8c2D89920 0x519BA24e959E33b3B6220CA98bd353d8c2D89920 0x519BA24e959E33b3B6220CA98bd353d8c2D89920 0x519BA24e959E33b3B6220CA98bd353d8c2D89920 0x519BA24e959E33b3B6220CA98bd353d8c2D89920 0x519BA24e959E33b3B6220CA98bd353d8c2D89920 0x519BA24e959E33b3B6220CA98bd353d8c2D89920
TradeAboveThreshold 0x812308712a6d1367f437e1c1e4af85c854e1e9f6 0x812308712a6d1367f437e1c1e4af85c854e1e9f6 0x812308712a6d1367f437e1c1e4af85c854e1e9f6 0x812308712a6d1367f437e1c1e4af85c854e1e9f6 0x812308712a6d1367f437e1c1e4af85c854e1e9f6 0x812308712a6d1367f437e1c1e4af85c854e1e9f6 0x812308712a6d1367f437e1c1e4af85c854e1e9f6 0x812308712a6d1367f437e1c1e4af85c854e1e9f6 0x812308712a6d1367f437e1c1e4af85c854e1e9f6 0x812308712a6d1367f437e1c1e4af85c854e1e9f6
StopLoss 0x412c36e5011cd2517016d243a2dfb37f73a242e7 0x412c36e5011cd2517016d243a2dfb37f73a242e7 0x412c36e5011cd2517016d243a2dfb37f73a242e7 0x412c36e5011cd2517016d243a2dfb37f73a242e7 0x412c36e5011cd2517016d243a2dfb37f73a242e7 0x412c36e5011cd2517016d243a2dfb37f73a242e7 0x412c36e5011cd2517016d243a2dfb37f73a242e7 0x412c36e5011cd2517016d243a2dfb37f73a242e7 0x412c36e5011cd2517016d243a2dfb37f73a242e7 0x412c36e5011cd2517016d243a2dfb37f73a242e7
CurrentBlockTimestampFactory 0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc 0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc 0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc 0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc 0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc 0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc 0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc 0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc 0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc 0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc
ComposableCowPoller - 0x5Eda08425781c2C39A28fAAf963C79487dc91bb1 - - - - - - - -

Audits

The core of the framework — ComposableCoW and ExtensibleFallbackHandler — has been audited by:

Not every contract in the table above is covered by these reports. Several order types, and ComposableCowPoller, were added after they were written and are therefore outside their scope. Refer to the scope section of each report to check whether a given contract was reviewed.

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

Deploy the older contracts (legacy)

Older contract like `ComposableCoW can no longer be deployed to their official addresses by compiling this repository (see #93). These are all affected contracts:

ComposableCoW, CurrentBlockTimestampFactory, ExtensibleFallbackHandler, GoodAfterTime, PerpetualStableSwap, StopLoss, TWAP, TradeAboveThreshold.

Their recorded initcode is replayed instead:

bash dev/deploy-canonical.sh --rpc-url $ETH_RPC_URL                                # dry run
PRIVATE_KEY=... bash dev/deploy-canonical.sh --rpc-url $ETH_RPC_URL --broadcast

See canonical/ for how it works and what it covers.

Deploy newer contracts

Every contract not listed in the legacy section is deployed using the official foundry scripts.

To deploy all newer contracts in a single run:

source .env
forge script script/deploy_All.s.sol:DeployAll --rpc-url $ETH_RPC_URL --broadcast -vvvv --verify

By default it skips the legacy contracts and binds to their canonical addresses, deploying everything else. Pass CANONICAL=false to deploy them from source too: those addresses won't match the canonical ones, but they are still deterministic.

Contracts already on the chain are reported and skipped, so an interrupted run can be repeated.

Deploy a single contract

Each contract also has its own script, for when only one of them needs deploying:

forge script script/deploy_ComposableCoW.s.sol:DeployComposableCoW --rpc-url $ETH_RPC_URL --broadcast -vvvv --verify
forge script script/deploy_ExtensibleFallbackHandler.s.sol:DeployExtensibleFallbackHandler --rpc-url $ETH_RPC_URL --broadcast -vvvv --verify
forge script script/deploy_ValueFactories.s.sol:DeployValueFactories --rpc-url $ETH_RPC_URL --broadcast -vvvv --verify

ComposableCowPoller pins a ComposableCoW and the COWShedFactoryForComposableCoW, so it needs COMPOSABLE_COW and COW_SHED_FACTORY_FOR_COMPOSABLE_COW alongside SETTLEMENT, and checks each address holds the contract it claims:

SETTLEMENT=0x9008D19f58AAbD9eD0D60971565AA8510560ab41 \
  COMPOSABLE_COW=0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 \
  COW_SHED_FACTORY_FOR_COMPOSABLE_COW=0x5e284e80f3bd6a7d80a8500d9c49878028110848 \
  forge script script/deploy_ComposableCowPoller.s.sol:DeployComposableCowPoller --rpc-url $ETH_RPC_URL --broadcast -vvvv --verify

Updating networks.json

networks.json lists the address and creation transaction of the latest official deployments by chain.

It is generated automatically. After deploying, regenerate it with:

bash dev/generate-networks-file.sh > networks.json

The script figures out the official contract address from 2 sources:

  • broadcast/**/*.json: Reads the Foundry deployment artifacts.
  • broadcast/networks-manual.json: Some chains can't be deployed to with the scripts (see #93) and are deployed by hand, leaving no artifact behind. Record those in broadcast/networks-manual.json, which has the same shape as networks.json and is merged on top of the generated result.

Re-running the script on an unchanged repository must reproduce the committed file, so a quick way to check there´s no drift is to run:

diff -u networks.json <(bash dev/generate-networks-file.sh)

Only chain 31337 is gitignored under broadcast/, so broadcasting to a local node that impersonates a real chain id leaves artifacts that override the published addresses. Keep anvil on its default chain id, and run the diff above before committing anything under broadcast/.

Contract verification on block explorer

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.

foundry.toml sets bytecode_hash = "none", so the deployed bytecode carries no metadata: explorers still verify it, but Sourcify cannot record a full match.

Local deployment

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.

  1. 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.

  2. 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 to anvil. This is needed for the next step.

    NOTE: --verify is omitted as with local deployments, these should not be submitted to Etherscan for verification.

  3. 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

About

🐮 🧾 Composable Conditional Orders for CoW Protocol

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages