Skip to content

Commit

Permalink
tasks: add more time lock tests
Browse files Browse the repository at this point in the history
  • Loading branch information
facuspagnuolo committed Oct 19, 2023
1 parent 44a7319 commit 03cf478
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 130 deletions.
8 changes: 5 additions & 3 deletions packages/tasks/contracts/base/TimeLockedTask.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
209 changes: 82 additions & 127 deletions packages/tasks/test/base/TimeLockedTask.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -385,228 +384,184 @@ 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<void> {
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 })

const { allowedAt } = await task.getTimeLock()
expect(allowedAt).to.be.equal(expectedNextAllowedDate)
}

async function moveToDate(timestamp: string, delta = 0): Promise<void> {
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')
})
})
})

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<number> {
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<number> {
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<number> {
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')
})
})
})
Expand Down

0 comments on commit 03cf478

Please sign in to comment.