Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -39,13 +40,15 @@ forge install base/base-std
<pre>
src
├── <a href="./src/StdPrecompiles.sol">StdPrecompiles.sol</a>: Precompile addresses with interface wrapper handles
├── <a href="./src/StdPredeploys.sol">StdPredeploys.sol</a>: Predeploy addresses with interface wrapper handles
├── interfaces
│ ├── <a href="./src/interfaces/IB20.sol">IB20.sol</a>: Core token standard
│ ├── <a href="./src/interfaces/IB20Asset.sol">IB20Asset.sol</a>: Asset variant of B20
│ ├── <a href="./src/interfaces/IB20Stablecoin.sol">IB20Stablecoin.sol</a>: Stablecoin variant of B20
│ ├── <a href="./src/interfaces/IB20Factory.sol">IB20Factory.sol</a>: B20 factory precompile
│ ├── <a href="./src/interfaces/IPolicyRegistry.sol">IPolicyRegistry.sol</a>: Policy registry precompile
│ └── <a href="./src/interfaces/IActivationRegistry.sol">IActivationRegistry.sol</a>: Activation registry precompile
│ ├── <a href="./src/interfaces/IActivationRegistry.sol">IActivationRegistry.sol</a>: Activation registry precompile
│ └── <a href="./src/interfaces/IBaseTime.sol">IBaseTime.sol</a>: BaseTime predeploy (millisecond block timestamp)
└── lib
├── <a href="./src/lib/B20Constants.sol">B20Constants.sol</a>: B20 role and policy-type identifier constants
└── <a href="./src/lib/B20FactoryLib.sol">B20FactoryLib.sol</a>: Pure encoders for B20 factory params and initCalls
Expand All @@ -59,6 +62,7 @@ These mock contracts replace the live precompiles in unit tests, allowing tests
test/lib/mocks
├── <a href="./test/lib/mocks/MockActivationRegistry.sol">MockActivationRegistry.sol</a>: Mock implementation of the activation registry precompile
├── <a href="./test/lib/mocks/MockPolicyRegistry.sol">MockPolicyRegistry.sol</a>: Mock implementation of the policy registry precompile
├── <a href="./test/lib/mocks/MockBaseTime.sol">MockBaseTime.sol</a>: Mock implementation of the BaseTime predeploy
└── <a href="./test/lib/mocks/MockB20Factory.sol">MockB20Factory.sol</a>: Mock implementation of the B20 factory precompile
</pre>

Expand Down
78 changes: 78 additions & 0 deletions changelog/03_Denim_BaseTime_millisecond_timestamp.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions changelog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |

</details>
Expand Down
10 changes: 9 additions & 1 deletion docs/reference/constants.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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 "<NAME>"` and cross-checked in `chisel`.*
Expand Down
4 changes: 3 additions & 1 deletion docs/reference/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
14 changes: 14 additions & 0 deletions src/StdPredeploys.sol
Original file line number Diff line number Diff line change
@@ -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);
}
31 changes: 31 additions & 0 deletions src/interfaces/IBaseTime.sol
Original file line number Diff line number Diff line change
@@ -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);
}
11 changes: 11 additions & 0 deletions test/lib/BaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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");
Expand All @@ -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
Expand Down
Loading
Loading