diff --git a/AGENTS.md b/AGENTS.md index 9122a522..57770100 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,8 @@ Solidity interfaces, libraries, and reference mocks for Base's precompiles: **B20** (ERC-20 superset with roles, policies, pausing, permits, memos), **PolicyRegistry** (allowlist/blocklist singleton), -and **ActivationRegistry** (feature flags). The Solidity mocks must mirror the Rust implementations +and **ActivationRegistry** (feature flags), plus a read-only interface for the **BaseTime** predeploy +(millisecond block timestamp). The Solidity mocks must mirror the Rust implementations in base/base **slot-for-slot** — storage layout parity is the core invariant of this repo. ## Commands @@ -52,7 +53,8 @@ cross-validates layout/behavior) → **smoke** (real txs against a live chain). ``` src/StdPrecompiles.sol # canonical precompile addresses + typed handles -src/interfaces/ # IB20, IB20Asset, IB20Stablecoin, IB20Factory, IPolicyRegistry, IActivationRegistry +src/StdPredeploys.sol # canonical predeploy addresses + typed handles (BaseTime) +src/interfaces/ # IB20, IB20Asset, IB20Stablecoin, IB20Factory, IPolicyRegistry, IActivationRegistry, IBaseTime src/lib/ # B20Constants (role/policy ids), B20FactoryLib (createB20 encoders) src/impls/ # reserved for reference impls (currently empty; mocks fill that role) test/unit/ # one directory per feature; slot-level assertions diff --git a/CHANGELOG.md b/CHANGELOG.md index 1869de2c..b8b8e935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,10 @@ and selector introduced in this section takes effect only once Denim activates. ### Compatibility -Denim doesn't remove or rename any selector, event topic, or error selector, and adds one view -function. It is **not** additive-only: two B20 changes alter the outcome of existing calls. +Denim doesn't remove or rename any selector, event topic, or error selector. It adds one +PolicyRegistry view function and the read-only `IBaseTime` interface for the new BaseTime predeploy, +neither of which changes an existing selector. It is **not** additive-only: two B20 changes alter +the outcome of existing calls. - A transfer, mint, or seize whose recipient is the token's own address now reverts `InvalidReceiver(to)` where it previously succeeded. @@ -29,6 +31,7 @@ function. It is **not** additive-only: two B20 changes alter the outcome of exis | --- | --- | --- | --- | | B20 (Asset and Stablecoin) | Reject the token itself as a credit recipient (**breaking**) | `transfer`, `transferFrom`, their memo variants, `mint`, `mintWithMemo`, `batchMint`, and `seizeWithMemo` revert `InvalidReceiver(to)` when `to` is the token's own address. Self-transfers (`from == to`) still succeed, and `seizeWithMemo` from the token address still recovers balances already stuck there. | [03_Denim_B20_token_receiver](changelog/03_Denim_B20_token_receiver.md) | | B20 (Asset and Stablecoin) | Transfer executor policy on every transfer path (**breaking**) | `TRANSFER_EXECUTOR_POLICY` checks `msg.sender` on `transfer`, `transferFrom`, and their memo variants, including when `msg.sender == from`. Previously it ran only on delegated `transferFrom`. No new selectors, events, errors, or storage. | [03_Denim_B20_transfer_executor_enforcement](changelog/03_Denim_B20_transfer_executor_enforcement.md) | +| BaseTime | Millisecond block timestamp | New predeploy at `0x4200000000000000000000000000000000000030`, exposed as `StdPredeploys.BASE_TIME`. `IBaseTime` provides `timestampMs()` (`block.timestamp * 1000 + timestampMillisPart()`), `timestampMillisPart()`, and `version()`. Read-only; the protocol writes the value each block. | [03_Denim_BaseTime_millisecond_timestamp](changelog/03_Denim_BaseTime_millisecond_timestamp.md) | | PolicyRegistry | NOT / invert policies | Bit 63 of a policy ID becomes the invert bit: `isAuthorized` resolves the base policy and returns the opposite result, failing closed on an unknown base. Works for simple and composite policies and as a composite child. Adds the `invertedPolicyId(uint64)` view helper. | [03_Denim_PolicyRegistry_not_policy](changelog/03_Denim_PolicyRegistry_not_policy.md) | ### Migration guidance @@ -46,6 +49,12 @@ If holders should keep initiating their own transfers, add them, or a policy cov executor policy before Denim activates. Otherwise only accounts the policy authorizes, such as a transfer agent calling `transferFrom`, can move tokens. +#### BaseTime readers + +Read `StdPredeploys.BASE_TIME.timestampMs()` for the block timestamp in milliseconds. Until the +block's `tx[1]` deposit executes (for example, during the `tx[0]` L1-info deposit), it combines the +current block's seconds with the previous block's millisecond component. + #### PolicyRegistry integrators Use `invertedPolicyId(policyId)` (or set bit 63) instead of maintaining a mirrored allowlist and diff --git a/README.md b/README.md index 6e50c616..a95a8b33 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ A collection of Solidity interfaces, libraries, and mock implementations for Bas - [**ActivationRegistry**](src/interfaces/IActivationRegistry.sol) — Feature flags controlled by Base team to activate/deactivate features. - [**PolicyRegistry**](docs/concepts/policies.md) — Membership sets controlled by custom admins, initially providing allow and block lists for B20 token operations. - [**B20**](docs/overview.md) — Standard ERC-20 implementation with extensions for roles, policies, memos, pausing, ERC-2612 permits, and a variant system. +- [**BaseTime**](src/interfaces/IBaseTime.sol) — Read-only interface for the predeploy exposing the current block timestamp in milliseconds. ## Documentation @@ -39,13 +40,15 @@ forge install base/base-std
 src
 ├── StdPrecompiles.sol: Precompile addresses with interface wrapper handles
+├── StdPredeploys.sol: Predeploy addresses with interface wrapper handles
 ├── interfaces
 │   ├── IB20.sol: Core token standard
 │   ├── IB20Asset.sol: Asset variant of B20
 │   ├── IB20Stablecoin.sol: Stablecoin variant of B20
 │   ├── IB20Factory.sol: B20 factory precompile
 │   ├── IPolicyRegistry.sol: Policy registry precompile
-│   └── IActivationRegistry.sol: Activation registry precompile
+│   ├── IActivationRegistry.sol: Activation registry precompile
+│   └── IBaseTime.sol: BaseTime predeploy (millisecond block timestamp)
 └── lib
     ├── B20Constants.sol: B20 role and policy-type identifier constants
     └── B20FactoryLib.sol: Pure encoders for B20 factory params and initCalls
@@ -59,6 +62,7 @@ These mock contracts replace the live precompiles in unit tests, allowing tests
 test/lib/mocks
 ├── MockActivationRegistry.sol: Mock implementation of the activation registry precompile
 ├── MockPolicyRegistry.sol: Mock implementation of the policy registry precompile
+├── MockBaseTime.sol: Mock implementation of the BaseTime predeploy
 └── MockB20Factory.sol: Mock implementation of the B20 factory precompile
 
diff --git a/changelog/03_Denim_BaseTime_millisecond_timestamp.md b/changelog/03_Denim_BaseTime_millisecond_timestamp.md new file mode 100644 index 00000000..73e77503 --- /dev/null +++ b/changelog/03_Denim_BaseTime_millisecond_timestamp.md @@ -0,0 +1,78 @@ +# BaseTime Millisecond Timestamp + +- **Feature Name**: millisecond_timestamp +- **Start Date**: 2026-10-08 +- **Authors**: Francis Li +- **Title**: BaseTime Millisecond Timestamp + +## Summary + +Denim installs the BaseTime predeploy at `0x4200000000000000000000000000000000000030`. It exposes the millisecond component of the current block timestamp, so contracts can read block time at millisecond precision. + +base-std adds the read-only `IBaseTime` interface and a typed handle, `StdPredeploys.BASE_TIME`. Read `StdPredeploys.BASE_TIME.timestampMs()` for the full millisecond timestamp. + +## Motivation + +`block.timestamp` has second precision, but from Denim a block's timestamp carries a sub-second component. Contracts that need the exact block time, such as auctions, rate limits, or time-weighted pricing, have no way to read the millisecond component from the EVM alone. + +## Background + +BaseTime is a proxied predeploy: Solidity bytecode at a `0x4200…` address, not a native precompile. That is why its handle lives in a new `StdPredeploys` library instead of `StdPrecompiles`. + +The protocol writes the millisecond component once per Denim block, through a depositor-only setter called by the block-scoped deposit at `tx[1]`. Before that deposit executes, the stored value is still the previous block's. + +## Specs + +### Interface Changes + +New interface `src/interfaces/IBaseTime.sol` and library `src/StdPredeploys.sol`: + +```solidity +interface IBaseTime { + function timestampMillisPart() external view returns (uint16); + function timestampMs() external view returns (uint64); + function version() external view returns (string memory); +} + +library StdPredeploys { + address internal constant BASE_TIME_ADDRESS = 0x4200000000000000000000000000000000000030; + IBaseTime internal constant BASE_TIME = IBaseTime(BASE_TIME_ADDRESS); +} +``` + +| Symbol | Selector / Topic0 | Status | Notes | +| ------ | ----------------- | ------ | ----- | +| `timestampMillisPart()` | `0x7b2fea99` | NEW (view) | Millisecond component: `0`, `200`, `400`, `600`, or `800`; never reverts | +| `timestampMs()` | `0x5745a677` | NEW (view) | `block.timestamp * 1000 + timestampMillisPart()`; never reverts | +| `version()` | `0x54fd4d50` | NEW (view) | Returns `"1.0.0"` | + +The depositor-only setter and its errors are protocol-internal and are not part of `IBaseTime`. + +### Behavioural Changes + +- No existing selector, event, error, or storage slot changes. The predeploy is new. +- Storage: `uint16 timestampMillisPart` in the low-order 2 bytes of slot 0. +- Until the block's `tx[1]` deposit executes (for example, during the `tx[0]` L1-info deposit), `timestampMs()` combines the current block's seconds with the previous block's millisecond component. + +### Examples + +```solidity +import {StdPredeploys} from "base-std/StdPredeploys.sol"; + +uint64 nowMs = StdPredeploys.BASE_TIME.timestampMs(); +// block.timestamp == 1_800_000_000, timestampMillisPart() == 400 +// nowMs == 1_800_000_000_400 +``` + +## Design Decisions & Alternatives Considered + +- **Read-only interface.** Only the protocol deposit can write the value, so exposing the setter would give integrators a selector that always reverts for them. +- **Separate `StdPredeploys` library.** Predeploys are EVM bytecode and precompiles are native code; keeping them in separate libraries keeps that distinction visible at the call site. +- **Slot-0 mock layout.** `MockBaseTime` stores the `uint16` at slot 0 instead of an ERC-7201 namespace, matching the real contract's layout so `vm.store`-based tests behave the same against either. + +## Migration Steps + +- Additive: no existing integration changes. +- After Denim activates, read `StdPredeploys.BASE_TIME.timestampMs()` for millisecond block time. +- Code that runs before `tx[1]` in a block, such as the L1-info deposit, sees the previous block's millisecond component. +- Before Denim, the predeploy has no implementation and calls to it revert; don't depend on it on a chain where Denim hasn't activated. diff --git a/changelog/README.md b/changelog/README.md index e7a9e837..95c8d5ba 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -29,6 +29,7 @@ Grouped by hardfork, one collapsible section per hardfork, newest first. | --- | --- | --- | --- | | B20 | Reject the token itself as a credit recipient | `src/interfaces/IB20.sol`, `src/interfaces/IB20Asset.sol` | [03_Denim_B20_token_receiver](03_Denim_B20_token_receiver.md) | | B20 | Transfer executor policy on every transfer path | `src/interfaces/IB20.sol` | [03_Denim_B20_transfer_executor_enforcement](03_Denim_B20_transfer_executor_enforcement.md) | +| BaseTime | Millisecond block timestamp predeploy (read-only) | `src/interfaces/IBaseTime.sol`, `src/StdPredeploys.sol` | [03_Denim_BaseTime_millisecond_timestamp](03_Denim_BaseTime_millisecond_timestamp.md) | | PolicyRegistry | NOT / invert policies | `src/interfaces/IPolicyRegistry.sol` | [03_Denim_PolicyRegistry_not_policy](03_Denim_PolicyRegistry_not_policy.md) | diff --git a/docs/reference/constants.md b/docs/reference/constants.md index 44f45e6a..0077d540 100644 --- a/docs/reference/constants.md +++ b/docs/reference/constants.md @@ -1,6 +1,6 @@ # Constants -*Role identifiers, policy-type identifiers, precompile addresses, and other fixed constants. See [`B20Constants`](../../src/lib/B20Constants.sol) and [`StdPrecompiles`](../../src/StdPrecompiles.sol).* +*Role identifiers, policy-type identifiers, precompile and predeploy addresses, and other fixed constants. See [`B20Constants`](../../src/lib/B20Constants.sol), [`StdPrecompiles`](../../src/StdPrecompiles.sol), and [`StdPredeploys`](../../src/StdPredeploys.sol).* ## Precompile addresses @@ -12,6 +12,14 @@ | `POLICY_REGISTRY_ADDRESS` | `0x8453000000000000000000000000000000000002` | Stores allowlist/blocklist/composite policies and answers `isAuthorized` checks consulted by every policy scope (see [Policies](../concepts/policies.md)). | | `ACTIVATION_REGISTRY_ADDRESS` | `0x8453000000000000000000000000000000000001` | Gates whether a B-20 variant or feature is live on a given chain; checked by the factory before it will create that variant. | +## Predeploy addresses + +*Fixed addresses of Base's predeploys (EVM bytecode at `0x4200…` addresses). See [`StdPredeploys`](../../src/StdPredeploys.sol).* + +| Name | Value | Purpose | +|---|---|---| +| `BASE_TIME_ADDRESS` | `0x4200000000000000000000000000000000000030` | Exposes the current block timestamp in milliseconds through [`IBaseTime`](../../src/interfaces/IBaseTime.sol) (`timestampMs`, `timestampMillisPart`); installed at Denim. | + ## Roles *Role identifiers checked via `hasRole`. See [`B20Constants`](../../src/lib/B20Constants.sol) and [`IB20`](../../src/interfaces/IB20.sol). Hex values are `keccak256` of the role name, verified with `cast keccak ""` and cross-checked in `chisel`.* diff --git a/docs/reference/interfaces.md b/docs/reference/interfaces.md index de911de2..2c45ca03 100644 --- a/docs/reference/interfaces.md +++ b/docs/reference/interfaces.md @@ -10,7 +10,9 @@ | [`IB20Factory`](../../src/interfaces/IB20Factory.sol) | B20 factory precompile | | [`IPolicyRegistry`](../../src/interfaces/IPolicyRegistry.sol) | Policy registry precompile | | [`IActivationRegistry`](../../src/interfaces/IActivationRegistry.sol) | Activation registry precompile | +| [`IBaseTime`](../../src/interfaces/IBaseTime.sol) | BaseTime predeploy (millisecond block timestamp, read-only) | | [`IERC8056`](../../src/interfaces/IERC8056.sol) | Scaled UI Amount standard (Asset variant multiplier) | | [`IERC165`](../../src/interfaces/IERC165.sol) | Interface detection | -See [`StdPrecompiles.sol`](../../src/StdPrecompiles.sol) for canonical precompile addresses. +See [`StdPrecompiles.sol`](../../src/StdPrecompiles.sol) for canonical precompile addresses and +[`StdPredeploys.sol`](../../src/StdPredeploys.sol) for predeploy addresses. diff --git a/src/StdPredeploys.sol b/src/StdPredeploys.sol new file mode 100644 index 00000000..ea618494 --- /dev/null +++ b/src/StdPredeploys.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.20 <0.9.0; + +import {IBaseTime} from "./interfaces/IBaseTime.sol"; + +/// @title StdPredeploys +/// @notice Address constants for Base's predeploys, each paired with an interface-typed handle +/// (e.g. `StdPredeploys.BASE_TIME.timestampMs()`). Predeploys are EVM bytecode at `0x4200…` +/// addresses, unlike the native precompiles in `StdPrecompiles`. +library StdPredeploys { + address internal constant BASE_TIME_ADDRESS = 0x4200000000000000000000000000000000000030; + + IBaseTime internal constant BASE_TIME = IBaseTime(BASE_TIME_ADDRESS); +} diff --git a/src/interfaces/IBaseTime.sol b/src/interfaces/IBaseTime.sol new file mode 100644 index 00000000..3ed142e3 --- /dev/null +++ b/src/interfaces/IBaseTime.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.20 <0.9.0; + +/// @title IBaseTime +/// @notice Read surface of the BaseTime predeploy, which exposes the millisecond component of the current +/// L2 block timestamp. +interface IBaseTime { + /*////////////////////////////////////////////////////////////// + TIMESTAMP QUERIES + //////////////////////////////////////////////////////////////*/ + + /// @notice Millisecond component (0, 200, 400, 600, or 800) of the current block timestamp. Never reverts. + /// + /// @dev Updated by the block's `tx[1]` deposit; earlier in the block it holds the previous block's value. + /// + /// @return Millisecond component of the current block timestamp. + function timestampMillisPart() external view returns (uint16); + + /// @notice Current block timestamp in milliseconds: `block.timestamp * 1000 + timestampMillisPart()`. + /// Never reverts. + /// + /// @dev Until the block's `tx[1]` deposit executes (e.g. during the `tx[0]` L1-info deposit), this + /// combines the current block's seconds with the previous block's millisecond component. + /// + /// @return Current block timestamp in milliseconds. + function timestampMs() external view returns (uint64); + + /// @notice Semantic version of the BaseTime implementation. Never reverts. + /// @return Semantic version string. + function version() external view returns (string memory); +} diff --git a/test/lib/BaseTest.sol b/test/lib/BaseTest.sol index eeb43b0d..563bf3b6 100644 --- a/test/lib/BaseTest.sol +++ b/test/lib/BaseTest.sol @@ -9,10 +9,12 @@ import {ActivationRegistryFeatureList} from "base-std-test/lib/mocks/ActivationR import {MockActivationRegistry} from "base-std-test/lib/mocks/MockActivationRegistry.sol"; import {MockPolicyRegistry} from "base-std-test/lib/mocks/MockPolicyRegistry.sol"; import {MockB20Factory} from "base-std-test/lib/mocks/MockB20Factory.sol"; +import {MockBaseTime} from "base-std-test/lib/mocks/MockBaseTime.sol"; import {IActivationRegistry} from "base-std/interfaces/IActivationRegistry.sol"; import {IPolicyRegistry} from "base-std/interfaces/IPolicyRegistry.sol"; import {StdPrecompiles} from "base-std/StdPrecompiles.sol"; +import {StdPredeploys} from "base-std/StdPredeploys.sol"; /// @notice Common base for every test contract in this suite. /// @@ -74,6 +76,9 @@ import {StdPrecompiles} from "base-std/StdPrecompiles.sol"; /// mutate a single ERC-7201-namespaced `features` map and emit the /// paired events. Only admin (resolved at setUp via /// `activationRegistry.admin()`) may flip features. +/// - `MockBaseTime` implements the `IBaseTime` read surface over a +/// slot-0 `uint16`. It is etched at the BaseTime predeploy only when +/// that address has no code, independently of the precompile probe. abstract contract BaseTest is Test { // -- Actors -- address internal admin = makeAddr("admin"); @@ -97,6 +102,12 @@ abstract contract BaseTest is Test { vm.label(StdPrecompiles.B20_FACTORY_ADDRESS, "B20Factory"); vm.label(StdPrecompiles.POLICY_REGISTRY_ADDRESS, "PolicyRegistry"); vm.label(StdPrecompiles.ACTIVATION_REGISTRY_ADDRESS, "ActivationRegistry"); + vm.label(StdPredeploys.BASE_TIME_ADDRESS, "BaseTime"); + + // BaseTime is EVM bytecode, so a code-size check is reliable here. + if (StdPredeploys.BASE_TIME_ADDRESS.code.length == 0) { + vm.etch(StdPredeploys.BASE_TIME_ADDRESS, type(MockBaseTime).runtimeCode); + } // Pick the world by detecting whether the live precompiles are // present (see contract NatSpec), then announce it so a run is never diff --git a/test/lib/BaseTimeTest.sol b/test/lib/BaseTimeTest.sol new file mode 100644 index 00000000..ba2fe651 --- /dev/null +++ b/test/lib/BaseTimeTest.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {BaseTest} from "base-std-test/lib/BaseTest.sol"; +import {MockBaseTimeStorage} from "base-std-test/lib/mocks/MockBaseTimeStorage.sol"; + +import {IBaseTime} from "base-std/interfaces/IBaseTime.sol"; +import {StdPredeploys} from "base-std/StdPredeploys.sol"; + +/// @notice Base test contract for `IBaseTime` unit tests. +/// +/// Inherits the BaseTime etch wiring from `BaseTest`; adds the predeploy +/// handle and a helper that writes the millisecond component directly, +/// standing in for the protocol's `tx[1]` deposit. +contract BaseTimeTest is BaseTest { + // -- Predeploy handle -- + IBaseTime internal baseTime = StdPredeploys.BASE_TIME; + + /// @notice Writes `part` into BaseTime's `timestampMillisPart` slot. + function _setTimestampMillisPart(uint16 part) internal { + vm.store( + StdPredeploys.BASE_TIME_ADDRESS, MockBaseTimeStorage.TIMESTAMP_MILLIS_PART_SLOT, bytes32(uint256(part)) + ); + } + + /// @notice Maps a fuzz seed to a valid millisecond component (0, 200, 400, 600, or 800). + function _validMillisPart(uint256 seed) internal pure returns (uint16) { + // forge-lint: disable-next-line(unsafe-typecast) + return uint16(bound(seed, 0, 4) * 200); + } +} diff --git a/test/lib/mocks/MockBaseTime.sol b/test/lib/mocks/MockBaseTime.sol new file mode 100644 index 00000000..1596a73e --- /dev/null +++ b/test/lib/mocks/MockBaseTime.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IBaseTime} from "base-std/interfaces/IBaseTime.sol"; + +/// @title MockBaseTime +/// @notice Reference implementation of the `IBaseTime` read surface. Etched at the canonical BaseTime +/// predeploy address via `vm.etch` from `BaseTest.setUp` when that address has no code. +/// +/// @dev No setter: the protocol's `tx[1]` deposit is the only writer on-chain, and tests set the +/// value with `vm.store` (see `BaseTimeTest`). +contract MockBaseTime is IBaseTime { + /// @notice Semantic version reported by the predeploy. + string internal constant VERSION = "1.0.0"; + + // Slot 0 to match the real predeploy's layout (see `MockBaseTimeStorage`), not ERC-7201. + uint16 internal _timestampMillisPart; + + /// @inheritdoc IBaseTime + function timestampMillisPart() external view returns (uint16) { + return _timestampMillisPart; + } + + /// @inheritdoc IBaseTime + function timestampMs() external view returns (uint64) { + return uint64(block.timestamp * 1000 + _timestampMillisPart); + } + + /// @inheritdoc IBaseTime + function version() external pure returns (string memory) { + return VERSION; + } +} diff --git a/test/lib/mocks/MockBaseTimeStorage.sol b/test/lib/mocks/MockBaseTimeStorage.sol new file mode 100644 index 00000000..f452a00e --- /dev/null +++ b/test/lib/mocks/MockBaseTimeStorage.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/// @title MockBaseTimeStorage +/// @notice Slot-layout constants for the `MockBaseTime` reference implementation. +/// +/// @dev BaseTime is a Solidity predeploy, not a native precompile, so its layout is the +/// compiler's sequential layout rather than an ERC-7201 namespace: `uint16 +/// timestampMillisPart` occupies the low-order 2 bytes of slot 0. +library MockBaseTimeStorage { + /// @notice Slot holding `timestampMillisPart`. + bytes32 internal constant TIMESTAMP_MILLIS_PART_SLOT = bytes32(uint256(0)); +} diff --git a/test/unit/BaseTime/selectors.t.sol b/test/unit/BaseTime/selectors.t.sol new file mode 100644 index 00000000..690c7918 --- /dev/null +++ b/test/unit/BaseTime/selectors.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {IBaseTime} from "base-std/interfaces/IBaseTime.sol"; + +/// @notice Pins each `IBaseTime` selector to the value exposed by the BaseTime predeploy. +contract BaseTimeSelectorsTest is Test { + /// @notice `timestampMillisPart()` selector equals `0x7b2fea99`. + function test_timestampMillisPart_selector_pinned() public pure { + assertEq(IBaseTime.timestampMillisPart.selector, bytes4(0x7b2fea99), "timestampMillisPart selector"); + } + + /// @notice `timestampMs()` selector equals `0x5745a677`. + function test_timestampMs_selector_pinned() public pure { + assertEq(IBaseTime.timestampMs.selector, bytes4(0x5745a677), "timestampMs selector"); + } + + /// @notice `version()` selector equals `0x54fd4d50`. + function test_version_selector_pinned() public pure { + assertEq(IBaseTime.version.selector, bytes4(0x54fd4d50), "version selector"); + } +} diff --git a/test/unit/BaseTime/timestampMillisPart.t.sol b/test/unit/BaseTime/timestampMillisPart.t.sol new file mode 100644 index 00000000..15927e52 --- /dev/null +++ b/test/unit/BaseTime/timestampMillisPart.t.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {BaseTimeTest} from "base-std-test/lib/BaseTimeTest.sol"; + +contract BaseTimeTimestampMillisPartTest is BaseTimeTest { + /// @notice Verifies timestampMillisPart returns zero before any write + /// @dev Default state of slot 0 on a freshly etched predeploy. + function test_timestampMillisPart_success_zeroByDefault() public view { + assertEq(baseTime.timestampMillisPart(), 0, "timestampMillisPart must default to zero"); + } + + /// @notice Verifies timestampMillisPart returns the stored millisecond component + /// @dev Fuzzes over the valid set {0, 200, 400, 600, 800}. + function test_timestampMillisPart_success_returnsStored(uint256 seed) public { + uint16 part = _validMillisPart(seed); + _setTimestampMillisPart(part); + assertEq(baseTime.timestampMillisPart(), part, "timestampMillisPart must return the stored value"); + } +} diff --git a/test/unit/BaseTime/timestampMs.t.sol b/test/unit/BaseTime/timestampMs.t.sol new file mode 100644 index 00000000..d6e1e3be --- /dev/null +++ b/test/unit/BaseTime/timestampMs.t.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {BaseTimeTest} from "base-std-test/lib/BaseTimeTest.sol"; + +contract BaseTimeTimestampMsTest is BaseTimeTest { + /// @notice Verifies timestampMs combines block.timestamp seconds with the stored millisecond component + /// @dev Timestamp is bounded so `ts * 1000 + part` fits in uint64. + function test_timestampMs_success_combinesSecondsAndMillis(uint256 ts, uint256 seed) public { + ts = bound(ts, 0, type(uint64).max / 1000 - 1); + uint16 part = _validMillisPart(seed); + vm.warp(ts); + _setTimestampMillisPart(part); + assertEq(baseTime.timestampMs(), ts * 1000 + part, "timestampMs must equal block.timestamp * 1000 + part"); + } +} diff --git a/test/unit/BaseTime/version.t.sol b/test/unit/BaseTime/version.t.sol new file mode 100644 index 00000000..d9503275 --- /dev/null +++ b/test/unit/BaseTime/version.t.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {BaseTimeTest} from "base-std-test/lib/BaseTimeTest.sol"; + +contract BaseTimeVersionTest is BaseTimeTest { + /// @notice Verifies version returns the predeploy's semantic version + function test_version_success_returnsSemver() public view { + assertEq(baseTime.version(), "1.0.0", "version must equal 1.0.0"); + } +} diff --git a/test/unit/storage/MockBaseTimeStorage.t.sol b/test/unit/storage/MockBaseTimeStorage.t.sol new file mode 100644 index 00000000..32910a40 --- /dev/null +++ b/test/unit/storage/MockBaseTimeStorage.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {BaseTimeTest} from "base-std-test/lib/BaseTimeTest.sol"; +import {MockBaseTimeStorage} from "base-std-test/lib/mocks/MockBaseTimeStorage.sol"; + +/// @notice Pins `MockBaseTime`'s storage to the real predeploy's layout: `uint16 timestampMillisPart` +/// in the low-order 2 bytes of slot 0. +contract MockBaseTimeStorageTest is BaseTimeTest { + /// @notice Verifies `TIMESTAMP_MILLIS_PART_SLOT` is slot 0. + function test_TIMESTAMP_MILLIS_PART_SLOT_success_isSlotZero() public pure { + assertEq(MockBaseTimeStorage.TIMESTAMP_MILLIS_PART_SLOT, bytes32(0), "timestampMillisPart must live in slot 0"); + } + + /// @notice Verifies a value written to slot 0 is what `timestampMillisPart` reads. + function test_timestampMillisPart_success_readsSlotZero(uint16 part) public { + _setTimestampMillisPart(part); + assertEq( + vm.load(address(baseTime), MockBaseTimeStorage.TIMESTAMP_MILLIS_PART_SLOT), + bytes32(uint256(part)), + "slot 0 must hold the written value" + ); + assertEq(baseTime.timestampMillisPart(), part, "timestampMillisPart must read slot 0"); + } + + /// @notice Verifies `timestampMillisPart` reads only the low-order 2 bytes of slot 0. + /// @dev Fills the high 30 bytes with `high`; a uint16 at offset 0 must ignore them. + function test_timestampMillisPart_success_readsLowOrderBytes(uint16 part, uint240 high) public { + vm.assume(high != 0); + vm.store( + address(baseTime), + MockBaseTimeStorage.TIMESTAMP_MILLIS_PART_SLOT, + bytes32((uint256(high) << 16) | uint256(part)) + ); + assertEq(baseTime.timestampMillisPart(), part, "timestampMillisPart must read the low-order 2 bytes"); + } +}