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