Skip to content

Commit

Permalink
CS5.7 - Prevent executing recurring swaps for missed intervals + add …
Browse files Browse the repository at this point in the history
…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`.
  • Loading branch information
kevincheng96 authored Oct 21, 2024
1 parent ff00501 commit 14f377b
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 81 deletions.
49 changes: 34 additions & 15 deletions src/RecurringSwap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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;
Expand Down Expand Up @@ -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) =
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions src/builder/Actions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 ===== */

Expand Down Expand Up @@ -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,
Expand All @@ -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
});
Expand Down
Loading

0 comments on commit 14f377b

Please sign in to comment.