diff --git a/interfaces/L2/IBaseTime.sol b/interfaces/L2/IBaseTime.sol new file mode 100644 index 000000000..6a8d18a74 --- /dev/null +++ b/interfaces/L2/IBaseTime.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Interfaces +import { ISemver } from "interfaces/universal/ISemver.sol"; + +/// @title IBaseTime +/// @notice Interface for the BaseTime predeploy. +interface IBaseTime is ISemver { + /// @notice Thrown when a caller other than the protocol depositor attempts to update BaseTime. + error BaseTime_NotDepositor(); + + /// @notice Thrown when the millisecond component is not aligned to a 200 millisecond interval. + error BaseTime_InvalidTimestampMillisPart(); + + /// @notice Returns the millisecond component of the current L2 block timestamp. + function timestampMillisPart() external view returns (uint16); + + /// @notice Returns the current L2 block timestamp in milliseconds. + function timestampMs() external view returns (uint64 timestampMs_); + + /// @notice Updates the millisecond component of the current L2 block timestamp. + function setTimestampMillisPart(uint16 _timestampMillisPart) external; +} diff --git a/scripts/Artifacts.s.sol b/scripts/Artifacts.s.sol index a84afb3b5..04f6aa189 100644 --- a/scripts/Artifacts.s.sol +++ b/scripts/Artifacts.s.sol @@ -48,6 +48,7 @@ contract Artifacts { _predeploys[keccak256("OperatorFeeVault")] = payable(Predeploys.OPERATOR_FEE_VAULT); _predeploys[keccak256("SchemaRegistry")] = payable(Predeploys.SCHEMA_REGISTRY); _predeploys[keccak256("EAS")] = payable(Predeploys.EAS); + _predeploys[keccak256("BaseTime")] = payable(Predeploys.BASE_TIME); } /// @notice Returns the address of a deployment. Also handles the predeploys. diff --git a/scripts/L2Genesis.s.sol b/scripts/L2Genesis.s.sol index bf1412b4e..46bb72bf7 100644 --- a/scripts/L2Genesis.s.sol +++ b/scripts/L2Genesis.s.sol @@ -183,6 +183,7 @@ contract L2Genesis is Script { // 1C,1D,1E,1F: not used. setSchemaRegistry(); // 20 setEAS(); // 21 + setBaseTime(); // 30 } function setProxyAdmin(Input memory _input) internal { @@ -313,6 +314,11 @@ contract L2Genesis is Script { }); } + /// @notice This predeploy is following the safety invariant #1. + function setBaseTime() internal { + _setImplementationCode(Predeploys.BASE_TIME); + } + /// @notice This predeploy is following the safety invariant #1. function setSchemaRegistry() internal { _setImplementationCode(Predeploys.SCHEMA_REGISTRY); diff --git a/snapshots/abi/BaseTime.json b/snapshots/abi/BaseTime.json new file mode 100644 index 000000000..e37d75c6f --- /dev/null +++ b/snapshots/abi/BaseTime.json @@ -0,0 +1,64 @@ +[ + { + "inputs": [ + { + "internalType": "uint16", + "name": "_timestampMillisPart", + "type": "uint16" + } + ], + "name": "setTimestampMillisPart", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "timestampMillisPart", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "timestampMs", + "outputs": [ + { + "internalType": "uint64", + "name": "timestampMs_", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "BaseTime_InvalidTimestampMillisPart", + "type": "error" + }, + { + "inputs": [], + "name": "BaseTime_NotDepositor", + "type": "error" + } +] \ No newline at end of file diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index 8f8e8fda5..ed3e6bd84 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -63,6 +63,10 @@ "initCodeHash": "0x812f8ec3945f0b2e2a9615cd7c5cdd60dbe8927f41e606cb2bd488c5e6dec02a", "sourceCodeHash": "0xcb329746df0baddd3dc03c6c88da5d6bdc0f0a96d30e6dc78d0891bb1e935032" }, + "src/L2/BaseTime.sol:BaseTime": { + "initCodeHash": "0xfcd48af0cfd907d0ce91f61ea437c81c9a98269ca2071d7d27291f20c483ccc0", + "sourceCodeHash": "0x98e246b6ff058ecb8aaeacaa33ce70674f4d3d4a8dcabe91df6f772c6d237667" + }, "src/L2/FeeDisburser.sol:FeeDisburser": { "initCodeHash": "0x1278027e3756e2989e80c0a7b513e221a5fe0d3dbd9ded108375a29b2c1f3d57", "sourceCodeHash": "0xac49a0ecf22b8a7bb3ebef830a2d27b19050f9b08941186e8563d5113cf0ce9c" diff --git a/snapshots/storageLayout/BaseTime.json b/snapshots/storageLayout/BaseTime.json new file mode 100644 index 000000000..7e863ba7b --- /dev/null +++ b/snapshots/storageLayout/BaseTime.json @@ -0,0 +1,9 @@ +[ + { + "bytes": "2", + "label": "timestampMillisPart", + "offset": 0, + "slot": "0", + "type": "uint16" + } +] \ No newline at end of file diff --git a/src/L2/BaseTime.sol b/src/L2/BaseTime.sol new file mode 100644 index 000000000..74f2eefa3 --- /dev/null +++ b/src/L2/BaseTime.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Libraries +import { Constants } from "src/libraries/Constants.sol"; + +// Interfaces +import { IBaseTime } from "interfaces/L2/IBaseTime.sol"; + +/// @custom:proxied true +/// @custom:predeploy 0x4200000000000000000000000000000000000030 +/// @title BaseTime +/// @notice Exposes the millisecond component of the current L2 block timestamp. +contract BaseTime is IBaseTime { + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + + /// @inheritdoc IBaseTime + uint16 public timestampMillisPart; + + /// @inheritdoc IBaseTime + function timestampMs() external view returns (uint64 timestampMs_) { + timestampMs_ = uint64(block.timestamp * 1000 + timestampMillisPart); + } + + /// @inheritdoc IBaseTime + function setTimestampMillisPart(uint16 _timestampMillisPart) external { + if (msg.sender != Constants.DEPOSITOR_ACCOUNT) revert BaseTime_NotDepositor(); + if (_timestampMillisPart > 800 || _timestampMillisPart % 200 != 0) { + revert BaseTime_InvalidTimestampMillisPart(); + } + + timestampMillisPart = _timestampMillisPart; + } +} diff --git a/src/libraries/Predeploys.sol b/src/libraries/Predeploys.sol index 79a55612a..86425f3b2 100644 --- a/src/libraries/Predeploys.sol +++ b/src/libraries/Predeploys.sol @@ -63,6 +63,9 @@ library Predeploys { /// @notice Address of the EAS predeploy. address internal constant EAS = 0x4200000000000000000000000000000000000021; + /// @notice Address of the BaseTime predeploy. + address internal constant BASE_TIME = 0x4200000000000000000000000000000000000030; + /// @custom:legacy /// @notice Address of the LegacyERC20ETH predeploy. Deprecated. Balances are migrated to the /// state trie as of the Bedrock upgrade. Contract has been locked and write functions @@ -90,6 +93,7 @@ library Predeploys { if (_addr == SCHEMA_REGISTRY) return "SchemaRegistry"; if (_addr == EAS) return "EAS"; if (_addr == LEGACY_ERC20_ETH) return "LegacyERC20ETH"; + if (_addr == BASE_TIME) return "BaseTime"; revert("Predeploys: unnamed predeploy"); } @@ -104,7 +108,8 @@ library Predeploys { || _addr == L2_STANDARD_BRIDGE || _addr == SEQUENCER_FEE_WALLET || _addr == OPTIMISM_MINTABLE_ERC20_FACTORY || _addr == L2_ERC721_BRIDGE || _addr == L1_BLOCK_ATTRIBUTES || _addr == L2_TO_L1_MESSAGE_PASSER || _addr == OPTIMISM_MINTABLE_ERC721_FACTORY || _addr == PROXY_ADMIN || _addr == BASE_FEE_VAULT - || _addr == L1_FEE_VAULT || _addr == OPERATOR_FEE_VAULT || _addr == SCHEMA_REGISTRY || _addr == EAS; + || _addr == L1_FEE_VAULT || _addr == OPERATOR_FEE_VAULT || _addr == SCHEMA_REGISTRY || _addr == EAS + || _addr == BASE_TIME; } function isPredeployNamespace(address _addr) internal pure returns (bool) { diff --git a/test/L2/BaseTime.t.sol b/test/L2/BaseTime.t.sol new file mode 100644 index 000000000..b5106232b --- /dev/null +++ b/test/L2/BaseTime.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { Test } from "forge-std/Test.sol"; + +// Libraries +import { Constants } from "src/libraries/Constants.sol"; + +// Target contract +import { BaseTime } from "src/L2/BaseTime.sol"; +import { IBaseTime } from "interfaces/L2/IBaseTime.sol"; + +/// @title BaseTime_TestInit +/// @notice Reusable test initialization for BaseTime tests. +abstract contract BaseTime_TestInit is Test { + BaseTime internal baseTime; + + /// @notice Sets up the test suite. + function setUp() public { + baseTime = new BaseTime(); + } + + /// @notice Sets the millisecond component as the protocol depositor. + function setTimestampMillisPart(uint16 _timestampMillisPart) internal { + vm.prank(Constants.DEPOSITOR_ACCOUNT); + baseTime.setTimestampMillisPart(_timestampMillisPart); + } +} + +/// @title BaseTime_TimestampMillisPart_Test +/// @notice Tests BaseTime's initial value. +contract BaseTime_TimestampMillisPart_Test is BaseTime_TestInit { + /// @notice Tests that the millisecond component initially equals zero. + function test_initialValue_succeeds() external view { + assertEq(baseTime.timestampMillisPart(), 0); + } +} + +/// @title BaseTime_SetTimestampMillisPart_Test +/// @notice Tests updating BaseTime's millisecond component. +contract BaseTime_SetTimestampMillisPart_Test is BaseTime_TestInit { + /// @notice Tests every valid millisecond component. + function test_setTimestampMillisPart_succeeds() external { + uint16[5] memory validValues = [uint16(0), 200, 400, 600, 800]; + + for (uint256 i; i < validValues.length; i++) { + setTimestampMillisPart(validValues[i]); + assertEq(baseTime.timestampMillisPart(), validValues[i]); + } + } + + /// @notice Tests that an invalid millisecond component is rejected. + function testFuzz_setTimestampMillisPart_invalidValue_reverts(uint16 _timestampMillisPart) external { + vm.assume(_timestampMillisPart > 800 || _timestampMillisPart % 200 != 0); + vm.expectRevert(IBaseTime.BaseTime_InvalidTimestampMillisPart.selector); + vm.prank(Constants.DEPOSITOR_ACCOUNT); + baseTime.setTimestampMillisPart(_timestampMillisPart); + } + + /// @notice Tests that callers other than the protocol depositor are rejected. + function testFuzz_setTimestampMillisPart_notDepositor_reverts(address _caller) external { + vm.assume(_caller != Constants.DEPOSITOR_ACCOUNT); + vm.expectRevert(IBaseTime.BaseTime_NotDepositor.selector); + vm.prank(_caller); + baseTime.setTimestampMillisPart(200); + } + + /// @notice Tests that the millisecond component occupies slot zero as a uint16. + function test_setTimestampMillisPart_usesSlotZero_succeeds() external { + setTimestampMillisPart(600); + + assertEq(vm.load(address(baseTime), bytes32(0)), bytes32(uint256(600))); + } +} + +/// @title BaseTime_TimestampMs_Test +/// @notice Tests BaseTime's full millisecond timestamp getter. +contract BaseTime_TimestampMs_Test is BaseTime_TestInit { + /// @notice Tests that timestampMs combines block.timestamp with the millisecond component. + function test_timestampMs_succeeds() external { + vm.warp(1725); + setTimestampMillisPart(600); + + assertEq(baseTime.timestampMs(), 1_725_600); + } +} diff --git a/test/scripts/L2Genesis.t.sol b/test/scripts/L2Genesis.t.sol index c0bd80933..13f232731 100644 --- a/test/scripts/L2Genesis.t.sol +++ b/test/scripts/L2Genesis.t.sol @@ -6,6 +6,7 @@ import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { L2Genesis } from "scripts/L2Genesis.s.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { LATEST_FORK } from "scripts/libraries/Config.sol"; +import { IBaseTime } from "interfaces/L2/IBaseTime.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; import { IOptimismMintableERC721Factory } from "interfaces/L2/IOptimismMintableERC721Factory.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; @@ -59,6 +60,12 @@ abstract contract L2Genesis_TestInit is Test { assertGt(Predeploys.WETH.code.length, 0); } + function _assertBaseTime() internal view { + assertEq(Predeploys.BASE_TIME, 0x4200000000000000000000000000000000000030); + assertTrue(Predeploys.isSupportedPredeploy(Predeploys.BASE_TIME)); + assertEq(IBaseTime(Predeploys.BASE_TIME).timestampMillisPart(), 0); + } + function _assertFeeVaultsWithoutRevenueShare() internal view { _assertFeeVault( Predeploys.BASE_FEE_VAULT, @@ -160,6 +167,7 @@ contract L2Genesis_Run_Test is L2Genesis_TestInit { _assertProxyAdmin(); _assertPredeploys(); + _assertBaseTime(); _assertFeeVaultsWithoutRevenueShare(); _assertFactories(); _assertForks();