Skip to content

Commit

Permalink
feat: OfficeHoursAction (ArbitrumFoundation#311)
Browse files Browse the repository at this point in the history
* office hours action

* ...

* feat: granular office hours (ArbitrumFoundation#312)

* feat: granular office hours

* format: forge fmt

* refactor: custom errors

* fix: casting

* test: OfficeHoursActionTest

* format: forge fmt

* docs: comments

* docs: comment fix

* fix: weekday math

* doc: fix typo

* docs: add utc comment

* docs: wording

* chore: update gas snapshot

* fix: localTimestamp for week of day

* test: local time edge cases

* format: forge fmt

* chore: update gas snapshot

---------

Co-authored-by: gzeon <[email protected]>
Co-authored-by: gzeon <[email protected]>
  • Loading branch information
3 people authored Sep 11, 2024
1 parent 691c022 commit 0a6fb53
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 1 deletion.
11 changes: 10 additions & 1 deletion .gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ L2SecurityCouncilMgmtFactoryTest:testOnlyOwnerCanDeploy() (gas: 25659644)
L2SecurityCouncilMgmtFactoryTest:testRemovalGovDeployment() (gas: 30526232)
L2SecurityCouncilMgmtFactoryTest:testSecurityCouncilManagerDeployment() (gas: 30545325)
NomineeGovernorV2UpgradeActionTest:testAction() (gas: 8153)
OfficeHoursActionTest:testConstructor() (gas: 9050)
OfficeHoursActionTest:testFuzzOfficeHoursDeployment(uint256,uint256,int256,uint256,uint256,uint256) (runs: 258, μ: 317071, ~: 317184)
OfficeHoursActionTest:testInvalidConstructorParameters() (gas: 235740)
OfficeHoursActionTest:testPerformBeforeMinimumTimestamp() (gas: 8646)
OfficeHoursActionTest:testPerformDuringOfficeHours() (gas: 9140)
OfficeHoursActionTest:testPerformFridayUTCSaturdayLocal() (gas: 304792)
OfficeHoursActionTest:testPerformMondayUTCSundayLocal() (gas: 304780)
OfficeHoursActionTest:testPerformOnWeekend() (gas: 9327)
OfficeHoursActionTest:testPerformOutsideOfficeHours() (gas: 9537)
OutboxActionsTest:testAddOutbxesAction() (gas: 651398)
OutboxActionsTest:testCantAddEOA() (gas: 968968)
OutboxActionsTest:testCantReAddOutbox() (gas: 974344)
Expand Down Expand Up @@ -144,7 +153,7 @@ SecurityCouncilMemberElectionGovernorTest:testOnlyNomineeElectionGovernorCanProp
SecurityCouncilMemberElectionGovernorTest:testProperInitialization() (gas: 49388)
SecurityCouncilMemberElectionGovernorTest:testProposeReverts() (gas: 32916)
SecurityCouncilMemberElectionGovernorTest:testRelay() (gas: 42229)
SecurityCouncilMemberElectionGovernorTest:testSelectTopNominees(uint256) (runs: 256, μ: 339999, ~: 339822)
SecurityCouncilMemberElectionGovernorTest:testSelectTopNominees(uint256) (runs: 258, μ: 339983, ~: 339822)
SecurityCouncilMemberElectionGovernorTest:testSelectTopNomineesFails() (gas: 273335)
SecurityCouncilMemberElectionGovernorTest:testSetFullWeightDuration() (gas: 34951)
SecurityCouncilMemberElectionGovernorTest:testVotesToWeight() (gas: 152898)
Expand Down
77 changes: 77 additions & 0 deletions src/gov-action-contracts/util/OfficeHoursAction.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.16;

/// @notice Action that requires the current time to be within office hours. Should be included as an L1 action in proposal data.
contract OfficeHoursAction {
/// @notice The minimum hour (inclusive) to execute the action on. 0 = midnight, 23 = 11pm
/// @dev We enforce the range would not cross local midnight
uint256 public immutable minLocalHour;
/// @notice The maximum hour (exclusive) to execute the action on. 0 = midnight, 23 = 11pm
/// @dev We enforce the range would not cross local midnight
uint256 public immutable maxLocalHour;

/// @notice The offset from UTC to local time. e.g. -5 for EST, -4 for EDT
int256 public immutable localHourOffset;

/// @notice The minimum day of week (inclusive) to execute the action on. 1 = Monday, 7 = Sunday
/// @dev We enforce the range would not cross weekends
uint256 public immutable minDayOfWeek;
/// @notice The maximum day of week (inclusive) to execute the action on. 1 = Monday, 7 = Sunday
/// @dev We enforce the range would not cross weekends
uint256 public immutable maxDayOfWeek;

/// @notice The minimum timestamp to execute the action on
uint256 public immutable minimumTimestamp;

error InvalidHourRange();
error InvalidHourStart();
error InvalidHourEnd();
error InvalidLocalHourOffset();
error InvalidDayOfWeekRange();
error InvalidDayOfWeekStart();
error InvalidDayOfWeekEnd();
error MinimumTimestampNotMet();
error OutsideOfficeDays();
error OutsideOfficeHours();

constructor(
uint256 _minLocalHour,
uint256 _maxLocalHour,
int256 _localHourOffset,
uint256 _minDayOfWeek,
uint256 _maxDayOfWeek,
uint256 _minimumTimestamp
) {
if (_maxLocalHour <= _minLocalHour) revert InvalidHourRange();
if (_minLocalHour > 24) revert InvalidHourStart();
if (_maxLocalHour == 0 || _maxLocalHour > 24) revert InvalidHourEnd();
// UTC is between -12 and +14 https://en.wikipedia.org/wiki/UTC
if (_localHourOffset < -12 || _localHourOffset > 14) revert InvalidLocalHourOffset();
if (_minDayOfWeek > _maxDayOfWeek) revert InvalidDayOfWeekRange();
if (_minDayOfWeek == 0 || _minDayOfWeek > 7) revert InvalidDayOfWeekStart();
if (_maxDayOfWeek == 0 || _maxDayOfWeek > 7) revert InvalidDayOfWeekEnd();

minLocalHour = _minLocalHour;
maxLocalHour = _maxLocalHour;
localHourOffset = _localHourOffset;
minDayOfWeek = _minDayOfWeek;
maxDayOfWeek = _maxDayOfWeek;
minimumTimestamp = _minimumTimestamp;
}

/// @notice Revert if the current time is outside of office hours, or if the minimum timestamp is not met.
function perform() external view {
if (block.timestamp < minimumTimestamp) revert MinimumTimestampNotMet();

// Convert to local time, leap seconds are not accounted for
uint256 localTimestamp = uint256(int256(block.timestamp) + (localHourOffset * 3600));

// Adding 3 because Unix epoch (January 1, 1970) was a Thursday
// from https://github.com/Vectorized/solady/blob/7175c21f95255dc7711ce84cc32080a41864abd6/src/utils/DateTimeLib.sol#L196
uint256 weekday = (localTimestamp / 86_400 + 3) % 7 + 1;
if (weekday < minDayOfWeek || weekday > maxDayOfWeek) revert OutsideOfficeDays();

uint256 localHour = localTimestamp % 86_400 / 3600;
if (localHour < minLocalHour || localHour >= maxLocalHour) revert OutsideOfficeHours();
}
}
138 changes: 138 additions & 0 deletions test/gov-actions/util/OfficeHoursAction.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.16;

import "forge-std/Test.sol";
import "../../../src/gov-action-contracts/util/OfficeHoursAction.sol";

contract OfficeHoursActionTest is Test {
OfficeHoursAction public officeHours;

function setUp() public {
// Setup office hours for weekdays 9 AM to 5 PM EST
officeHours = new OfficeHoursAction(9, 17, -5, 1, 5, block.timestamp);
}

function testConstructor() public {
assertEq(officeHours.minLocalHour(), 9);
assertEq(officeHours.maxLocalHour(), 17);
assertEq(officeHours.localHourOffset(), -5);
assertEq(officeHours.minDayOfWeek(), 1);
assertEq(officeHours.maxDayOfWeek(), 5);
assertEq(officeHours.minimumTimestamp(), block.timestamp);
}

function testPerformDuringOfficeHours() public {
// Set time to Wednesday (3) at 11 AM EST (16:00 UTC)
vm.warp(1_672_848_000); // Wednesday, January 4, 2023 16:00:00 UTC
officeHours.perform(); // Should not revert
}

function testPerformOutsideOfficeHours() public {
// Set time to Thursday (4) at 8 PM EST (00:00 UTC next day)
vm.warp(1_672_876_800); // Thursday, January 5, 2023 00:00:00 UTC
vm.expectRevert(OfficeHoursAction.OutsideOfficeHours.selector);
officeHours.perform();
}

function testPerformOnWeekend() public {
// Set time to Saturday (6) at 11 AM EST (16:00 UTC)
vm.warp(1_673_107_200); // Saturday, January 7, 2023 16:00:00 UTC
vm.expectRevert(OfficeHoursAction.OutsideOfficeDays.selector);
officeHours.perform();
}

function testPerformBeforeMinimumTimestamp() public {
// Set time to before the minimum timestamp
vm.warp(block.timestamp - 1);
vm.expectRevert(OfficeHoursAction.MinimumTimestampNotMet.selector);
officeHours.perform();
}

function testInvalidConstructorParameters() public {
// Test invalid hour range
vm.expectRevert(OfficeHoursAction.InvalidHourRange.selector);
new OfficeHoursAction(17, 9, -5, 1, 5, block.timestamp);

// Test invalid hour start
vm.expectRevert(OfficeHoursAction.InvalidHourStart.selector);
new OfficeHoursAction(25, 26, -5, 1, 5, block.timestamp);

// Test invalid hour end
vm.expectRevert(OfficeHoursAction.InvalidHourEnd.selector);
new OfficeHoursAction(9, 25, -5, 1, 5, block.timestamp);

// Test invalid local hour offset
vm.expectRevert(OfficeHoursAction.InvalidLocalHourOffset.selector);
new OfficeHoursAction(9, 17, -13, 1, 5, block.timestamp);

// Test invalid day of week range
vm.expectRevert(OfficeHoursAction.InvalidDayOfWeekRange.selector);
new OfficeHoursAction(9, 17, -5, 5, 1, block.timestamp);

// Test invalid day of week start
vm.expectRevert(OfficeHoursAction.InvalidDayOfWeekStart.selector);
new OfficeHoursAction(9, 17, -5, 0, 5, block.timestamp);

// Test invalid day of week end
vm.expectRevert(OfficeHoursAction.InvalidDayOfWeekEnd.selector);
new OfficeHoursAction(9, 17, -5, 1, 8, block.timestamp);
}

function testFuzzOfficeHoursDeployment(
uint256 _minLocalHour,
uint256 _maxLocalHour,
int256 _localHourOffset,
uint256 _minDayOfWeek,
uint256 _maxDayOfWeek,
uint256 _minimumTimestamp
) public {
// Bound the input values to reasonable ranges
_minLocalHour = bound(_minLocalHour, 0, 23);
_maxLocalHour = bound(_maxLocalHour, _minLocalHour + 1, 24);
_localHourOffset = int256(bound(uint256(int256(_localHourOffset)), 0, 26)) - 12; // -12 to 14
_minDayOfWeek = bound(_minDayOfWeek, 1, 7);
_maxDayOfWeek = bound(_maxDayOfWeek, _minDayOfWeek, 7);

// Deploy the contract
OfficeHoursAction newOfficeHours = new OfficeHoursAction(
_minLocalHour,
_maxLocalHour,
_localHourOffset,
_minDayOfWeek,
_maxDayOfWeek,
_minimumTimestamp
);

// Verify that the deployed contract has the correct parameters
assertEq(newOfficeHours.minLocalHour(), _minLocalHour);
assertEq(newOfficeHours.maxLocalHour(), _maxLocalHour);
assertEq(newOfficeHours.localHourOffset(), _localHourOffset);
assertEq(newOfficeHours.minDayOfWeek(), _minDayOfWeek);
assertEq(newOfficeHours.maxDayOfWeek(), _maxDayOfWeek);
assertEq(newOfficeHours.minimumTimestamp(), _minimumTimestamp);
}

function testPerformFridayUTCSaturdayLocal() public {
// Create a new OfficeHoursAction for UTC+9 with office hours on weekdays all day
OfficeHoursAction jstOfficeHours = new OfficeHoursAction(0, 24, 9, 1, 5, block.timestamp);

// Set time to Friday 11:00 PM UTC (08:00 AM JST)
// This is 2023-01-06 23:00:00 UTC, which is 2023-01-07 08:00:00 PST (Saturday)
vm.warp(1_673_046_000);

vm.expectRevert(OfficeHoursAction.OutsideOfficeDays.selector);
jstOfficeHours.perform();
}

function testPerformMondayUTCSundayLocal() public {
// Create a new OfficeHoursAction for UTC-5 with office hours on weekdays all day
OfficeHoursAction estOfficeHours = new OfficeHoursAction(0, 24, -5, 1, 5, block.timestamp);

// Set time to Monday 12:30 AM UTC (2:30 PM Monday LIT)
// This is 2023-01-09 00:30:00 UTC, which is 2023-01-08 19:30:00 EST (Sunday)
vm.warp(1_673_224_200);

vm.expectRevert(OfficeHoursAction.OutsideOfficeDays.selector);
estOfficeHours.perform();
}
}

0 comments on commit 0a6fb53

Please sign in to comment.