Skip to content

Commit

Permalink
Implement verify function
Browse files Browse the repository at this point in the history
  • Loading branch information
fedgiac committed Jan 25, 2024
1 parent d975d21 commit 6b2fba0
Show file tree
Hide file tree
Showing 6 changed files with 506 additions and 4 deletions.
131 changes: 129 additions & 2 deletions src/ConstantProduct.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,131 @@
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

contract ConstantProduct {}
import {IERC20} from "lib/composable-cow/lib/@openzeppelin/contracts/interfaces/IERC20.sol";
import {IUniswapV2Pair} from "lib/uniswap-v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import {ConditionalOrdersUtilsLib as Utils} from "lib/composable-cow/src/types/ConditionalOrdersUtilsLib.sol";
import {
IConditionalOrderGenerator,
IConditionalOrder,
IERC165,
GPv2Order
} from "lib/composable-cow/src/BaseConditionalOrder.sol";

/**
* @title CoW AMM
* @author CoW Protocol Developers
* @dev Automated market maker based on the concept of function-maximising AMMs.
* It relies on the CoW Protocol infrastructure to guarantee batch execution of
* its orders.
* Order creation and execution is based on the Composable CoW base contracts.
*/
contract ConstantProduct is IConditionalOrderGenerator {
uint32 public constant MAX_ORDER_DURATION = 5 * 60;

/// All data used by an order to validate the AMM conditions.
struct Data {
/// A Uniswap v2 pair. This is used to determine the tokens traded by
/// the AMM, and also use to establish the reference price used when
/// computing a valid tradable order.
IUniswapV2Pair referencePair;
/// The app data that must be used in the order.
/// See `GPv2Order.Data` for more information on the app data.
bytes32 appData;
}

/**
* @inheritdoc IConditionalOrderGenerator
*/
function getTradeableOrder(address owner, address, bytes32, bytes calldata staticInput, bytes calldata)
public
view
override
returns (GPv2Order.Data memory order)
{
revert("unimplemented");
}

/**
* @inheritdoc IConditionalOrder
* @dev Most parameters are ignored: we only need to validate the order with
* the current reserves and the validated order parameters.
*/
function verify(
address owner,
address,
bytes32,
bytes32,
bytes32,
bytes calldata staticInput,
bytes calldata,
GPv2Order.Data calldata order
) external view override {
_verify(owner, staticInput, order);
}

/**
* @dev Wrapper for the `verify` function with only the parameters that are
* required for verification. Compared to implementing the logic inside
* `verify`, it frees up some stack slots and reduces "stack too deep"
* issues.
* @param owner the contract who is the owner of the order
* @param staticInput the static input for all discrete orders cut from this
* conditional order
* @param order `GPv2Order.Data` of a discrete order to be verified.
*/
function _verify(address owner, bytes calldata staticInput, GPv2Order.Data calldata order) internal view {
ConstantProduct.Data memory data = abi.decode(staticInput, (Data));

IERC20 sellToken = IERC20(data.referencePair.token0());
IERC20 buyToken = IERC20(data.referencePair.token1());
uint256 sellReserve = sellToken.balanceOf(owner);
uint256 buyReserve = buyToken.balanceOf(owner);
if (order.sellToken != sellToken) {
if (order.sellToken != buyToken) {
revert IConditionalOrder.OrderNotValid("invalid sell token");
}
(sellToken, buyToken) = (buyToken, sellToken);
(sellReserve, buyReserve) = (buyReserve, sellReserve);
}
if (order.buyToken != buyToken) {
revert IConditionalOrder.OrderNotValid("invalid buy token");
}

if (order.receiver != GPv2Order.RECEIVER_SAME_AS_OWNER) {
revert IConditionalOrder.OrderNotValid("invalid receiver");
}
// We add a maximum duration to avoid spamming the orderbook and force
// an order refresh if the order is old.
if (order.validTo > block.timestamp + MAX_ORDER_DURATION) {
revert IConditionalOrder.OrderNotValid("invalid validTo");
}
if (order.appData != data.appData) {
revert IConditionalOrder.OrderNotValid("invalid appData");
}
if (order.feeAmount != 0) {
revert IConditionalOrder.OrderNotValid("invalid feeAmount");
}
if (order.buyTokenBalance != GPv2Order.BALANCE_ERC20) {
revert IConditionalOrder.OrderNotValid("invalid buyTokenBalance");
}
if (order.sellTokenBalance != GPv2Order.BALANCE_ERC20) {
revert IConditionalOrder.OrderNotValid("invalid sellTokenBalance");
}
// These are the checks needed to satisfy the conditions on in/out
// amounts for the function-maximising AMM.
if ((sellReserve - 2 * order.sellAmount) * order.buyAmount < buyReserve * order.sellAmount) {
revert IConditionalOrder.OrderNotValid("received amount too low");
}

// No checks on:
//bytes32 kind;
//bool partiallyFillable;
}

/**
* @inheritdoc IERC165
*/
function supportsInterface(bytes4 interfaceId) external view virtual override returns (bool) {
return interfaceId == type(IConditionalOrderGenerator).interfaceId || interfaceId == type(IERC165).interfaceId;
}
}
6 changes: 4 additions & 2 deletions test/ConstantProduct.t.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;

contract E2eCounterTest {}
import {VerifyTest} from "./ConstantProduct/VerifyTest.sol";

contract ConstantProductTest is VerifyTest {}
96 changes: 96 additions & 0 deletions test/ConstantProduct/ConstantProductTestHarness.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;

import {BaseComposableCoWTest} from "lib/composable-cow/test/ComposableCoW.base.t.sol";

import {ConstantProduct, GPv2Order, IUniswapV2Pair, IERC20} from "../../src/ConstantProduct.sol";

abstract contract ConstantProductTestHarness is BaseComposableCoWTest {
ConstantProduct constantProduct;
address internal orderOwner = addressFromString("order owner");

address private USDC = addressFromString("USDC");
address private WETH = addressFromString("WETH");
address private DEFAULT_PAIR = addressFromString("default USDC/WETH pair");
address private DEFAULT_RECEIVER = addressFromString("default receiver");
bytes32 private DEFAULT_APPDATA = keccak256(bytes("unit test"));

function setUp() public virtual override(BaseComposableCoWTest) {
super.setUp();

constantProduct = new ConstantProduct();
}

function setUpDefaultPair() internal returns (IUniswapV2Pair pair) {
vm.mockCall(DEFAULT_PAIR, abi.encodeWithSelector(IUniswapV2Pair.token0.selector), abi.encode(USDC));
vm.mockCall(DEFAULT_PAIR, abi.encodeWithSelector(IUniswapV2Pair.token1.selector), abi.encode(WETH));
// Reverts for everything else
vm.mockCallRevert(DEFAULT_PAIR, hex"", abi.encode("Called unexpected function on mock pair"));
pair = IUniswapV2Pair(DEFAULT_PAIR);
require(pair.token0() != pair.token1(), "Pair setup failed: should use distinct tokens");
}

function setUpDefaultData() internal returns (ConstantProduct.Data memory) {
setUpDefaultPair();
return getDefaultData();
}

function setUpDefaultReserves(address owner) internal {
ConstantProduct.Data memory defaultData = setUpDefaultData();
vm.mockCall(
defaultData.referencePair.token0(),
abi.encodeWithSelector(IERC20.balanceOf.selector, owner),
abi.encode(1337)
);
vm.mockCall(
defaultData.referencePair.token1(),
abi.encodeWithSelector(IERC20.balanceOf.selector, owner),
abi.encode(1337)
);
}

// This function calls `verify` while filling all unused parameters with
// arbitrary data.
function verifyWrapper(address owner, ConstantProduct.Data memory staticInput, GPv2Order.Data memory order)
internal
view
{
constantProduct.verify(
owner,
addressFromString("sender"),
keccak256(bytes("order hash")),
keccak256(bytes("domain separator")),
keccak256(bytes("context")),
abi.encode(staticInput),
bytes("offchain input"),
order
);
}

function getDefaultData() internal view returns (ConstantProduct.Data memory) {
return ConstantProduct.Data(IUniswapV2Pair(DEFAULT_PAIR), DEFAULT_APPDATA);
}

function getDefaultOrder() internal view returns (GPv2Order.Data memory) {
ConstantProduct.Data memory data = getDefaultData();

return GPv2Order.Data(
IERC20(USDC), // IERC20 sellToken;
IERC20(WETH), // IERC20 buyToken;
GPv2Order.RECEIVER_SAME_AS_OWNER, // address receiver;
0, // uint256 sellAmount;
0, // uint256 buyAmount;
uint32(block.timestamp) + constantProduct.MAX_ORDER_DURATION() / 2, // uint32 validTo;
data.appData, // bytes32 appData;
0, // uint256 feeAmount;
GPv2Order.KIND_SELL, // bytes32 kind;
true, // bool partiallyFillable;
GPv2Order.BALANCE_ERC20, // bytes32 sellTokenBalance;
GPv2Order.BALANCE_ERC20 // bytes32 buyTokenBalance;
);
}

function addressFromString(string memory s) internal pure returns (address) {
return address(uint160(uint256(keccak256(bytes(s)))));
}
}
7 changes: 7 additions & 0 deletions test/ConstantProduct/VerifyTest.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;

import {ValidateOrderParametersTest} from "./verify/ValidateOrderParametersTest.sol";
import {ValidateAmmMath} from "./verify/ValidateAmmMath.sol";

abstract contract VerifyTest is ValidateOrderParametersTest, ValidateAmmMath {}
137 changes: 137 additions & 0 deletions test/ConstantProduct/verify/ValidateAmmMath.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;

import {ConstantProductTestHarness} from "../ConstantProductTestHarness.sol";
import {ConstantProduct, GPv2Order, IUniswapV2Pair, IERC20, IConditionalOrder} from "../../../src/ConstantProduct.sol";

abstract contract ValidateAmmMath is ConstantProductTestHarness {
IUniswapV2Pair pair = IUniswapV2Pair(addressFromString("pair for math verification"));

function setUpAmmWithReserves(uint256 amountToken0, uint256 amountToken1) internal {
IERC20 token0 = IERC20(addressFromString("token0 for math verification"));
IERC20 token1 = IERC20(addressFromString("token1 for math verification"));
vm.mockCall(address(pair), abi.encodeWithSelector(IUniswapV2Pair.token0.selector), abi.encode(token0));
vm.mockCall(address(pair), abi.encodeWithSelector(IUniswapV2Pair.token1.selector), abi.encode(token1));
// Reverts for everything else
vm.mockCallRevert(address(pair), hex"", abi.encode("Called unexpected function on mock pair"));
require(pair.token0() != pair.token1(), "Pair setup failed: should use distinct tokens");

vm.mockCall(
address(token0), abi.encodeWithSelector(IERC20.balanceOf.selector, orderOwner), abi.encode(amountToken0)
);
vm.mockCall(
address(token1), abi.encodeWithSelector(IERC20.balanceOf.selector, orderOwner), abi.encode(amountToken1)
);
}

function setUpOrderWithReserves(uint256 amountToken0, uint256 amountToken1)
internal
returns (ConstantProduct.Data memory data, GPv2Order.Data memory order)
{
setUpAmmWithReserves(amountToken0, amountToken1);
order = getDefaultOrder();
order.sellToken = IERC20(pair.token0());
order.buyToken = IERC20(pair.token1());
order.sellAmount = 0;
order.buyAmount = 0;

data = ConstantProduct.Data(pair, order.appData);
}

// Note: if X is the reserve of the tokens that is taken from the AMM, and
// Y the reserve of the token that is deposited into the AMM, then given
// any in amount x you can compute the out amount as:
// Y * x
// y = ---------
// X - 2x

function testExactAmountsInOut() public {
uint256 poolOut = 1000 ether;
uint256 poolIn = 10 ether;
(ConstantProduct.Data memory data, GPv2Order.Data memory order) = setUpOrderWithReserves(poolOut, poolIn);

uint256 amountOut = 100 ether;
uint256 amountIn = poolIn * amountOut / (poolOut - 2 * amountOut);
order.sellAmount = amountOut;
order.buyAmount = amountIn;

verifyWrapper(orderOwner, data, order);

// The next line is there so that we can see at a glance that the out
// amount is reasonable given the in amount, since the math could be
// hiding the fact that the AMM leads to bad orders.
require(amountIn == 1.25 ether);
}

function testOneTooMuchOut() public {
uint256 poolOut = 1000 ether;
uint256 poolIn = 10 ether;
(ConstantProduct.Data memory data, GPv2Order.Data memory order) = setUpOrderWithReserves(poolOut, poolIn);

uint256 amountOut = 100 ether;
uint256 amountIn = poolIn * amountOut / (poolOut - 2 * amountOut);
order.sellAmount = amountOut + 1;
order.buyAmount = amountIn;

vm.expectRevert(abi.encodeWithSelector(IConditionalOrder.OrderNotValid.selector, "received amount too low"));
verifyWrapper(orderOwner, data, order);
}

function testOneTooLittleIn() public {
uint256 poolOut = 1000 ether;
uint256 poolIn = 10 ether;
(ConstantProduct.Data memory data, GPv2Order.Data memory order) = setUpOrderWithReserves(poolOut, poolIn);

uint256 amountOut = 100 ether;
uint256 amountIn = poolIn * amountOut / (poolOut - 2 * amountOut);
order.sellAmount = amountOut;
order.buyAmount = amountIn - 1;

vm.expectRevert(abi.encodeWithSelector(IConditionalOrder.OrderNotValid.selector, "received amount too low"));
verifyWrapper(orderOwner, data, order);
}

function testInvertInOutToken() public {
uint256 poolOut = 1000 ether;
uint256 poolIn = 10 ether;
(ConstantProduct.Data memory data, GPv2Order.Data memory order) = setUpOrderWithReserves(poolIn, poolOut);

uint256 amountOut = 100 ether;
uint256 amountIn = poolIn * amountOut / (poolOut - 2 * amountOut);
(order.sellToken, order.buyToken) = (order.buyToken, order.sellToken);
order.sellAmount = amountOut;
order.buyAmount = amountIn;

verifyWrapper(orderOwner, data, order);
}

function testInvertedTokenOneTooMuchOut() public {
uint256 poolOut = 1000 ether;
uint256 poolIn = 10 ether;
(ConstantProduct.Data memory data, GPv2Order.Data memory order) = setUpOrderWithReserves(poolIn, poolOut);

uint256 amountOut = 100 ether;
uint256 amountIn = poolIn * amountOut / (poolOut - 2 * amountOut);
(order.sellToken, order.buyToken) = (order.buyToken, order.sellToken);
order.sellAmount = amountOut + 1;
order.buyAmount = amountIn;

vm.expectRevert(abi.encodeWithSelector(IConditionalOrder.OrderNotValid.selector, "received amount too low"));
verifyWrapper(orderOwner, data, order);
}

function testInvertedTokensOneTooLittleIn() public {
uint256 poolOut = 1000 ether;
uint256 poolIn = 10 ether;
(ConstantProduct.Data memory data, GPv2Order.Data memory order) = setUpOrderWithReserves(poolIn, poolOut);

uint256 amountOut = 100 ether;
uint256 amountIn = poolIn * amountOut / (poolOut - 2 * amountOut);
(order.sellToken, order.buyToken) = (order.buyToken, order.sellToken);
order.sellAmount = amountOut;
order.buyAmount = amountIn - 1;

vm.expectRevert(abi.encodeWithSelector(IConditionalOrder.OrderNotValid.selector, "received amount too low"));
verifyWrapper(orderOwner, data, order);
}
}
Loading

0 comments on commit 6b2fba0

Please sign in to comment.