From 03cf4780a40a5c1f75abb8c2a479b3b1773ce17e Mon Sep 17 00:00:00 2001 From: Facu Spagnuolo Date: Wed, 18 Oct 2023 23:16:30 -0300 Subject: [PATCH] tasks: add more time lock tests --- .../tasks/contracts/base/TimeLockedTask.sol | 8 +- .../tasks/test/base/TimeLockedTask.test.ts | 209 +++++++----------- 2 files changed, 87 insertions(+), 130 deletions(-) diff --git a/packages/tasks/contracts/base/TimeLockedTask.sol b/packages/tasks/contracts/base/TimeLockedTask.sol index a5186cb1..8d12d67e 100644 --- a/packages/tasks/contracts/base/TimeLockedTask.sol +++ b/packages/tasks/contracts/base/TimeLockedTask.sol @@ -156,10 +156,12 @@ abstract contract TimeLockedTask is ITimeLockedTask, Authorized { // 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); - // Since we already checked the current timestamp is not before the allowed timestamp set, - // we simply need to check we are within the allowed execution window - if (block.timestamp - currentAllowedAt > window) revert TaskTimeLockActive(block.timestamp, allowedAt); + // 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); // Finally set the next allowed date to the corresponding number of months from the current date _nextAllowedAt = _getNextAllowedDateForMonthlyRelativeFrequency(currentAllowedAt, monthsToAdd); diff --git a/packages/tasks/test/base/TimeLockedTask.test.ts b/packages/tasks/test/base/TimeLockedTask.test.ts index 3effff4d..04a97212 100644 --- a/packages/tasks/test/base/TimeLockedTask.test.ts +++ b/packages/tasks/test/base/TimeLockedTask.test.ts @@ -8,7 +8,6 @@ import { getSigners, HOUR, MINUTE, - setNextBlockTimestamp, ZERO_BYTES32, } from '@mimic-fi/v3-helpers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' @@ -385,11 +384,29 @@ describe('TimeLockedTask', () => { task = task.connect(owner) }) - async function assertItCannotBeExecuted() { + async function setInitialTimeLock(mode: number, frequency: BigNumberish, timestamp: string, window: number) { + const allowedAt = new Date(timestamp).getTime() / 1000 + await task.setTimeLock(mode, frequency, allowedAt, window) + } + + async function moveToDate(timestamp: string, delta = 0): Promise { + const date = new Date(timestamp).getTime() / 1000 + const now = await currentTimestamp() + const diff = date - now.toNumber() + delta + await advanceTime(diff) + } + + async function assertItCannotBeExecuted(currentAllowedTimestamp: string) { await expect(task.call()).to.be.revertedWith('TaskTimeLockActive') + + const expectedCurrentAllowedDate = new Date(currentAllowedTimestamp).getTime() / 1000 + const { allowedAt } = await task.getTimeLock() + expect(allowedAt).to.be.equal(expectedCurrentAllowedDate) } - async function assertItCanBeExecuted(expectedNextAllowedDate: BigNumberish) { + async function assertItCanBeExecuted(nextAllowedTimestamp: string) { + const expectedNextAllowedDate = new Date(nextAllowedTimestamp).getTime() / 1000 + const tx = await task.call() await assertEvent(tx, 'TimeLockAllowedAtSet', { allowedAt: expectedNextAllowedDate }) @@ -397,91 +414,67 @@ describe('TimeLockedTask', () => { expect(allowedAt).to.be.equal(expectedNextAllowedDate) } - async function moveToDate(timestamp: string, delta = 0): Promise { - const date = new Date(`${timestamp}T00:00:00Z`).getTime() / 1000 - const now = await currentTimestamp() - const diff = date - now.toNumber() + delta - await advanceTime(diff) - } - context('seconds mode', () => { const mode = MODE.SECONDS const frequency = HOUR * 2 - async function getNextAllowedDate(delayed: number) { - const previousTimeLock = await task.getTimeLock() - const now = await currentTimestamp() - return (previousTimeLock.window.eq(0) ? now : previousTimeLock.allowedAt).add(delayed) - } - context('without execution window', () => { const window = 0 const allowedAt = 0 - beforeEach('set time lock', async () => { - await task.setTimeLock(mode, frequency, allowedAt, window) - }) - it('locks the task properly', async () => { // It can be executed immediately - await assertItCanBeExecuted(await getNextAllowedDate(frequency + 1)) + await task.setTimeLock(mode, frequency, allowedAt, window) + await moveToDate('2025-01-01T10:20:29Z') + await assertItCanBeExecuted('2025-01-01T12:20:30Z') // It is locked for a period equal to the frequency set - await assertItCannotBeExecuted() - await advanceTime(frequency / 2) - await assertItCannotBeExecuted() - await advanceTime(frequency / 2) - await assertItCanBeExecuted(await getNextAllowedDate(frequency + 1)) + await assertItCannotBeExecuted('2025-01-01T12:20:30Z') + await moveToDate('2025-01-01T11:20:30Z') + await assertItCannotBeExecuted('2025-01-01T12:20:30Z') + await moveToDate('2025-01-01T12:20:29Z') + await assertItCanBeExecuted('2025-01-01T14:20:30Z') // It is locked for a period equal to the frequency set again - await assertItCannotBeExecuted() - await advanceTime(frequency - 10) - await assertItCannotBeExecuted() + await assertItCannotBeExecuted('2025-01-01T14:20:30Z') + await moveToDate('2025-01-01T14:20:28Z') + await assertItCannotBeExecuted('2025-01-01T14:20:30Z') // It can be executed at any point in time in the future - await advanceTime(frequency * 1000) - await assertItCanBeExecuted(await getNextAllowedDate(frequency + 1)) + await moveToDate('2026-01-01T01:02:03Z') + await assertItCanBeExecuted('2026-01-01T03:02:04Z') }) }) context('with execution window', () => { const window = MINUTE * 30 - const allowedAt = new Date('2023-10-05').getTime() / 1000 - - beforeEach('set time lock', async () => { - await task.setTimeLock(mode, frequency, allowedAt, window) - }) + const allowedAt = new Date('2026-06-01T08:22:34Z').getTime() / 1000 it('locks the task properly', async () => { - // Move to an executable window - const now = await currentTimestamp() - const periods = now.sub(allowedAt).div(frequency).toNumber() - const nextAllowedDate = allowedAt + (periods + 1) * frequency - await setNextBlockTimestamp(nextAllowedDate) - // It can be executed immediately - await assertItCanBeExecuted(await getNextAllowedDate(frequency * (periods + 2))) + await task.setTimeLock(mode, frequency, allowedAt, window) + await moveToDate('2026-06-01T08:22:34Z') + await assertItCanBeExecuted('2026-06-01T10:22:34Z') // It is locked for a period equal to the frequency set - await assertItCannotBeExecuted() - await advanceTime(frequency / 2) - await assertItCannotBeExecuted() - await advanceTime(frequency / 2) - await assertItCanBeExecuted(await getNextAllowedDate(frequency)) + await assertItCannotBeExecuted('2026-06-01T10:22:34Z') + await moveToDate('2026-06-01T09:22:34Z') + await assertItCannotBeExecuted('2026-06-01T10:22:34Z') + await moveToDate('2026-06-01T10:22:34Z') + await assertItCanBeExecuted('2026-06-01T12:22:34Z') // It is locked for a period equal to the frequency set again - await assertItCannotBeExecuted() - await advanceTime(frequency - 10) - await assertItCannotBeExecuted() + await assertItCannotBeExecuted('2026-06-01T12:22:34Z') + await moveToDate('2026-06-01T12:20:34Z') + await assertItCannotBeExecuted('2026-06-01T12:22:34Z') // It cannot be executed after the execution window - const timeLock = await task.getTimeLock() - await setNextBlockTimestamp(timeLock.allowedAt.add(window).add(1)) - await assertItCannotBeExecuted() + await moveToDate('2026-06-01T12:52:35Z') + await assertItCannotBeExecuted('2026-06-01T12:22:34Z') // It can be executed one period after - await setNextBlockTimestamp(timeLock.allowedAt.add(frequency)) - await assertItCanBeExecuted(await getNextAllowedDate(frequency * 2)) + await moveToDate('2026-06-01T14:22:34Z') + await assertItCanBeExecuted('2026-06-01T16:22:34Z') }) }) }) @@ -489,124 +482,86 @@ describe('TimeLockedTask', () => { context('on-day mode', () => { const mode = MODE.ON_DAY const frequency = 5 - const allowedAt = new Date('2028-10-05T00:00:00Z').getTime() / 1000 const window = 1 * DAY - beforeEach('set time lock', async () => { - await task.setTimeLock(mode, frequency, allowedAt, window) - }) - - async function getNextAllowedDate(): Promise { - const now = new Date((await currentTimestamp()).toNumber() * 1000) - const month = now.getMonth() - const nextMonth = (month + 1) % 12 - const nextYear = nextMonth > month ? now.getFullYear() : now.getFullYear() + 1 - return new Date(`${nextYear}-${(nextMonth + 1).toString().padStart(2, '0')}-05T00:00:00Z`).getTime() / 1000 - } - it('locks the task properly', async () => { // Move to an executable window - await moveToDate('2028-10-05') + await setInitialTimeLock(mode, frequency, '2028-10-05T01:02:03Z', window) + await moveToDate('2028-10-05T01:02:03Z') // It can be executed immediately - await assertItCanBeExecuted(await getNextAllowedDate()) + await assertItCanBeExecuted('2028-11-05T01:02:03Z') // It is locked for at least a month - await assertItCannotBeExecuted() - await moveToDate('2028-10-20') - await assertItCannotBeExecuted() + 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-05', window + 1) - await assertItCannotBeExecuted() + 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-05', window - 10) - await assertItCanBeExecuted(await getNextAllowedDate()) + await moveToDate('2028-12-05T01:02:03Z') + await assertItCanBeExecuted('2029-01-05T01:02:03Z') }) }) context('on-last-day mode', () => { const mode = MODE.ON_LAST_DAY const frequency = 0 - const allowedAt = new Date('2030-10-31T00:00:00Z').getTime() / 1000 const window = 1 * DAY - beforeEach('set time lock', async () => { - await task.setTimeLock(mode, frequency, allowedAt, window) - }) - - async function getNextAllowedDate(): Promise { - const now = new Date((await currentTimestamp()).toNumber() * 1000) - const month = now.getMonth() - const nextMonth = (month + 1) % 12 - const nextYear = nextMonth > month ? now.getFullYear() : now.getFullYear() + 1 - const lastDayOfMonth = new Date(nextYear, nextMonth + 1, 0).getDate() - const date = `${nextYear}-${(nextMonth + 1).toString().padStart(2, '0')}-${lastDayOfMonth}T00:00:00Z` - return new Date(date).getTime() / 1000 - } - it('locks the task properly', async () => { // Move to an executable window - await moveToDate('2030-10-31') + await setInitialTimeLock(mode, frequency, '2030-10-31T10:32:20Z', window) + await moveToDate('2030-10-31T10:32:20Z') // It can be executed immediately - await assertItCanBeExecuted(await getNextAllowedDate()) + await assertItCanBeExecuted('2030-11-30T10:32:20Z') // It is locked for at least a month - await assertItCannotBeExecuted() - await moveToDate('2030-11-20') - await assertItCannotBeExecuted() + 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('2030-12-31', window + 1) - await assertItCannotBeExecuted() + 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-31', window - 10) - await assertItCanBeExecuted(await getNextAllowedDate()) + await moveToDate('2031-01-31T10:32:20Z') + await assertItCanBeExecuted('2031-02-28T10:32:20Z') }) }) context('every-month mode', () => { const mode = MODE.EVERY_X_MONTH - const frequency = 3 - const allowedAt = new Date('2032-10-06T00:00:00Z').getTime() / 1000 + const frequency = 2 const window = 1 * DAY - beforeEach('set time lock', async () => { - await task.setTimeLock(mode, frequency, allowedAt, window) - }) - - async function getNextAllowedDate(): Promise { - const now = new Date((await currentTimestamp()).toNumber() * 1000) - const month = now.getMonth() - const nextMonth = (month + frequency) % 12 - const nextYear = nextMonth > month ? now.getFullYear() : now.getFullYear() + 1 - return new Date(`${nextYear}-${(nextMonth + 1).toString().padStart(2, '0')}-06T00:00:00Z`).getTime() / 1000 - } - it('locks the task properly', async () => { // Move to an executable window - await moveToDate('2032-10-06') + await setInitialTimeLock(mode, frequency, '2032-01-01T10:05:20Z', window) + await moveToDate('2032-01-01T10:05:20Z') // It can be executed immediately - await assertItCanBeExecuted(await getNextAllowedDate()) + await assertItCanBeExecuted('2032-03-01T10:05:20Z') // It is locked for at least the number of set months - await assertItCannotBeExecuted() - await moveToDate('2032-11-06') - await assertItCannotBeExecuted() - await moveToDate('2032-12-06') - await assertItCannotBeExecuted() + 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('2033-01-06', window + 1) - await assertItCannotBeExecuted() + await moveToDate('2032-03-02T10:05:21Z') + await assertItCannotBeExecuted('2032-03-01T10:05:20Z') // It can be executed one period after - await moveToDate('2033-04-06', window - 1) - await assertItCanBeExecuted(await getNextAllowedDate()) + await moveToDate('2032-05-02T10:05:19Z') + await assertItCanBeExecuted('2032-07-01T10:05:20Z') }) }) })