From 14f377ba90e348b1eaa01e1954bae9959cfa4821 Mon Sep 17 00:00:00 2001 From: Kevin Cheng Date: Mon, 21 Oct 2024 11:38:08 -0700 Subject: [PATCH] CS5.7 - Prevent executing recurring swaps for missed intervals + add SwapWindow.length (#90) Previously, if a recurring swap interval was missed in the past, a swap could be repeatedly executed for each missed interval. This change makes it so that swaps can no longer be executed for missed intervals. We also define another `SwapWindow` struct that defines the `startTime`, `interval`, and `length` of the window. The `length` field is the only new field. It allows us to keep the window tighter so we don't end up in a situation where a "Swap every Monday" configuration results in a "Buy on Sunday" + "Buy on Monday". A swap is executable any time between the `swapWindowStartTime` + `swapWindowLength`. The `swapWindowStartTime` shifts in increments of `swapWindowInterval`. --- src/RecurringSwap.sol | 49 ++-- src/builder/Actions.sol | 9 +- test/RecurringSwap.t.sol | 239 ++++++++++++++----- test/builder/QuarkBuilderRecurringSwap.t.sol | 13 +- 4 files changed, 229 insertions(+), 81 deletions(-) diff --git a/src/RecurringSwap.sol b/src/RecurringSwap.sol index 6e28ca9..ebae2e2 100644 --- a/src/RecurringSwap.sol +++ b/src/RecurringSwap.sol @@ -21,7 +21,8 @@ contract RecurringSwap is QuarkScript { error BadPrice(); error InvalidInput(); - error SwapWindowNotOpen(uint256 nextSwapTime, uint256 currentTime); + error SwapWindowClosed(uint256 currentWindowStartTime, uint256 windowLength, uint256 currentTime); + error SwapWindowNotOpen(uint256 nextWindowStartTime, uint256 windowLength, uint256 currentTime); /// @notice Emitted when a swap is executed event SwapExecuted( @@ -42,19 +43,27 @@ contract RecurringSwap is QuarkScript { /** * @dev Note: This script uses the following storage layout in the Quark wallet: - * mapping(bytes32 hashedSwapConfig => uint256 nextSwapTime) + * mapping(bytes32 hashedSwapConfig => uint256 nextWindowStart) * where hashedSwapConfig = getNonceIsolatedKey(keccak256(SwapConfig)) */ /// @notice Parameters for a recurring swap order struct SwapConfig { - uint256 startTime; - /// @dev In seconds - uint256 interval; + SwapWindow swapWindow; SwapParams swapParams; SlippageParams slippageParams; } + /// @notice Parameters for performing a swap + struct SwapWindow { + /// @dev Timestamp of the start of the first swap window + uint256 startTime; + /// @dev Measured in seconds; time between the start of each swap window + uint256 interval; + /// @dev Measured in seconds; defines how long the window for executing the swap remains open + uint256 length; + } + /// @notice Parameters for performing a swap struct SwapParams { address uniswapRouter; @@ -93,20 +102,29 @@ contract RecurringSwap is QuarkScript { } bytes32 hashedConfig = _hashConfig(config); - uint256 nextSwapTime; + uint256 nextWindowStart; if (read(hashedConfig) == 0) { - nextSwapTime = config.startTime; + nextWindowStart = config.swapWindow.startTime; } else { - nextSwapTime = uint256(read(hashedConfig)); + nextWindowStart = uint256(read(hashedConfig)); } - // Check conditions - if (block.timestamp < nextSwapTime) { - revert SwapWindowNotOpen(nextSwapTime, block.timestamp); + // Check that swap window is open + if (block.timestamp < nextWindowStart) { + revert SwapWindowNotOpen(nextWindowStart, config.swapWindow.length, block.timestamp); + } + + // Find the last window start time and the next window start time + uint256 completedIntervals = (block.timestamp - config.swapWindow.startTime) / config.swapWindow.interval; + uint256 lastWindowStart = config.swapWindow.startTime + (completedIntervals * config.swapWindow.interval); + uint256 updatedNextWindowStart = lastWindowStart + config.swapWindow.interval; + + // Check that current swap window (lastWindowStart + swapWindow.length) is not closed + if (block.timestamp > lastWindowStart + config.swapWindow.length) { + revert SwapWindowClosed(lastWindowStart, config.swapWindow.length, block.timestamp); } - // Update nextSwapTime - write(hashedConfig, bytes32(nextSwapTime + config.interval)); + write(hashedConfig, bytes32(updatedNextWindowStart)); (uint256 amountIn, uint256 amountOut) = _calculateSwapAmounts(config); (uint256 actualAmountIn, uint256 actualAmountOut) = @@ -249,8 +267,9 @@ contract RecurringSwap is QuarkScript { function _hashConfig(SwapConfig calldata config) internal pure returns (bytes32) { return keccak256( abi.encodePacked( - config.startTime, - config.interval, + config.swapWindow.startTime, + config.swapWindow.interval, + config.swapWindow.length, abi.encodePacked( config.swapParams.uniswapRouter, config.swapParams.recipient, diff --git a/src/builder/Actions.sol b/src/builder/Actions.sol index 520a858..473dd08 100644 --- a/src/builder/Actions.sol +++ b/src/builder/Actions.sol @@ -66,6 +66,7 @@ library Actions { uint256 constant AVERAGE_BLOCK_TIME = 12 seconds; uint256 constant RECURRING_SWAP_MAX_SLIPPAGE = 1e17; // 1% + uint256 constant RECURRING_SWAP_WINDOW_LENGTH = 1 days; /* ===== Custom Errors ===== */ @@ -1505,6 +1506,11 @@ library Actions { RecurringSwap.SwapConfig memory swapConfig; // Local scope to avoid stack too deep { + RecurringSwap.SwapWindow memory swapWindow = RecurringSwap.SwapWindow({ + startTime: swap.blockTimestamp - AVERAGE_BLOCK_TIME, + interval: swap.interval, + length: RECURRING_SWAP_WINDOW_LENGTH + }); RecurringSwap.SwapParams memory swapParams = RecurringSwap.SwapParams({ uniswapRouter: UniswapRouter.knownRouter(swap.chainId), recipient: swap.sender, @@ -1525,8 +1531,7 @@ library Actions { shouldInvert: shouldInvert }); swapConfig = RecurringSwap.SwapConfig({ - startTime: swap.blockTimestamp - AVERAGE_BLOCK_TIME, - interval: swap.interval, + swapWindow: swapWindow, swapParams: swapParams, slippageParams: slippageParams }); diff --git a/test/RecurringSwap.t.sol b/test/RecurringSwap.t.sol index d205886..3f170c4 100644 --- a/test/RecurringSwap.t.sol +++ b/test/RecurringSwap.t.sol @@ -50,6 +50,9 @@ contract RecurringSwapTest is Test { // Price feeds address constant ETH_USD_PRICE_FEED = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; // Price is $1790.45 address constant USDC_USD_PRICE_FEED = 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6; + // Swap window params + uint256 constant SWAP_WINDOW_INTERVAL = 1 days; + uint256 constant SWAP_WINDOW_LENGTH = 1 days; constructor() { // Fork setup @@ -81,13 +84,13 @@ contract RecurringSwapTest is Test { uint256 startingUSDC = 100_000e6; deal(USDC, address(aliceWallet), startingUSDC); - uint40 swapInterval = 86_400; // 1 day interval uint256 amountToSell = 3_000e6; // Price of ETH is $1,790.45 at the current block uint256 expectedAmountOutMinimum = 1.65 ether; RecurringSwap.SwapConfig memory swapConfig = _createSwapConfig({ startTime: block.timestamp, - swapInterval: swapInterval, + swapInterval: SWAP_WINDOW_INTERVAL, + swapLength: SWAP_WINDOW_LENGTH, amount: amountToSell, isExactOut: false }); @@ -127,13 +130,13 @@ contract RecurringSwapTest is Test { uint256 startingUSDC = 100_000e6; deal(USDC, address(aliceWallet), startingUSDC); - uint40 swapInterval = 86_400; // 1 day interval uint256 amountToSwap = 10 ether; // Price of ETH is $1,790.45 at the current block uint256 expectedAmountInMaximum = 1_800e6 * 10; RecurringSwap.SwapConfig memory swapConfig = _createSwapConfig({ startTime: block.timestamp, - swapInterval: swapInterval, + swapInterval: SWAP_WINDOW_INTERVAL, + swapLength: SWAP_WINDOW_LENGTH, amount: amountToSwap, isExactOut: true }); @@ -175,10 +178,14 @@ contract RecurringSwapTest is Test { uint256 startingWETH = 100 ether; deal(WETH, address(aliceWallet), startingWETH); - uint40 swapInterval = 86_400; // 1 day interval uint256 amountToSell = 10 ether; // Price of ETH is $1,790.45 at the current block uint256 expectedAmountOutMinimum = 17_800e6; + RecurringSwap.SwapWindow memory swapWindow = RecurringSwap.SwapWindow({ + startTime: block.timestamp, + interval: SWAP_WINDOW_INTERVAL, + length: SWAP_WINDOW_LENGTH + }); RecurringSwap.SwapParams memory swapParams = RecurringSwap.SwapParams({ uniswapRouter: UNISWAP_ROUTER, recipient: address(aliceWallet), @@ -193,12 +200,8 @@ contract RecurringSwapTest is Test { priceFeeds: _array1(ETH_USD_PRICE_FEED), shouldInvert: _array1(false) }); - RecurringSwap.SwapConfig memory swapConfig = RecurringSwap.SwapConfig({ - startTime: block.timestamp, - interval: swapInterval, - swapParams: swapParams, - slippageParams: slippageParams - }); + RecurringSwap.SwapConfig memory swapConfig = + RecurringSwap.SwapConfig({swapWindow: swapWindow, swapParams: swapParams, slippageParams: slippageParams}); QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( aliceWallet, @@ -236,10 +239,14 @@ contract RecurringSwapTest is Test { uint256 startingWETH = 100 ether; deal(WETH, address(aliceWallet), startingWETH); - uint40 swapInterval = 86_400; // 1 day interval uint256 amountToSwap = 1_800e6; // Price of ETH is $1,790.45 at the current block uint256 expectedAmountInMaximum = 1.1 ether; + RecurringSwap.SwapWindow memory swapWindow = RecurringSwap.SwapWindow({ + startTime: block.timestamp, + interval: SWAP_WINDOW_INTERVAL, + length: SWAP_WINDOW_LENGTH + }); RecurringSwap.SwapParams memory swapParams = RecurringSwap.SwapParams({ uniswapRouter: UNISWAP_ROUTER, recipient: address(aliceWallet), @@ -254,12 +261,8 @@ contract RecurringSwapTest is Test { priceFeeds: _array1(ETH_USD_PRICE_FEED), shouldInvert: _array1(false) }); - RecurringSwap.SwapConfig memory swapConfig = RecurringSwap.SwapConfig({ - startTime: block.timestamp, - interval: swapInterval, - swapParams: swapParams, - slippageParams: slippageParams - }); + RecurringSwap.SwapConfig memory swapConfig = + RecurringSwap.SwapConfig({swapWindow: swapWindow, swapParams: swapParams, slippageParams: slippageParams}); QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( aliceWallet, @@ -296,11 +299,11 @@ contract RecurringSwapTest is Test { vm.pauseGasMetering(); deal(USDC, address(aliceWallet), 100_000e6); - uint40 swapInterval = 86_400; // 1 day interval uint256 amountToSwap = 10 ether; RecurringSwap.SwapConfig memory swapConfig = _createSwapConfig({ startTime: block.timestamp, - swapInterval: swapInterval, + swapInterval: SWAP_WINDOW_INTERVAL, + swapLength: SWAP_WINDOW_LENGTH, amount: amountToSwap, isExactOut: true }); @@ -327,13 +330,16 @@ contract RecurringSwapTest is Test { // 2a. Cannot buy again unless time interval has passed vm.expectRevert( abi.encodeWithSelector( - RecurringSwap.SwapWindowNotOpen.selector, block.timestamp + swapInterval, block.timestamp + RecurringSwap.SwapWindowNotOpen.selector, + block.timestamp + SWAP_WINDOW_INTERVAL, + SWAP_WINDOW_LENGTH, + block.timestamp ) ); aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v1, r1, s1); // 2b. Execute recurring swap a second time after warping 1 day - vm.warp(block.timestamp + swapInterval); + vm.warp(block.timestamp + SWAP_WINDOW_INTERVAL); aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v1, r1, s1); assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 2 * amountToSwap); @@ -344,11 +350,11 @@ contract RecurringSwapTest is Test { vm.pauseGasMetering(); deal(USDC, address(aliceWallet), 100_000e6); - uint40 swapInterval = 86_400; // 1 day interval uint256 amountToSwap = 10 ether; RecurringSwap.SwapConfig memory swapConfig = _createSwapConfig({ startTime: block.timestamp, - swapInterval: swapInterval, + swapInterval: SWAP_WINDOW_INTERVAL, + swapLength: SWAP_WINDOW_LENGTH, amount: amountToSwap, isExactOut: true }); @@ -380,7 +386,7 @@ contract RecurringSwapTest is Test { aliceWallet.executeQuarkOperationWithSubmissionToken(cancelOp, submissionTokens[1], v2, r2, s2); // 3. Replayable transaction can no longer be executed - vm.warp(block.timestamp + swapInterval); + vm.warp(block.timestamp + SWAP_WINDOW_INTERVAL); vm.expectRevert( abi.encodeWithSelector( QuarkNonceManager.NonReplayableNonce.selector, address(aliceWallet), op.nonce, submissionTokens[1] @@ -404,7 +410,6 @@ contract RecurringSwapTest is Test { uint256 startingUSDC = 100_000e6; deal(USDC, address(aliceWallet), startingUSDC); - uint40 swapInterval = 86_400; // 1 day interval uint256 amountToSell = 3_000e6; uint256 expectedAmountOutMinimum = 1.65 ether; // We are swapping from USDC -> WETH, so the order of the price feeds should be: @@ -416,7 +421,8 @@ contract RecurringSwapTest is Test { }); RecurringSwap.SwapConfig memory swapConfig = _createSwapConfig({ startTime: block.timestamp, - swapInterval: swapInterval, + swapInterval: SWAP_WINDOW_INTERVAL, + swapLength: SWAP_WINDOW_LENGTH, amount: amountToSell, isExactOut: false, slippageParams: slippageParams @@ -445,7 +451,6 @@ contract RecurringSwapTest is Test { vm.pauseGasMetering(); deal(USDC, address(aliceWallet), 100_000e6); - uint40 swapInterval = 86_400; // 1 day interval uint256 amountToSwap1 = 10 ether; uint256 amountToSwap2 = 5 ether; bytes32[] memory submissionTokens; @@ -457,7 +462,8 @@ contract RecurringSwapTest is Test { // Two swap configs using the same nonce: one to swap 10 ETH and the other to swap 5 ETH RecurringSwap.SwapConfig memory swapConfig1 = _createSwapConfig({ startTime: block.timestamp, - swapInterval: swapInterval, + swapInterval: SWAP_WINDOW_INTERVAL, + swapLength: SWAP_WINDOW_LENGTH, amount: amountToSwap1, isExactOut: true }); @@ -471,7 +477,8 @@ contract RecurringSwapTest is Test { op1.expiry = type(uint256).max; RecurringSwap.SwapConfig memory swapConfig2 = _createSwapConfig({ startTime: block.timestamp, - swapInterval: swapInterval, + swapInterval: SWAP_WINDOW_INTERVAL, + swapLength: SWAP_WINDOW_LENGTH, amount: amountToSwap2, isExactOut: true }); @@ -507,7 +514,7 @@ contract RecurringSwapTest is Test { assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), amountToSwap1 + amountToSwap2); // 2. Warp until next swap period - vm.warp(block.timestamp + swapInterval); + vm.warp(block.timestamp + SWAP_WINDOW_INTERVAL); // 3a. Execute recurring swap order #1 aliceWallet.executeQuarkOperationWithSubmissionToken(op1, submissionTokens[2], v1, r1, s1); @@ -523,7 +530,7 @@ contract RecurringSwapTest is Test { aliceWallet.executeQuarkOperationWithSubmissionToken(cancelOp, submissionTokens[4], v3, r3, s3); // 5. Warp until next swap period - vm.warp(block.timestamp + swapInterval); + vm.warp(block.timestamp + SWAP_WINDOW_INTERVAL); // 6. Both recurring swap orders can no longer be executed vm.expectRevert( @@ -542,11 +549,65 @@ contract RecurringSwapTest is Test { assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 2 * amountToSwap1 + 2 * amountToSwap2); } + function testRecurringSwapCannotSwapMultipleTimesForMissedWindows() public { + // gas: disable gas metering except while executing operations + vm.pauseGasMetering(); + + deal(USDC, address(aliceWallet), 100_000e6); + uint256 amountToSwap = 10 ether; + RecurringSwap.SwapConfig memory swapConfig = _createSwapConfig({ + startTime: block.timestamp, + swapInterval: SWAP_WINDOW_INTERVAL, + swapLength: SWAP_WINDOW_LENGTH, + amount: amountToSwap, + isExactOut: true + }); + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, + recurringSwap, + abi.encodeWithSelector(RecurringSwap.swap.selector, swapConfig), + ScriptType.ScriptAddress, + 2 + ); + op.expiry = type(uint256).max; + (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 0 ether); + + // gas: meter execute + vm.resumeGasMetering(); + // 1. Execute recurring swap for the first time + aliceWallet.executeQuarkOperation(op, v1, r1, s1); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), amountToSwap); + + // 2. Skip a few swap intervals by warping past multiple swap intervals + vm.warp(block.timestamp + 10 * SWAP_WINDOW_INTERVAL); + + // 3. Execute recurring swap a second time + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v1, r1, s1); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 2 * amountToSwap); + + // 4. Cannot buy again at the current timestamp even though we skipped a few swap intervals + vm.expectRevert( + abi.encodeWithSelector( + RecurringSwap.SwapWindowNotOpen.selector, + block.timestamp + SWAP_WINDOW_INTERVAL, + SWAP_WINDOW_LENGTH, + block.timestamp + ) + ); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[2], v1, r1, s1); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 2 * amountToSwap); + } + function testRevertsForInvalidInput() public { // gas: disable gas metering except while executing operations vm.pauseGasMetering(); - uint40 swapInterval = 86_400; // 1 day interval uint256 amountToSwap = 10 ether; RecurringSwap.SlippageParams memory invalidSlippageParams1 = RecurringSwap.SlippageParams({ maxSlippage: 1e17, // 1% @@ -560,14 +621,16 @@ contract RecurringSwapTest is Test { }); RecurringSwap.SwapConfig memory invalidSwapConfig1 = _createSwapConfig({ startTime: block.timestamp, - swapInterval: swapInterval, + swapInterval: SWAP_WINDOW_INTERVAL, + swapLength: SWAP_WINDOW_LENGTH, amount: amountToSwap, isExactOut: true, slippageParams: invalidSlippageParams1 }); RecurringSwap.SwapConfig memory invalidSwapConfig2 = _createSwapConfig({ startTime: block.timestamp, - swapInterval: swapInterval, + swapInterval: SWAP_WINDOW_INTERVAL, + swapLength: SWAP_WINDOW_LENGTH, amount: amountToSwap, isExactOut: true, slippageParams: invalidSlippageParams2 @@ -601,11 +664,11 @@ contract RecurringSwapTest is Test { vm.pauseGasMetering(); deal(USDC, address(aliceWallet), 100_000e6); - uint40 swapInterval = 86_400; // 1 day interval uint256 amountToSwap = 10 ether; RecurringSwap.SwapConfig memory swapConfig = _createSwapConfig({ startTime: block.timestamp, - swapInterval: swapInterval, + swapInterval: SWAP_WINDOW_INTERVAL, + swapLength: SWAP_WINDOW_LENGTH, amount: amountToSwap, isExactOut: true }); @@ -631,7 +694,10 @@ contract RecurringSwapTest is Test { // 2. Cannot buy again unless time interval has passed vm.expectRevert( abi.encodeWithSelector( - RecurringSwap.SwapWindowNotOpen.selector, block.timestamp + swapInterval, block.timestamp + RecurringSwap.SwapWindowNotOpen.selector, + block.timestamp + SWAP_WINDOW_INTERVAL, + SWAP_WINDOW_LENGTH, + block.timestamp ) ); aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v1, r1, s1); @@ -639,16 +705,69 @@ contract RecurringSwapTest is Test { assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), amountToSwap); } + function testRevertsForSwapWhenSwapWindowIsClosed() public { + // gas: disable gas metering except while executing operations + vm.pauseGasMetering(); + + deal(USDC, address(aliceWallet), 100_000e6); + uint256 amountToSwap = 10 ether; + uint256 windowLength = 30; + RecurringSwap.SwapConfig memory swapConfig = _createSwapConfig({ + startTime: block.timestamp, + swapInterval: SWAP_WINDOW_INTERVAL, + swapLength: windowLength, + amount: amountToSwap, + isExactOut: true + }); + (QuarkWallet.QuarkOperation memory op, bytes32[] memory submissionTokens) = new QuarkOperationHelper() + .newReplayableOpWithCalldata( + aliceWallet, + recurringSwap, + abi.encodeWithSelector(RecurringSwap.swap.selector, swapConfig), + ScriptType.ScriptAddress, + 2 + ); + op.expiry = type(uint256).max; + (uint8 v1, bytes32 r1, bytes32 s1) = new SignatureHelper().signOp(alicePrivateKey, aliceWallet, op); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 0 ether); + + // gas: meter execute + vm.resumeGasMetering(); + // 1. Execute recurring swap for the first time + aliceWallet.executeQuarkOperation(op, v1, r1, s1); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), amountToSwap); + + // 2. Warp a window into the future and execute a second swap + vm.warp(block.timestamp + SWAP_WINDOW_INTERVAL); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[1], v1, r1, s1); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 2 * amountToSwap); + + // 3. Warp more than a window into the future and fail to execute a third swap + vm.warp(block.timestamp + SWAP_WINDOW_INTERVAL + windowLength + 1); + uint256 lastWindowStart = block.timestamp - windowLength - 1; + vm.expectRevert( + abi.encodeWithSelector( + RecurringSwap.SwapWindowClosed.selector, lastWindowStart, windowLength, block.timestamp + ) + ); + aliceWallet.executeQuarkOperationWithSubmissionToken(op, submissionTokens[2], v1, r1, s1); + + assertEq(IERC20(WETH).balanceOf(address(aliceWallet)), 2 * amountToSwap); + } + function testRevertsForSwapBeforeStartTime() public { // gas: disable gas metering except while executing operations vm.pauseGasMetering(); deal(USDC, address(aliceWallet), 100_000e6); - uint40 swapInterval = 86_400; // 1 day interval uint256 amountToSwap = 10 ether; RecurringSwap.SwapConfig memory swapConfig = _createSwapConfig({ startTime: block.timestamp + 100, - swapInterval: swapInterval, + swapInterval: SWAP_WINDOW_INTERVAL, + swapLength: SWAP_WINDOW_LENGTH, amount: amountToSwap, isExactOut: true }); @@ -664,7 +783,9 @@ contract RecurringSwapTest is Test { // gas: meter execute vm.expectRevert( - abi.encodeWithSelector(RecurringSwap.SwapWindowNotOpen.selector, block.timestamp + 100, block.timestamp) + abi.encodeWithSelector( + RecurringSwap.SwapWindowNotOpen.selector, block.timestamp + 100, SWAP_WINDOW_LENGTH, block.timestamp + ) ); aliceWallet.executeQuarkOperation(op, v1, r1, s1); } @@ -674,11 +795,11 @@ contract RecurringSwapTest is Test { vm.pauseGasMetering(); deal(USDC, address(aliceWallet), 100_000e6); - uint40 swapInterval = 86_400; // 1 day interval uint256 amountToSwap = 10 ether; RecurringSwap.SwapConfig memory swapConfig = _createSwapConfig({ startTime: block.timestamp, - swapInterval: swapInterval, + swapInterval: SWAP_WINDOW_INTERVAL, + swapLength: SWAP_WINDOW_LENGTH, amount: amountToSwap, isExactOut: true }); @@ -702,7 +823,6 @@ contract RecurringSwapTest is Test { vm.pauseGasMetering(); deal(USDC, address(aliceWallet), 100_000e6); - uint40 swapInterval = 86_400; // 1 day interval uint256 amountToSell = 3_000e6; RecurringSwap.SlippageParams memory slippageParams = RecurringSwap.SlippageParams({ maxSlippage: 0e18, // 0% accepted slippage @@ -711,7 +831,8 @@ contract RecurringSwapTest is Test { }); RecurringSwap.SwapConfig memory swapConfig = _createSwapConfig({ startTime: block.timestamp, - swapInterval: swapInterval, + swapInterval: SWAP_WINDOW_INTERVAL, + swapLength: SWAP_WINDOW_LENGTH, amount: amountToSell, isExactOut: false, slippageParams: slippageParams @@ -737,7 +858,6 @@ contract RecurringSwapTest is Test { vm.pauseGasMetering(); deal(USDC, address(aliceWallet), 100_000e6); - uint40 swapInterval = 86_400; // 1 day interval uint256 amountToSell = 3_000e6; RecurringSwap.SlippageParams memory slippageParams = RecurringSwap.SlippageParams({ maxSlippage: 5e17, // 5% accepted slippage @@ -746,7 +866,8 @@ contract RecurringSwapTest is Test { }); RecurringSwap.SwapConfig memory swapConfig = _createSwapConfig({ startTime: block.timestamp, - swapInterval: swapInterval, + swapInterval: SWAP_WINDOW_INTERVAL, + swapLength: SWAP_WINDOW_LENGTH, amount: amountToSell, isExactOut: false, slippageParams: slippageParams @@ -795,11 +916,13 @@ contract RecurringSwapTest is Test { return arr; } - function _createSwapConfig(uint256 startTime, uint256 swapInterval, uint256 amount, bool isExactOut) - internal - view - returns (RecurringSwap.SwapConfig memory) - { + function _createSwapConfig( + uint256 startTime, + uint256 swapInterval, + uint256 swapLength, + uint256 amount, + bool isExactOut + ) internal view returns (RecurringSwap.SwapConfig memory) { RecurringSwap.SlippageParams memory slippageParams = RecurringSwap.SlippageParams({ maxSlippage: 1e17, // 1% priceFeeds: _array1(ETH_USD_PRICE_FEED), @@ -808,6 +931,7 @@ contract RecurringSwapTest is Test { return _createSwapConfig({ startTime: startTime, swapInterval: swapInterval, + swapLength: swapLength, amount: amount, isExactOut: isExactOut, slippageParams: slippageParams @@ -817,10 +941,13 @@ contract RecurringSwapTest is Test { function _createSwapConfig( uint256 startTime, uint256 swapInterval, + uint256 swapLength, uint256 amount, bool isExactOut, RecurringSwap.SlippageParams memory slippageParams ) internal view returns (RecurringSwap.SwapConfig memory) { + RecurringSwap.SwapWindow memory swapWindow = + RecurringSwap.SwapWindow({startTime: startTime, interval: swapInterval, length: swapLength}); bytes memory swapPath; if (isExactOut) { // Exact out swap @@ -838,12 +965,8 @@ contract RecurringSwapTest is Test { isExactOut: isExactOut, path: swapPath }); - RecurringSwap.SwapConfig memory swapConfig = RecurringSwap.SwapConfig({ - startTime: startTime, - interval: swapInterval, - swapParams: swapParams, - slippageParams: slippageParams - }); + RecurringSwap.SwapConfig memory swapConfig = + RecurringSwap.SwapConfig({swapWindow: swapWindow, swapParams: swapParams, slippageParams: slippageParams}); return swapConfig; } } diff --git a/test/builder/QuarkBuilderRecurringSwap.t.sol b/test/builder/QuarkBuilderRecurringSwap.t.sol index 0f72050..9446339 100644 --- a/test/builder/QuarkBuilderRecurringSwap.t.sol +++ b/test/builder/QuarkBuilderRecurringSwap.t.sol @@ -80,6 +80,11 @@ contract QuarkBuilderRecurringSwapTest is Test, QuarkBuilderTest { pure returns (RecurringSwap.SwapConfig memory) { + RecurringSwap.SwapWindow memory swapWindow = RecurringSwap.SwapWindow({ + startTime: swap.blockTimestamp - Actions.AVERAGE_BLOCK_TIME, + interval: swap.interval, + length: Actions.RECURRING_SWAP_WINDOW_LENGTH + }); RecurringSwap.SwapParams memory swapParams = RecurringSwap.SwapParams({ uniswapRouter: UniswapRouter.knownRouter(swap.chainId), recipient: swap.sender, @@ -96,12 +101,8 @@ contract QuarkBuilderRecurringSwapTest is Test, QuarkBuilderTest { priceFeeds: priceFeeds, shouldInvert: shouldInvert }); - return RecurringSwap.SwapConfig({ - startTime: swap.blockTimestamp - Actions.AVERAGE_BLOCK_TIME, - interval: swap.interval, - swapParams: swapParams, - slippageParams: slippageParams - }); + return + RecurringSwap.SwapConfig({swapWindow: swapWindow, swapParams: swapParams, slippageParams: slippageParams}); } function testInsufficientFunds() public {