diff --git a/packages/tasks/contracts/base/TimeLockedTask.sol b/packages/tasks/contracts/base/TimeLockedTask.sol index 381dab70..0ddf668b 100644 --- a/packages/tasks/contracts/base/TimeLockedTask.sol +++ b/packages/tasks/contracts/base/TimeLockedTask.sol @@ -30,15 +30,13 @@ abstract contract TimeLockedTask is ITimeLockedTask, Authorized { /** * @dev Time-locks supports different frequency modes * @param Seconds To indicate the execution must occur every certain number of seconds - * @param OnDay To indicate the execution must occur on day number from 1 to 28 - * @param OnLastMonthDay To indicate the execution must occur on the last day of the month - * @param EverySomeMonths To indicate the execution must occur every certain number of months + * @param OnDay To indicate the execution must occur on day number from 1 to 28 every certain months + * @param OnLastMonthDay To indicate the execution must occur on the last day of the month every certain months */ enum Mode { Seconds, OnDay, - OnLastMonthDay, - EverySomeMonths + OnLastMonthDay } // Time lock mode @@ -131,40 +129,18 @@ abstract contract TimeLockedTask is ITimeLockedTask, Authorized { _nextAllowedAt = allowedAt + ((periods + 1) * frequency); } } else { - uint256 day; - uint256 monthsToAdd; - - if (mode == Mode.EverySomeMonths) { - // Check the difference in months matches the frequency value - uint256 diff = allowedAt.diffMonths(block.timestamp); - if (diff % frequency != 0) revert TaskTimeLockActive(block.timestamp, allowedAt); - day = allowedAt.getDay(); - monthsToAdd = frequency; - } else { - if (mode == Mode.OnDay) { - day = allowedAt.getDay(); - } else if (mode == Mode.OnLastMonthDay) { - day = block.timestamp.getDaysInMonth(); - } else { - revert TaskInvalidFrequencyMode(uint8(mode)); - } - - // Check the current day matches the one in the configuration - if (block.timestamp.getDay() != day) revert TaskTimeLockActive(block.timestamp, allowedAt); - monthsToAdd = 1; - } - - // Construct when would be the current allowed timestamp only considering the current month and year - uint256 currentAllowedAt = _getCurrentAllowedDateForMonthlyRelativeFrequency(allowedAt, day); - if (block.timestamp < currentAllowedAt) revert TaskTimeLockActive(block.timestamp, currentAllowedAt); + // Check the current timestamp is not before the current allowed date + uint256 currentAllowedDateDay = mode == Mode.OnDay ? allowedAt.getDay() : block.timestamp.getDaysInMonth(); + uint256 currentAllowedDate = _getCurrentAllowedDate(allowedAt, currentAllowedDateDay); + if (block.timestamp < currentAllowedDate) revert TaskTimeLockActive(block.timestamp, currentAllowedDate); - // Otherwise, we simply need to check we are within the allowed execution window - uint256 finalCurrentAllowedAt = currentAllowedAt + window; - bool exceedsExecutionWindow = block.timestamp > finalCurrentAllowedAt; - if (exceedsExecutionWindow) revert TaskTimeLockActive(block.timestamp, finalCurrentAllowedAt); + // Check the current timestamp has not passed the allowed execution window + uint256 extendedCurrentAllowedDate = currentAllowedDate + window; + bool exceedsExecutionWindow = block.timestamp > extendedCurrentAllowedDate; + if (exceedsExecutionWindow) revert TaskTimeLockActive(block.timestamp, extendedCurrentAllowedDate); // Finally set the next allowed date to the corresponding number of months from the current date - _nextAllowedAt = _getNextAllowedDateForMonthlyRelativeFrequency(currentAllowedAt, monthsToAdd); + _nextAllowedAt = _getNextAllowedDate(currentAllowedDate, frequency); } } @@ -190,36 +166,26 @@ abstract contract TimeLockedTask is ITimeLockedTask, Authorized { if (window == 0 || window > frequency) revert TaskInvalidAllowedWindow(mode, window); if (allowedAt == 0) revert TaskInvalidAllowedDate(mode, allowedAt); } - } else if (mode == uint8(Mode.OnDay)) { - // It must be valid for every month, then the frequency value cannot be larger than 28 days - if (frequency == 0 || frequency > 28) revert TaskInvalidFrequency(mode, frequency); - - // The execution window that cannot be larger than 28 days - if (window == 0 || window > DAYS_28) revert TaskInvalidAllowedWindow(mode, window); - - // The allowed date must match the specified frequency value - if (allowedAt == 0 || allowedAt.getDay() != frequency) revert TaskInvalidAllowedDate(mode, allowedAt); - } else if (mode == uint8(Mode.OnLastMonthDay)) { - // There must be no frequency value in this case - if (frequency != 0) revert TaskInvalidFrequency(mode, frequency); - - // The execution window that cannot be larger than 28 days - if (window == 0 || window > DAYS_28) revert TaskInvalidAllowedWindow(mode, window); - - // The allowed date timestamp must be the last day of the month - if (allowedAt == 0) revert TaskInvalidAllowedDate(mode, allowedAt); - if (allowedAt.getDay() != allowedAt.getDaysInMonth()) revert TaskInvalidAllowedDate(mode, allowedAt); - } else if (mode == uint8(Mode.EverySomeMonths)) { - // There is no limit on the number of months + } else { + // The other modes can be "on-day" or "on-last-day" where the frequency represents a number of months + // There is no limit for the frequency, it simply cannot be zero if (frequency == 0) revert TaskInvalidFrequency(mode, frequency); // The execution window cannot be larger than the number of months considering months of 28 days if (window == 0 || window > frequency * DAYS_28) revert TaskInvalidAllowedWindow(mode, window); - // The execution allowed at timestamp and the day cannot be greater than the 28th - if (allowedAt == 0 || allowedAt.getDay() > 28) revert TaskInvalidAllowedDate(mode, allowedAt); - } else { - revert TaskInvalidFrequencyMode(mode); + // The allowed date cannot be zero + if (allowedAt == 0) revert TaskInvalidAllowedDate(mode, allowedAt); + + // If the mode is "on-day", the allowed date must be valid for every month, then the allowed day cannot be + // larger than 28. But if the mode is "on-last-day", the allowed date day must be the last day of the month + if (mode == uint8(Mode.OnDay)) { + if (allowedAt.getDay() > 28) revert TaskInvalidAllowedDate(mode, allowedAt); + } else if (mode == uint8(Mode.OnLastMonthDay)) { + if (allowedAt.getDay() != allowedAt.getDaysInMonth()) revert TaskInvalidAllowedDate(mode, allowedAt); + } else { + revert TaskInvalidFrequencyMode(mode); + } } _mode = Mode(mode); @@ -240,15 +206,9 @@ abstract contract TimeLockedTask is ITimeLockedTask, Authorized { } /** - * @dev Tells the allowed date based on a current allowed date considering the current timestamp and a specific day. - * It builds a new date using the current timestamp's month and year, following by the specified day, and using - * the current allowed date hours, minutes, and seconds. + * @dev Tells the corresponding allowed date based on a current timestamp */ - function _getCurrentAllowedDateForMonthlyRelativeFrequency(uint256 allowedAt, uint256 day) - private - view - returns (uint256) - { + function _getCurrentAllowedDate(uint256 allowedAt, uint256 day) private view returns (uint256) { (uint256 year, uint256 month, ) = block.timestamp.timestampToDate(); return _getAllowedDateFor(allowedAt, year, month, day); } @@ -256,11 +216,7 @@ abstract contract TimeLockedTask is ITimeLockedTask, Authorized { /** * @dev Tells the next allowed date based on a current allowed date considering a number of months to increase */ - function _getNextAllowedDateForMonthlyRelativeFrequency(uint256 allowedAt, uint256 monthsToIncrease) - private - view - returns (uint256) - { + function _getNextAllowedDate(uint256 allowedAt, uint256 monthsToIncrease) private view returns (uint256) { (uint256 year, uint256 month, uint256 day) = allowedAt.timestampToDate(); uint256 nextMonth = (month + monthsToIncrease) % 12; uint256 nextYear = year + ((month + monthsToIncrease) / 12); diff --git a/packages/tasks/test/base/TimeLockedTask.test.ts b/packages/tasks/test/base/TimeLockedTask.test.ts index 21161a0e..c3f28530 100644 --- a/packages/tasks/test/base/TimeLockedTask.test.ts +++ b/packages/tasks/test/base/TimeLockedTask.test.ts @@ -177,81 +177,15 @@ describe('TimeLockedTask', () => { const mode = MODE.ON_DAY context('when a frequency is given', () => { - context('when the frequency is lower than or equal to 28', () => { - const frequency = 28 - - context('when a window is given', () => { - context('when the window is shorter than the frequency', () => { - const window = frequency * DAY - 1 - - context('when an allowed date is given', () => { - context('when the allowed day matches the frequency', () => { - const allowedAt = new Date(`2023-10-${frequency}`).getTime() / 1000 - - itSetsTheTimeLockProperly(mode, frequency, allowedAt, window) - }) - - context('when the allowed day does not match the frequency', () => { - const allowedAt = new Date(`2023-10-${frequency + 1}`).getTime() / 1000 - - itReverts(mode, frequency, allowedAt, window, 'TaskInvalidAllowedDate') - }) - }) - - context('when no allowed date is given', () => { - const allowedAt = 0 - - itReverts(mode, frequency, allowedAt, window, 'TaskInvalidAllowedDate') - }) - }) - - context('when the window is larger than the frequency', () => { - const window = frequency * DAY + 1 - - itReverts(mode, frequency, 0, window, 'TaskInvalidAllowedWindow') - }) - }) - - context('when no window is given', () => { - const window = 0 - - itReverts(mode, frequency, 0, window, 'TaskInvalidAllowedWindow') - }) - }) - - context('when the frequency is greater than 28', () => { - const frequency = 29 - - itReverts(mode, frequency, 0, 0, 'TaskInvalidFrequency') - }) - }) - - context('when no frequency is given', () => { - const frequency = 0 - - itReverts(mode, frequency, 0, 0, 'TaskInvalidFrequency') - }) - }) - - context('on-last-day mode', () => { - const mode = MODE.ON_LAST_DAY - - context('when a frequency is given', () => { - const frequency = 1 - - itReverts(mode, frequency, 0, 0, 'TaskInvalidFrequency') - }) - - context('when no frequency is given', () => { - const frequency = 0 + const frequency = 10 context('when a window is given', () => { - context('when the window is shorter than 28 days', () => { - const window = 28 * DAY - 1 + context('when the window is shorter than months of 28 days', () => { + const window = frequency * DAY * 28 - 1 context('when an allowed date is given', () => { - context('when the allowed date is a last day of a month', () => { - const allowedDates = ['2022-06-30', '2023-10-31', '2021-12-31', '2020-02-29', '2021-02-28'] + context('when the allowed date day is lower than or equal to 28', () => { + const allowedDates = ['2022-06-01', '2023-10-11', '2021-12-21', '2020-02-28'] allowedDates.forEach((date) => { context(`for ${date}`, () => { @@ -262,8 +196,8 @@ describe('TimeLockedTask', () => { }) }) - context('when the allowed date is not the last day of a month', () => { - const notAllowedDates = ['2022-08-30', '2020-02-28'] + context('when the allowed date day is not greater than 28', () => { + const notAllowedDates = ['2022-08-30', '2032-02-29', '2020-07-31'] notAllowedDates.forEach((date) => { context(`for ${date}`, () => { @@ -282,8 +216,8 @@ describe('TimeLockedTask', () => { }) }) - context('when the window is larger than 28 days', () => { - const window = 28 * DAY + 1 + context('when the window is larger than months of 28 days', () => { + const window = frequency * DAY * 28 + 1 itReverts(mode, frequency, 0, window, 'TaskInvalidAllowedWindow') }) @@ -295,21 +229,27 @@ describe('TimeLockedTask', () => { itReverts(mode, frequency, 0, window, 'TaskInvalidAllowedWindow') }) }) + + context('when no frequency is given', () => { + const frequency = 0 + + itReverts(mode, frequency, 0, 0, 'TaskInvalidFrequency') + }) }) - context('every-month mode', () => { - const mode = MODE.EVERY_X_MONTH + context('on-last-day mode', () => { + const mode = MODE.ON_LAST_DAY context('when a frequency is given', () => { - const frequency = 3 + const frequency = 10 context('when a window is given', () => { context('when the window is shorter than months of 28 days', () => { const window = 28 * DAY * frequency - 1 context('when an allowed date is given', () => { - context('when the allowed day is lower than or equal to 28', () => { - const allowedDates = ['2022-06-10', '2023-10-01', '2021-02-28'] + context('when the allowed date is a last day of a month', () => { + const allowedDates = ['2022-06-30', '2023-10-31', '2021-12-31', '2020-02-29', '2021-02-28'] allowedDates.forEach((date) => { context(`for ${date}`, () => { @@ -320,8 +260,8 @@ describe('TimeLockedTask', () => { }) }) - context('when the allowed day is greater than 28', () => { - const notAllowedDates = ['2022-08-30', '2020-02-29', '2023-10-31', '2023-06-30'] + context('when the allowed date is not the last day of a month', () => { + const notAllowedDates = ['2022-08-30', '2020-02-28'] notAllowedDates.forEach((date) => { context(`for ${date}`, () => { @@ -340,7 +280,7 @@ describe('TimeLockedTask', () => { }) }) - context('when the window is larger than months of 28 days', () => { + context('when the window is larger than 28 days', () => { const window = 28 * DAY * frequency + 1 itReverts(mode, frequency, 0, window, 'TaskInvalidAllowedWindow') @@ -484,88 +424,132 @@ describe('TimeLockedTask', () => { context('on-day mode', () => { const mode = MODE.ON_DAY - it('locks the task properly', async () => { - // Move to an executable window - await setInitialTimeLock(mode, 5, '2028-10-05T01:02:03Z', DAY) - await moveToDate('2028-10-05T01:02:03Z') + context('with 1 month frequency', () => { + const frequency = 1 + + it('locks the task properly', async () => { + // Move to an executable window + await setInitialTimeLock(mode, frequency, '2028-10-05T01:02:03Z', DAY) + await moveToDate('2028-10-05T01:02:03Z') + + // It can be executed immediately + await assertItCanBeExecuted('2028-11-05T01:02:03Z') + + // It is locked for at least a month + await assertItCannotBeExecuted('2028-11-05T01:02:03Z') + await moveToDate('2028-10-20T01:02:03Z') + await assertItCannotBeExecuted('2028-11-05T01:02:03Z') + + // It cannot be executed after the execution window + await moveToDate('2028-11-06T01:02:03Z') + await assertItCannotBeExecuted('2028-11-05T01:02:03Z') + + // It can be executed one period after + await moveToDate('2028-12-05T01:02:03Z') + await assertItCanBeExecuted('2029-01-05T01:02:03Z') + }) + }) + + context('with 2 months frequency', () => { + const frequency = 2 + + it('locks the task properly', async () => { + // Move to an executable window + await setInitialTimeLock(mode, frequency, '2032-01-01T10:05:20Z', DAY) + await moveToDate('2032-01-01T10:05:20Z') - // It can be executed immediately - await assertItCanBeExecuted('2028-11-05T01:02:03Z') + // It can be executed immediately + await assertItCanBeExecuted('2032-03-01T10:05:20Z') + + // It is locked for at least the number of set months + await assertItCannotBeExecuted('2032-03-01T10:05:20Z') + await moveToDate('2032-02-01T10:05:20Z') + await assertItCannotBeExecuted('2032-03-01T10:05:20Z') + await moveToDate('2032-02-28T10:05:20Z') + await assertItCannotBeExecuted('2032-03-01T10:05:20Z') + + // It cannot be executed after the execution window + await moveToDate('2032-03-02T10:05:21Z') + await assertItCannotBeExecuted('2032-03-01T10:05:20Z') - // It is locked for at least a month - await assertItCannotBeExecuted('2028-11-05T01:02:03Z') - await moveToDate('2028-10-20T01:02:03Z') - await assertItCannotBeExecuted('2028-11-05T01:02:03Z') + // It can be executed one period after + await moveToDate('2032-05-02T10:05:19Z') + await assertItCanBeExecuted('2032-07-01T10:05:20Z') - // It cannot be executed after the execution window - await moveToDate('2028-11-06T01:02:03Z') - await assertItCannotBeExecuted('2028-11-05T01:02:03Z') + // Change time lock to 24 months + await setInitialTimeLock(mode, 24, '2033-01-01T05:04:03Z', DAY) + await assertItCannotBeExecuted('2033-01-01T05:04:03Z') - // It can be executed one period after - await moveToDate('2028-12-05T01:02:03Z') - await assertItCanBeExecuted('2029-01-05T01:02:03Z') + // Move to an executable window + await moveToDate('2033-01-01T05:04:03Z') + await assertItCanBeExecuted('2035-01-01T05:04:03Z') + }) }) }) context('on-last-day mode', () => { const mode = MODE.ON_LAST_DAY - it('locks the task properly', async () => { - // Move to an executable window - await setInitialTimeLock(mode, 0, '2030-10-31T10:32:20Z', DAY) - await moveToDate('2030-10-31T10:32:20Z') + context('with 1 month frequency', () => { + const frequency = 1 - // It can be executed immediately - await assertItCanBeExecuted('2030-11-30T10:32:20Z') + it('locks the task properly', async () => { + // Move to an executable window + await setInitialTimeLock(mode, frequency, '2030-10-31T10:32:20Z', DAY) + await moveToDate('2030-10-31T10:32:20Z') + + // It can be executed immediately + await assertItCanBeExecuted('2030-11-30T10:32:20Z') - // It is locked for at least a month - await assertItCannotBeExecuted('2030-11-30T10:32:20Z') - await moveToDate('2030-11-20T10:32:20Z') - await assertItCannotBeExecuted('2030-11-30T10:32:20Z') + // It is locked for at least a month + await assertItCannotBeExecuted('2030-11-30T10:32:20Z') + await moveToDate('2030-11-20T10:32:20Z') + await assertItCannotBeExecuted('2030-11-30T10:32:20Z') - // It cannot be executed after the execution window - await moveToDate('2031-01-01T10:32:20Z') - await assertItCannotBeExecuted('2030-11-30T10:32:20Z') + // It cannot be executed after the execution window + await moveToDate('2031-01-01T10:32:20Z') + await assertItCannotBeExecuted('2030-11-30T10:32:20Z') - // It can be executed one period after - await moveToDate('2031-01-31T10:32:20Z') - await assertItCanBeExecuted('2031-02-28T10:32:20Z') + // It can be executed one period after + await moveToDate('2031-01-31T10:32:20Z') + await assertItCanBeExecuted('2031-02-28T10:32:20Z') + }) }) - }) - context('every-month mode', () => { - const mode = MODE.EVERY_X_MONTH + context('with 3 months frequency', () => { + const frequency = 3 - it('locks the task properly', async () => { - // Move to an executable window - await setInitialTimeLock(mode, 2, '2032-01-01T10:05:20Z', DAY) - await moveToDate('2032-01-01T10:05:20Z') + it('locks the task properly', async () => { + // Move to an executable window + await setInitialTimeLock(mode, frequency, '2032-01-31T10:05:20Z', DAY) + await moveToDate('2032-01-31T10:05:20Z') - // It can be executed immediately - await assertItCanBeExecuted('2032-03-01T10:05:20Z') + // It can be executed immediately + await assertItCanBeExecuted('2032-04-30T10:05:20Z') - // It is locked for at least the number of set months - await assertItCannotBeExecuted('2032-03-01T10:05:20Z') - await moveToDate('2032-02-01T10:05:20Z') - await assertItCannotBeExecuted('2032-03-01T10:05:20Z') - await moveToDate('2032-02-28T10:05:20Z') - await assertItCannotBeExecuted('2032-03-01T10:05:20Z') + // It is locked for at least the number of set months + await assertItCannotBeExecuted('2032-04-30T10:05:20Z') + await moveToDate('2032-02-28T10:05:20Z') + await assertItCannotBeExecuted('2032-04-30T10:05:20Z') + await moveToDate('2032-03-31T10:05:20Z') + await assertItCannotBeExecuted('2032-04-30T10:05:20Z') - // It cannot be executed after the execution window - await moveToDate('2032-03-02T10:05:21Z') - await assertItCannotBeExecuted('2032-03-01T10:05:20Z') + // It cannot be executed after the execution window + await moveToDate('2032-05-01T10:05:20Z') + await assertItCannotBeExecuted('2032-04-30T10:05:20Z') - // It can be executed one period after - await moveToDate('2032-05-02T10:05:19Z') - await assertItCanBeExecuted('2032-07-01T10:05:20Z') + // It can be executed one period after + await moveToDate('2032-06-30T10:05:20Z') + await assertItCanBeExecuted('2032-09-30T10:05:20Z') - // Change time lock to 24 months - await setInitialTimeLock(mode, 24, '2033-01-01T05:04:03Z', DAY) - await assertItCannotBeExecuted('2033-01-01T05:04:03Z') + // Change time lock to 24 months + await setInitialTimeLock(mode, 24, '2033-01-31T05:04:03Z', DAY) + await assertItCannotBeExecuted('2033-01-31T05:04:03Z') - // Move to an executable window - await moveToDate('2033-01-01T05:04:03Z') - await assertItCanBeExecuted('2035-01-01T05:04:03Z') + // Move to an executable window + await moveToDate('2033-01-31T05:04:03Z') + await assertItCanBeExecuted('2035-01-31T05:04:03Z') + }) }) }) })