Skip to content

Commit

Permalink
Merge branch 'main' into automatic-formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
fedgiac committed Feb 5, 2024
2 parents 595b2f8 + 411ec22 commit d2bcbc6
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 53 deletions.
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,5 @@ out/
/broadcast/*/31337/
/broadcast/**/dry-run/

# Docs
docs/

# Dotenv file
.env
59 changes: 13 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,71 +1,38 @@
## Foundry
## CoW AMM

**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**

Foundry consists of:

- **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.
CoW AMM is an automated market maker running on top of CoW Protocol.

## Documentation

https://book.getfoundry.sh/

## Usage

### Build
You can find detailed documentation on the building blocks of this repo in the following files:

```shell
$ forge build
```
- [amm.md](./docs/amm.md): details on what a CoW AMM is and how to set it up.

### Test
## Research

```shell
$ forge test
```
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).

### Format

```shell
$ forge fmt
```
## Development

### Dev set up

You can install git hooks to help you catch simple mistakes before running some git actions like committing.
See the [dedicated instructions](./dev/hooks/install.md) for how to install the hooks.

### Gas Snapshots

```shell
$ forge snapshot
```

### Anvil

```shell
$ anvil
```

### Deploy
### Build

```shell
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
$ forge build
```

### Cast
### Test

```shell
$ cast <subcommand>
$ forge test
```

### Help
### Format

```shell
$ forge --help
$ anvil --help
$ cast --help
$ forge fmt
```
100 changes: 100 additions & 0 deletions docs/amm.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 10 additions & 0 deletions src/ConstantProduct.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -109,6 +113,7 @@ contract ConstantProduct is IConditionalOrderGenerator {
priceNumerator * selfReserve0,
Math.Rounding.Up
);
tradedAmountToken0 = sellAmount;
} else {
sellToken = token1;
buyToken = token0;
Expand All @@ -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(
Expand Down
17 changes: 14 additions & 3 deletions test/ConstantProduct/ConstantProductTestHarness.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}
1 change: 1 addition & 0 deletions test/ConstantProduct/verify/ValidateAmmMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit d2bcbc6

Please sign in to comment.