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

Account for subtraction underflow in order creation #14

Merged
merged 2 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions src/ConstantProduct.sol
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ contract ConstantProduct is IConditionalOrderGenerator {
if (selfReserve1TimesPriceNumerator < selfReserve0TimesPriceDenominator) {
sellToken = token0;
buyToken = token1;
sellAmount = selfReserve0 / 2 - Math.ceilDiv(selfReserve1TimesPriceNumerator, 2 * priceDenominator);
sellAmount = sub(selfReserve0 / 2, Math.ceilDiv(selfReserve1TimesPriceNumerator, 2 * priceDenominator));
buyAmount = Math.mulDiv(
sellAmount,
selfReserve1TimesPriceNumerator + (priceDenominator * sellAmount),
Expand All @@ -118,7 +118,7 @@ contract ConstantProduct is IConditionalOrderGenerator {
} else {
sellToken = token1;
buyToken = token0;
sellAmount = selfReserve1 / 2 - Math.ceilDiv(selfReserve0TimesPriceDenominator, 2 * priceNumerator);
sellAmount = sub(selfReserve1 / 2, Math.ceilDiv(selfReserve0TimesPriceDenominator, 2 * priceNumerator));
buyAmount = Math.mulDiv(
sellAmount,
selfReserve0TimesPriceDenominator + (priceNumerator * sellAmount),
Expand All @@ -129,9 +129,7 @@ contract ConstantProduct is IConditionalOrderGenerator {
}

if (tradedAmountToken0 < data.minTradedToken0) {
revert IWatchtowerCustomErrors.PollTryAtEpoch(
Utils.validToBucket(MAX_ORDER_DURATION) + 1, "traded amount too small"
);
revertPollAtNextBucket("traded amount too small");
}

order = GPv2Order.Data(
Expand Down Expand Up @@ -233,4 +231,32 @@ contract ConstantProduct is IConditionalOrderGenerator {
function supportsInterface(bytes4 interfaceId) external view virtual override returns (bool) {
return interfaceId == type(IConditionalOrderGenerator).interfaceId || interfaceId == type(IERC165).interfaceId;
}

/**
* @dev Computes the difference between the two input values. If it detects
* an underflow, the function reverts with a custom error that informs the
* watchtower to poll next.
* If the function reverted with a standard underflow, the watchtower would
* stop polling the order.
* @param lhs The minuend of the subtraction
* @param rhs The subtrahend of the subtraction
* @return The difference of the two input values
*/
function sub(uint256 lhs, uint256 rhs) internal view returns (uint256) {
if (lhs < rhs) {
revertPollAtNextBucket("subtraction underflow");
fedgiac marked this conversation as resolved.
Show resolved Hide resolved
}
unchecked {
return lhs - rhs;
}
}

/**
* @dev Reverts call execution with a custom error that indicates to the
* watchtower to poll for new order at the start of the next validity
* bucket.
*/
function revertPollAtNextBucket(string memory message) internal view {
revert IWatchtowerCustomErrors.PollTryAtEpoch(Utils.validToBucket(MAX_ORDER_DURATION) + 1, message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {IWatchtowerCustomErrors} from "src/interfaces/IWatchtowerCustomErrors.so

abstract contract ValidateOrderParametersTest is ConstantProductTestHarness {
function testValidOrderParameters() public {
ConstantProduct.Data memory defaultData = setUpDefaultData();
ConstantProduct.Data memory defaultData = getDefaultData();
setUpDefaultReserves(orderOwner);
setUpDefaultReferencePairReserves(42, 1337);

Expand All @@ -24,7 +24,7 @@ abstract contract ValidateOrderParametersTest is ConstantProductTestHarness {
}

function testOrderValidityMovesToNextBucket() public {
ConstantProduct.Data memory defaultData = setUpDefaultData();
ConstantProduct.Data memory defaultData = getDefaultData();
setUpDefaultReserves(orderOwner);
setUpDefaultReferencePairReserves(42, 1337);

Expand All @@ -41,19 +41,11 @@ abstract contract ValidateOrderParametersTest is ConstantProductTestHarness {
}

function testRevertsIfAmountTooLowOnSellToken() public {
ConstantProduct.Data memory defaultData = setUpDefaultData();
ConstantProduct.Data memory defaultData = getDefaultData();
setUpDefaultReserves(orderOwner);
setUpDefaultReferencePairReserves(42, 1337);

uint256 smallOffset = 42;
require(smallOffset < constantProduct.MAX_ORDER_DURATION());
uint256 nextTimestamp = 1337 * constantProduct.MAX_ORDER_DURATION() + smallOffset;
uint256 nextBucket = 1338 * constantProduct.MAX_ORDER_DURATION();
require(
nextTimestamp % constantProduct.MAX_ORDER_DURATION() != 0,
"test was designed so that the timestamp doesn't fall exactly at the start of a bucket, please change the offset"
);
vm.warp(nextTimestamp);
uint256 nextBucket = moveTimeToMidFutureBucket();

GPv2Order.Data memory order = getTradeableOrderWrapper(orderOwner, defaultData);
require(order.sellToken == defaultData.token0, "test was design for token0 to be the sell token");
Expand All @@ -69,19 +61,11 @@ abstract contract ValidateOrderParametersTest is ConstantProductTestHarness {
}

function testRevertsIfAmountTooLowOnBuyToken() public {
ConstantProduct.Data memory defaultData = setUpDefaultData();
ConstantProduct.Data memory defaultData = getDefaultData();
setUpDefaultReserves(orderOwner);
setUpDefaultReferencePairReserves(1337, 42);

uint256 smallOffset = 42;
require(smallOffset < constantProduct.MAX_ORDER_DURATION());
uint256 nextTimestamp = 1337 * constantProduct.MAX_ORDER_DURATION() + smallOffset;
uint256 nextBucket = 1338 * constantProduct.MAX_ORDER_DURATION();
require(
nextTimestamp % constantProduct.MAX_ORDER_DURATION() != 0,
"test was designed so that the timestamp doesn't fall exactly at the start of a bucket, please change the offset"
);
vm.warp(nextTimestamp);
uint256 nextBucket = moveTimeToMidFutureBucket();

GPv2Order.Data memory order = getTradeableOrderWrapper(orderOwner, defaultData);
require(order.buyToken == defaultData.token0, "test was design for token0 to be the buy token");
Expand All @@ -95,4 +79,33 @@ abstract contract ValidateOrderParametersTest is ConstantProductTestHarness {
);
getTradeableOrderUncheckedWrapper(orderOwner, defaultData);
}

function testSellAmountSubtractionUnderflow() public {
ConstantProduct.Data memory defaultData = getDefaultData();
(uint256 selfReserve0, uint256 selfReserve1) = (1337, 1337);
(uint256 uniswapReserve0, uint256 uniswapReserve1) = (1, 1);
setUpDefaultWithReserves(orderOwner, selfReserve0, selfReserve1);
setUpDefaultReferencePairReserves(uniswapReserve0, uniswapReserve1);

uint256 nextBucket = moveTimeToMidFutureBucket();

vm.expectRevert(
abi.encodeWithSelector(
IWatchtowerCustomErrors.PollTryAtEpoch.selector, nextBucket + 1, "subtraction underflow"
)
);
getTradeableOrderUncheckedWrapper(orderOwner, defaultData);
}

function moveTimeToMidFutureBucket() internal returns (uint256 nextBucketStart) {
uint256 smallOffset = 42;
require(smallOffset < constantProduct.MAX_ORDER_DURATION());
uint256 nextTimestamp = 1337 * constantProduct.MAX_ORDER_DURATION() + smallOffset;
nextBucketStart = 1338 * constantProduct.MAX_ORDER_DURATION();
require(
nextTimestamp % constantProduct.MAX_ORDER_DURATION() != 0,
"test was designed so that the timestamp doesn't fall exactly at the start of a bucket, please change the offset"
);
vm.warp(nextTimestamp);
}
}