Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement verify function #1

Merged
merged 9 commits into from
Jan 31, 2024
Merged
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("receiver must be zero address");
}
// 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("validity too far in the future");
}
if (order.appData != data.appData) {
revert IConditionalOrder.OrderNotValid("invalid appData");
}
if (order.feeAmount != 0) {
revert IConditionalOrder.OrderNotValid("fee amount must be zero");
}
if (order.buyTokenBalance != GPv2Order.BALANCE_ERC20) {
revert IConditionalOrder.OrderNotValid("buyTokenBalance must be erc20");
}
if (order.sellTokenBalance != GPv2Order.BALANCE_ERC20) {
revert IConditionalOrder.OrderNotValid("sellTokenBalance must be erc20");
}
// These are the checks needed to satisfy the conditions on in/out
// amounts for a constant-product curve AMM.
if ((sellReserve - 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 {
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"));
IUniswapV2Pair pair = IUniswapV2Pair(DEFAULT_PAIR);
require(pair.token0() != pair.token1(), "Pair setup failed: should use distinct tokens");
}

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

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 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 {}
Loading