-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
506 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))))); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.