From b0fae5b2ca5b5dde7c7279284de5870fb959072d Mon Sep 17 00:00:00 2001 From: Federico Giacon <58218759+fedgiac@users.noreply.github.com> Date: Mon, 5 Feb 2024 08:35:49 +0000 Subject: [PATCH 1/2] Add minimum amount for creating amm order (#7) --- src/ConstantProduct.sol | 10 +++++++ src/interfaces/IPriceOracle.sol | 2 +- .../ConstantProductTestHarness.sol | 17 +++++++++-- .../ValidateOrderParametersTest.sol | 30 ++++++++++++++++++- .../verify/ValidateAmmMath.sol | 1 + 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/ConstantProduct.sol b/src/ConstantProduct.sol index f54600c..fbfbc35 100644 --- a/src/ConstantProduct.sol +++ b/src/ConstantProduct.sol @@ -30,6 +30,9 @@ contract ConstantProduct is IConditionalOrderGenerator { IERC20 token0; /// The second of the tokens traded by this AMM. IERC20 token1; + /// The minimum amount of token0 that needs to be traded for an order + /// to be created on getTradeableOrder. + uint256 minTradedToken0; /// An onchain source for the price of the two tokens. The price should /// be expressed in terms of amount of token0 per amount of token1. IPriceOracle priceOracle; @@ -99,6 +102,7 @@ contract ConstantProduct is IConditionalOrderGenerator { // isn't the AMM best price. uint256 selfReserve0TimesPriceDenominator = selfReserve0 * priceDenominator; uint256 selfReserve1TimesPriceNumerator = selfReserve1 * priceNumerator; + uint256 tradedAmountToken0; if (selfReserve1TimesPriceNumerator < selfReserve0TimesPriceDenominator) { sellToken = token0; buyToken = token1; @@ -109,6 +113,7 @@ contract ConstantProduct is IConditionalOrderGenerator { priceNumerator * selfReserve0, Math.Rounding.Up ); + tradedAmountToken0 = sellAmount; } else { sellToken = token1; buyToken = token0; @@ -119,6 +124,11 @@ contract ConstantProduct is IConditionalOrderGenerator { priceDenominator * selfReserve1, Math.Rounding.Up ); + tradedAmountToken0 = buyAmount; + } + + if (tradedAmountToken0 < data.minTradedToken0) { + revert IConditionalOrder.OrderNotValid("traded amount too small"); } order = GPv2Order.Data( diff --git a/src/interfaces/IPriceOracle.sol b/src/interfaces/IPriceOracle.sol index ae6b5f1..5f4acc2 100644 --- a/src/interfaces/IPriceOracle.sol +++ b/src/interfaces/IPriceOracle.sol @@ -24,7 +24,7 @@ interface IPriceOracle { * oracle implementation. For example, it could be a specific pool id for * balancer, or the address of a specific price feed for Chainlink. * We recommend this data be implemented as the abi-encoding of a dedicated - * data struct for ease of type-checking and decoding the input. + * data struct for ease of type-checking and decoding the input. * @return priceNumerator The numerator of the price, expressed in amount of * token1 per amount of token0. * @return priceDenominator The denominator of the price, expressed in diff --git a/test/ConstantProduct/ConstantProductTestHarness.sol b/test/ConstantProduct/ConstantProductTestHarness.sol index 6f939a0..ac48856 100644 --- a/test/ConstantProduct/ConstantProductTestHarness.sol +++ b/test/ConstantProduct/ConstantProductTestHarness.sol @@ -38,6 +38,7 @@ abstract contract ConstantProductTestHarness is BaseComposableCoWTest { return ConstantProduct.Data( IERC20(USDC), IERC20(WETH), + 0, uniswapV2PriceOracle, abi.encode(UniswapV2PriceOracle.Data(IUniswapV2Pair(DEFAULT_PAIR))), DEFAULT_APPDATA @@ -80,9 +81,8 @@ abstract contract ConstantProductTestHarness is BaseComposableCoWTest { } // This function calls `getTradeableOrder` while filling all unused - // parameters with arbitrary data. Since every tradeable order is supposed - // to be executable, it also immediately checks that the order is valid. - function getTradeableOrderWrapper(address owner, ConstantProduct.Data memory staticInput) + // parameters with arbitrary data. + function getTradeableOrderUncheckedWrapper(address owner, ConstantProduct.Data memory staticInput) internal view returns (GPv2Order.Data memory order) @@ -94,6 +94,17 @@ abstract contract ConstantProductTestHarness is BaseComposableCoWTest { abi.encode(staticInput), bytes("offchain input") ); + } + + // This function calls `getTradeableOrder` while filling all unused + // parameters with arbitrary data. It also immediately checks that the order + // is valid. + function getTradeableOrderWrapper(address owner, ConstantProduct.Data memory staticInput) + internal + view + returns (GPv2Order.Data memory order) + { + order = getTradeableOrderUncheckedWrapper(owner, staticInput); verifyWrapper(owner, staticInput, order); } diff --git a/test/ConstantProduct/getTradeableOrder/ValidateOrderParametersTest.sol b/test/ConstantProduct/getTradeableOrder/ValidateOrderParametersTest.sol index 741c4cf..bdbb509 100644 --- a/test/ConstantProduct/getTradeableOrder/ValidateOrderParametersTest.sol +++ b/test/ConstantProduct/getTradeableOrder/ValidateOrderParametersTest.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.13; import {ConstantProductTestHarness} from "../ConstantProductTestHarness.sol"; -import {ConstantProduct, GPv2Order} from "../../../src/ConstantProduct.sol"; +import {ConstantProduct, GPv2Order, IConditionalOrder} from "../../../src/ConstantProduct.sol"; abstract contract ValidateOrderParametersTest is ConstantProductTestHarness { function testValidOrderParameters() public { @@ -38,4 +38,32 @@ abstract contract ValidateOrderParametersTest is ConstantProductTestHarness { order = getTradeableOrderWrapper(orderOwner, defaultData); assertEq(order.validTo, 2 * constantProduct.MAX_ORDER_DURATION()); } + + function testRevertsIfAmountTooLowOnSellToken() public { + ConstantProduct.Data memory defaultData = setUpDefaultData(); + setUpDefaultReserves(orderOwner); + setUpDefaultReferencePairReserves(42, 1337); + + GPv2Order.Data memory order = getTradeableOrderWrapper(orderOwner, defaultData); + require(order.sellToken == defaultData.token0, "test was design for token0 to be the sell token"); + defaultData.minTradedToken0 = order.sellAmount; + order = getTradeableOrderWrapper(orderOwner, defaultData); + defaultData.minTradedToken0 = order.sellAmount + 1; + vm.expectRevert(abi.encodeWithSelector(IConditionalOrder.OrderNotValid.selector, "traded amount too small")); + getTradeableOrderUncheckedWrapper(orderOwner, defaultData); + } + + function testRevertsIfAmountTooLowOnBuyToken() public { + ConstantProduct.Data memory defaultData = setUpDefaultData(); + setUpDefaultReserves(orderOwner); + setUpDefaultReferencePairReserves(1337, 42); + + GPv2Order.Data memory order = getTradeableOrderWrapper(orderOwner, defaultData); + require(order.buyToken == defaultData.token0, "test was design for token0 to be the buy token"); + defaultData.minTradedToken0 = order.buyAmount; + order = getTradeableOrderWrapper(orderOwner, defaultData); + defaultData.minTradedToken0 = order.buyAmount + 1; + vm.expectRevert(abi.encodeWithSelector(IConditionalOrder.OrderNotValid.selector, "traded amount too small")); + getTradeableOrderUncheckedWrapper(orderOwner, defaultData); + } } diff --git a/test/ConstantProduct/verify/ValidateAmmMath.sol b/test/ConstantProduct/verify/ValidateAmmMath.sol index 63001b9..7970698 100644 --- a/test/ConstantProduct/verify/ValidateAmmMath.sol +++ b/test/ConstantProduct/verify/ValidateAmmMath.sol @@ -40,6 +40,7 @@ abstract contract ValidateAmmMath is ConstantProductTestHarness { data = ConstantProduct.Data( order.sellToken, order.buyToken, + 0, uniswapV2PriceOracle, abi.encode(abi.encode(UniswapV2PriceOracle.Data(pair))), order.appData From 411ec2298433f956642357c6e17330875930976c Mon Sep 17 00:00:00 2001 From: Federico Giacon <58218759+fedgiac@users.noreply.github.com> Date: Mon, 5 Feb 2024 09:34:17 +0000 Subject: [PATCH 2/2] Add docs on CoW AMM (#8) * Add docs on CoW AMM * Typos and style changes from code review Co-authored-by: mfw78 <53399572+mfw78@users.noreply.github.com> * Fix remaining composable CoW -> `ComposableCoW` --------- Co-authored-by: mfw78 <53399572+mfw78@users.noreply.github.com> --- .gitignore | 3 -- README.md | 51 +++++---------------------- docs/amm.md | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 45 deletions(-) create mode 100644 docs/amm.md diff --git a/.gitignore b/.gitignore index 85198aa..3269660 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,5 @@ out/ /broadcast/*/31337/ /broadcast/**/dry-run/ -# Docs -docs/ - # Dotenv file .env diff --git a/README.md b/README.md index 9265b45..b3ddb96 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,18 @@ -## Foundry +## CoW AMM -**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** +CoW AMM is an automated market maker running on top of CoW Protocol. -Foundry consists of: +## Documentation -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. +You can find detailed documentation on the building blocks of this repo in the following files: -## Documentation +- [amm.md](./docs/amm.md): details on what a CoW AMM is and how to set it up. + +## Research -https://book.getfoundry.sh/ +Details on the theory behind CoW AMM can be found on the paper [Arbitrageurs' profits, LVR, and sandwich attacks: batch trading as an AMM design response](https://arxiv.org/pdf/2307.02074.pdf). -## Usage +## Development ### Build @@ -32,35 +31,3 @@ $ forge test ```shell $ forge fmt ``` - -### Gas Snapshots - -```shell -$ forge snapshot -``` - -### Anvil - -```shell -$ anvil -``` - -### Deploy - -```shell -$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key -``` - -### Cast - -```shell -$ cast -``` - -### Help - -```shell -$ forge --help -$ anvil --help -$ cast --help -``` diff --git a/docs/amm.md b/docs/amm.md new file mode 100644 index 0000000..c7696e4 --- /dev/null +++ b/docs/amm.md @@ -0,0 +1,100 @@ +# CoW AMM + +CoW AMM is an automated market maker running on top of CoW Protocol. + +## How it works + +The AMM contract itself is a dedicated Safe multisig. +It stores reserves of two tokens and allows anyone to create orders between these two tokens on CoW Protocol as long as the trade doesn't decrease the product of the reserves stored on the contract. + +The order is based on the `ComposableCoW` framework: this repository only specifies the code needed for the dedicated handler and the related price oracles. +All the code for the `ComposableCoW` framework is [here](https://github.com/cowprotocol/composable-cow). + +The AMM also provides a view function (`getTradeableOrder`): this function doesn't impact which order is valid or not, but returns an order that can be traded in a way that tries to rebalance the AMM to align with the reference price of a price oracle. + +This order is intended to be picked up automatically by the [watchtower](https://github.com/cowprotocol/watch-tower) without any necessity for the AMM to interact with the CoW Protocol API. +For now, most solvers rely on the order that's automatically generated by the watchtower to balance the AMM. +Eventually, we expect solvers to be aware of the peculiar dynamics of the CoW AMM and create their own orders when settling a CoW Protocol batch. + +CoW AMM orders are executed in batches by CoW Protocol solvers. +Only one order per AMM is intended to be executed per batch. +This is currently not enforced on-chain but it's a planned feature of a future version of the CoW AMM. + +Further information on the theoretical research work that serves as the base of CoW AMM can be found in the paper [Arbitrageurs' profits, LVR, and sandwich attacks: batch trading as an AMM design response](https://arxiv.org/pdf/2307.02074.pdf). + +Batch trading guarantees that despite the minimum viable order follows the constant-product curve (as it's the case for Uniswap v2) the surplus captured by CoW Protocol yields to a better execution price and thus higher profits for liquidity providers. + +## Requirements + +- The CoW AMM funds will be stored in a safe. Every pair of tokens requires its dedicated safe. The owners of the safe have full access to the funds. +- The CoW AMM safe needs to use the safe's `ExtensibleFallbackHandler` fallback handler (needed to validate a `ComposableCoW` order). +- The CoW AMM safe needs to create a new [`ComposableCoW`](https://github.com/cowprotocol/composable-cow) order that uses the standard deployment of `ConstantProduct` as its handler. + +## Order setup + +### `ComposableCoW` order + +A `ComposableCoW` order is created by calling the function `create(ConditionalOrderParams params, bool dispatch)` on the `ComposableCoW` main contract. + +The conditional order parameters should be set as follows: +- `IConditionalOrder handler`: the address of the standard deployment of `ConstantProduct` for the desired chain. See file `networks.json` for a list of official deployments by chain id. +- `bytes32 salt`: this value is used to make the order unique. It's recommended to use a value that hasn't been used before for an order on the same safe. Note that it's discouraged to use the CoW AMM safe for other orders outside of setting up the AMM, in which case conflicts are not a concern. +- `bytes staticInput`: The configuration values for the CoW AMM. See next section for more details. + +If `dispatch` is set to true, then the order will be automatically picked up on CoW Protocol's orderbook by the [watchtower service](https://github.com/cowprotocol/watch-tower). + +### ConstantProduct static input + +The static input of the constant product handler comprises the following parameters: + +- `IERC20 token0`: the first of the two tokens traded by the AMM. +- `IERC20 token1`: the second of the two tokens traded by the AMM. +- `uint256 minTradedToken0`: the minimum amount of token0 that needs to be traded for an order to be returned by `getTradeableOrder`. Order with lower traded amount can still be created manually. +- `IPriceOracle priceOracle`: the address of a contract that implements the generic price oracle interface. + See the section below for more information on which price oracles are available. +- `bytes priceOracleData`: the extra oracle information needed to recover the price. + See the section below for more information on how to set this value based on the chosen price oracle. +- `bytes32 appData`: The app data (as defined in a CoW Protocol order) that must be used for the order to be valid. + +If Foundry is available in your system, you can generate the bytes calldata with the following command: +```sh +token0=0x1111111111111111111111111111111111111111 +token1=0x2222222222222222222222222222222222222222 +minTradedToken0=31337 +priceOracle=0x1337133713371337133713371337133713371337 +priceOracleData=0xca11d47a +appData=0x3232323232323232323232323232323232323232323232323232323232323232 +cast abi-encode 'f(address,address,uint256,address,bytes,bytes32)' "$token0" "$token1" "$minTradedToken0" "$priceOracle" "$priceOracleData" "$appData" +``` + +### Supported price oracles + +Price oracles are an abstraction that transform disparate on-chain price information into a standardized price source that can be used by the `ConstantFroduct` to retrieve token price information. + +We support the following price oracles: +- `UniswapV2PriceOracle`, based on the limit price of a predefined Uniswap v2 pair. + +Contract addresses for each supported chain can be found in the file `networks.json`. + +#### UniswapV2PriceOracle + +The Uniswap v2 price oracle returns the limit price that can be computed from an address (pool) that supports the `IUniswapV2Pair` interface. +The oracle data needs to include a single parameter: +- `IUniswapV2Pair referencePair`: the address of a Uniswap pool for the tokens `token0` and `token1`. + +Note that the order of the tokens does _not_ need to be consistent with the order of the tokens in the pool. +The order of the tokens in the constant product static input determines that the price is expressed in terms of amount of token0 per amount of token1. +If the tokens are not the same as those traded on the chosen reference pair, no order will be created. + +If Foundry is available in your system, you can generate the bytes calldata with the following command: +```sh +referencePair=0x1111111111111111111111111111111111111111 +cast abi-encode 'f(address)' "$referencePair" +``` + +## Risk profile + +The risks for the funds on the AMM are comparable to the risks of depositing the same reserves on a constant-product curve like Uniswap v2. + +The AMM relies on price oracle exclusively for generating orders that will plausibly be settled in the current market conditions, but they aren't used to determine whether an order is valid. +If a price oracle is compromised or manipulated, the main risk is that the liquidity available on CoW protocol will be used suboptimally by the solvers that aren't aware of the custom semantics of a CoW AMM.