Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fdfc7e5
contracts-bedrock: add Espresso BatchAuthenticator and supporting infra
QuentinI May 7, 2026
25f2234
Update packages/contracts-bedrock/src/L1/BatchAuthenticator.sol
QuentinI May 13, 2026
8b76632
Remove stray path
QuentinI May 13, 2026
97d4a34
Remove hardcoded Sepolia URL
QuentinI May 13, 2026
857717f
Use OZ v5 in mock verifier
QuentinI May 13, 2026
00d6510
Fix Codex's suggestion
QuentinI May 20, 2026
cabd2ee
Lower pragma
QuentinI May 20, 2026
7ea3c59
Remove unrelated foundry.toml changes
QuentinI May 20, 2026
5e4bcf8
scripts/checks: clean up exclude lists per PR review
QuentinI May 20, 2026
f84ac86
Fix re-initialization
QuentinI May 21, 2026
6b85fa1
Add batcher address history
QuentinI May 25, 2026
a882446
contracts-bedrock: drop OZ TUP from espresso deploy
palango May 20, 2026
c8db328
Remove pause from BatchAuthenticator
QuentinI May 25, 2026
8630695
Add a defensive check
QuentinI May 25, 2026
353252f
test: add end-to-end dual-batcher switch test
QuentinI May 25, 2026
de02776
Fix tests
QuentinI May 28, 2026
0ccf6ec
switchBatcher from toggle to a setter
QuentinI May 29, 2026
d9a4585
Use OZ Checkpoints for batcher history
QuentinI May 29, 2026
3b4618a
contracts-bedrock: deploy espresso impls via vm.getCode, drop suppres…
QuentinI Jun 1, 2026
4821ebe
Check batcher in Espresso mode
shenkeyao May 28, 2026
c67ab4d
regenerate snapshots for UnauthorizedEspressoBatcher error
QuentinI Jun 2, 2026
d2ed3bc
contracts-bedrock: wire espresso proxies to shared OP Stack ProxyAdmin
QuentinI Jun 4, 2026
12048f9
add caller to BatchInfoAuthenticated event
QuentinI Jun 4, 2026
9448a74
forge fmt
piersy Jun 9, 2026
05fd400
Remove unused imports
piersy Jun 10, 2026
eeed5c2
Rename tests to fit test name convention
piersy Jun 10, 2026
308f90f
op-chain-ops/script: resolve directory-qualified getCode artifact names
QuentinI Jun 11, 2026
6fd5401
contracts-bedrock: fix semgrep checks-fast findings
QuentinI Jun 11, 2026
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: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,9 @@
[submodule "packages/contracts-bedrock/lib/superchain-registry"]
path = packages/contracts-bedrock/lib/superchain-registry
url = https://github.com/ethereum-optimism/superchain-registry
[submodule "packages/contracts-bedrock/lib/espresso-tee-contracts"]
path = packages/contracts-bedrock/lib/espresso-tee-contracts
url = https://github.com/EspressoSystems/espresso-tee-contracts
[submodule "packages/contracts-bedrock/lib/openzeppelin-contracts-upgradeable-v5"]
path = packages/contracts-bedrock/lib/openzeppelin-contracts-upgradeable-v5
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
5 changes: 5 additions & 0 deletions op-chain-ops/script/cheatcodes_external.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ func (c *CheatCodesPrecompile) getArtifact(input string) (*foundry.Artifact, err
name = parts[0]
contract = parts[1]
}
// Foundry accepts fully-qualified names of the form "path/to/File.sol:Contract"
// (e.g. "src/universal/Proxy.sol:Proxy", used to disambiguate from a shadowing
// library artifact). The artifacts FS is keyed by the source-file basename, so
// reduce any directory-qualified path to its basename before lookup.
name = path.Base(name)
return c.h.af.ReadArtifact(name, contract)
}

Expand Down
25 changes: 25 additions & 0 deletions op-chain-ops/script/script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,31 @@ func TestScript(t *testing.T) {
require.NoError(t, h.cheatcodes.Precompile.DumpState("noop"))
}

func TestGetCodeArtifactResolution(t *testing.T) {
logger := testlog.Logger(t, log.LevelInfo)
af := foundry.OpenArtifactsDir("./testdata/test-artifacts")
h := NewHost(logger, af, nil, DefaultContext)
require.NoError(t, h.EnableCheats())

// The artifacts FS is keyed by the source-file basename. Foundry's getCode
// cheatcode accepts a "File.sol:Contract" identifier and a directory-qualified
// "path/to/File.sol:Contract" identifier. Both must resolve to the same
// artifact in the Go host.
want, err := af.ReadArtifact("ScriptExample.s.sol", "FooBar")
require.NoError(t, err)

for _, input := range []string{
"ScriptExample.s.sol:FooBar",
"some/nested/dir/ScriptExample.s.sol:FooBar",
} {
t.Run(input, func(t *testing.T) {
got, err := h.cheatcodes.Precompile.GetCode(input)
require.NoError(t, err)
require.Equal(t, []byte(want.Bytecode.Object), got)
})
}
}

func mustEncodeStringCalldata(t *testing.T, method, input string) []byte {
packer, err := abi.JSON(strings.NewReader(fmt.Sprintf(`[{"type":"function","name":"%s","inputs":[{"type":"string","name":"input"}]}]`, method)))
require.NoError(t, err)
Expand Down
44 changes: 43 additions & 1 deletion packages/contracts-bedrock/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
src = 'src'
out = 'forge-artifacts'
script = 'scripts'
# espresso: explicitly set libs to prevent Forge from searching espresso-tee-contracts/lib
# nested subdirectories, which causes ambiguous import errors for openzeppelin-contracts.
libs = ['lib']
build_info_path = 'artifacts/build-info'
snapshots = 'notarealpath' # workaround for foundry#9477
allow_internal_expect_revert = true # workaround described in https://github.com/PaulRBerg/prb-math/issues/248

optimizer = true
optimizer_runs = 999999
Expand All @@ -20,6 +25,7 @@ optimizer_runs = 999999
additional_compiler_profiles = [
{ name = "dispute", optimizer_runs = 5000 },
{ name = "validator", optimizer_runs = 200 },
{ name = "via-ir", via_ir = true },
]
compilation_restrictions = [
{ paths = "src/dispute/FaultDisputeGame.sol", optimizer_runs = 5000 },
Expand All @@ -42,18 +48,35 @@ ast = true
evm_version = 'cancun'

remappings = [
# Espresso-tee-contracts context-specific remappings (must come before general @openzeppelin remappings)
'lib/espresso-tee-contracts/:@espresso-tee/=lib/espresso-tee-contracts/src/',
'lib/espresso-tee-contracts/:@openzeppelin/contracts/=lib/openzeppelin-contracts-v5/contracts',
# espresso: OZ upgradeable v5 is at a top-level lib (not nested under espresso-tee-contracts/lib/)
# to avoid ambiguous import errors when Forge adds lib/espresso-tee-contracts/ as an include path.
'lib/espresso-tee-contracts/:@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable-v5/contracts',
'lib/espresso-tee-contracts/:aws-nitro-enclave-attestation/=lib/espresso-tee-contracts/lib/aws-nitro-enclave-attestation/contracts/src/',
'lib/espresso-tee-contracts/:solady/=lib/solady/src',
# Context remappings for OZ upgradeable v5: its @openzeppelin/contracts/ imports → OZ v5 non-upgradeable
'lib/openzeppelin-contracts-upgradeable-v5/:@openzeppelin/contracts/=lib/openzeppelin-contracts-v5/contracts',
# General remappings
'@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts',
# espresso: OZ upgradeable v5 alias for src/ contracts (e.g. BatchAuthenticator.sol)
'@openzeppelin/contracts-upgradeable-v5/=lib/openzeppelin-contracts-upgradeable-v5/contracts',
'@espresso-tee-contracts/=lib/espresso-tee-contracts/src',
'@nitro-validator/=lib/espresso-tee-contracts/lib/nitro-validator/src',
'aws-nitro-enclave-attestation/=lib/espresso-tee-contracts/lib/aws-nitro-enclave-attestation/contracts/src',
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts',
'@openzeppelin/contracts-v5/=lib/openzeppelin-contracts-v5/contracts',
'@rari-capital/solmate/=lib/solmate',
'@lib-keccak/=lib/lib-keccak/contracts/lib',
'solady/=lib/solady/',
'@solady/=lib/solady/src',
'@solady-v0.0.245/=lib/solady-v0.0.245/src',
'forge-std/=lib/forge-std/src',
'ds-test/=lib/forge-std/lib/ds-test/src',
'safe-contracts/=lib/safe-contracts/contracts',
'kontrol-cheatcodes/=lib/kontrol-cheatcodes/src',
'interfaces/=interfaces'
'interfaces/=interfaces',
]

fs_permissions = [
Expand All @@ -70,6 +93,7 @@ fs_permissions = [
{ access='read', path='./lib/superchain-registry/superchain/extra/' },
{ access='read', path='./lib/superchain-registry/validation/standard/' },
{ access='read', path='../../op-core/nuts/' },
{ access='read', path='./lib/espresso-tee-contracts/out/' },
]

# 5159 = selfdestruct deprecation
Expand Down Expand Up @@ -99,6 +123,16 @@ wrap_comments=true
# PROFILE: CI #
################################################################

[profile.ci]
# Limit parallel Solc jobs to avoid OOM on xlarge (16GB) runners.
# Default is CPU count (8). With espresso-tee-contracts submodule deps, the two 0.8.15
# groups (530 + 211 files) run together and exceed 16GB even at jobs=2. Serialize to 1.
jobs = 1
# Limit test execution threads to avoid OOM during forge test on xlarge (16GB) runners.
# espresso-tee-contracts increases per-test memory pressure; default (8 threads on xlarge)
# can exhaust 16GB. 4 threads keeps memory within budget while retaining parallelism.
threads = 4

[profile.ci.fuzz]
runs = 128

Expand All @@ -111,6 +145,9 @@ depth = 32
################################################################

[profile.cicoverage]
# Limit parallel Solc jobs to avoid OOM on xlarge (16GB) runners.
# espresso-tee-contracts adds additional compilation groups that exhaust memory without serialization.
jobs = 1
optimizer = false
compilation_restrictions = []

Expand All @@ -133,6 +170,7 @@ optimizer_runs = 0
# See the info in the "DEFAULT" profile to understand this section.
additional_compiler_profiles = [
{ name = "dispute", optimizer_runs = 0 },
{ name = "via-ir", via_ir = true },
]
compilation_restrictions = [
{ paths = "src/dispute/FaultDisputeGame.sol", optimizer_runs = 0 },
Expand All @@ -148,6 +186,8 @@ compilation_restrictions = [
{ paths = "src/L1/OptimismPortal2.sol", optimizer_runs = 0 },
{ paths = "src/universal/StorageSetter.sol", optimizer_runs = 0 }
]
# Limit parallel Solc jobs to avoid OOM on xlarge (16GB) runners.
jobs = 1

[profile.ciheavy.fuzz]
runs = 20000
Expand Down Expand Up @@ -201,6 +241,8 @@ depth = 32
################################################################

[profile.lite]
# Limit parallel Solc jobs to avoid OOM on xlarge (16GB) runners.
jobs = 1
optimizer = false
optimizer_runs = 0

Expand Down
73 changes: 73 additions & 0 deletions packages/contracts-bedrock/interfaces/L1/IBatchAuthenticator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IEspressoTEEVerifier} from "@espresso-tee-contracts/interface/IEspressoTEEVerifier.sol";
import {ISystemConfig} from "interfaces/L1/ISystemConfig.sol";

interface IBatchAuthenticator {
/// @notice Error thrown when an invalid address (zero address) is provided.
error InvalidAddress(address contract_);

/// @notice Error thrown when the fallback batcher caller does not match the expected address.
error UnauthorizedFallbackBatcher(address sender, address expected);

/// @notice Error thrown when `setEspressoBatcher` is called with the address
/// that is already the currently-active batcher.
error NoChange(address batcher);

/// @notice Error thrown when the Espresso TEE batcher caller does not match the configured espressoBatcher.
error UnauthorizedEspressoBatcher(address sender, address expected);

/// @notice Emitted when a batch info is authenticated. `caller` is the
/// address that invoked `authenticateBatchInfo`.
event BatchInfoAuthenticated(bytes32 commitment, address indexed caller);

/// @notice Emitted when a signer registration is initiated through this contract.
event SignerRegistrationInitiated(address indexed caller);

/// @notice Emitted when the Espresso batcher address is updated. `fromBlock`
/// is the L1 block number at which `newEspressoBatcher` becomes the
/// authorized batcher.
event EspressoBatcherUpdated(
address indexed oldEspressoBatcher,
address indexed newEspressoBatcher,
uint64 indexed fromBlock
);

/// @notice Emitted when the active batcher is switched.
event BatcherSwitched(bool indexed activeIsEspresso);

function authenticateBatchInfo(bytes32 commitment, bytes memory _signature) external;

function espressoTEEVerifier() external view returns (IEspressoTEEVerifier);

function nitroValidator() external view returns (address);

function owner() external view returns (address);

/// @notice Returns the currently-active Espresso batcher address (the value of the
/// latest history entry).
function espressoBatcher() external view returns (address);

/// @notice Number of entries in the Espresso batcher history.
function espressoBatcherHistoryLength() external view returns (uint256);

/// @notice Returns the Espresso batcher history entry at `_index`
/// (oldest first). Reverts on out-of-bounds index.
function espressoBatcherAt(uint32 _index) external view returns (address batcher_, uint64 fromBlock_);

/// @notice Returns the Espresso batcher address that was authorized at
/// L1 block `_l1Block`. Returns `address(0)` if `_l1Block` precedes the first
/// entry.
function espressoBatcherAtBlock(uint64 _l1Block) external view returns (address);

function registerSigner(bytes memory verificationData, bytes memory data) external;

function activeIsEspresso() external view returns (bool);

function systemConfig() external view returns (ISystemConfig);

function setActiveIsEspresso(bool _desired) external;

function setEspressoBatcher(address _newEspressoBatcher) external;
}
15 changes: 10 additions & 5 deletions packages/contracts-bedrock/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,14 @@ forge-build *ARGS:
--skip-simulation \
2>/dev/null || true

# Developer build command (faster).
# Developer build command (faster). Skip forge lint-on-build so we don't fail on 287+ warnings (e.g. unsafe-typecast in deps).
forge-build-dev *ARGS:
FOUNDRY_PROFILE=lite forge build {{ARGS}}
@# Use default profile (not lite) so the source build cache is shared — re-using
@# the default profile's compiled artifacts avoids recompiling 1000+ files from scratch
@# under the lite profile, which OOMs on xlarge (16GB) CI runners.
@# FOUNDRY_JOBS=1 serializes solc compilation to avoid OOM when adding test files to the
@# build (the default profile has no jobs limit, and 4 parallel solc groups exhaust 16GB).
FOUNDRY_JOBS=1 forge build {{ARGS}}

# Builds source contracts only.
build-source:
Expand All @@ -66,7 +71,6 @@ build-go-ffi:
clean:
rm -rf ./artifacts ./forge-artifacts ./cache ./scripts/go-ffi/go-ffi ./deployments/hardhat/*


########################################################
# TEST #
########################################################
Expand Down Expand Up @@ -189,7 +193,7 @@ coverage: build-go-ffi

# Runs contract coverage with lcov.
coverage-lcov *ARGS: build-go-ffi
forge coverage {{ARGS}} --report lcov --report-file lcov.info
FOUNDRY_PROFILE="${FOUNDRY_PROFILE:-default}" forge coverage {{ARGS}} --report lcov --report-file lcov.info

# Runs upgrade path variant of contract coverage tests.
coverage-upgrade *ARGS:
Expand Down Expand Up @@ -334,7 +338,8 @@ lint-check:

# Updates the selectors for the contracts
update-selectors:
forge selectors up --all
@# FOUNDRY_JOBS=1 serializes solc groups to avoid OOM on CI runners (large: 7.5GB).
FOUNDRY_JOBS=1 forge selectors up --all

# Checks for unused imports in Solidity contracts. Does not build contracts.
unused-imports-check-no-build:
Expand Down
1 change: 1 addition & 0 deletions packages/contracts-bedrock/lib/espresso-tee-contracts
Submodule espresso-tee-contracts added at bf6e60
3 changes: 2 additions & 1 deletion packages/contracts-bedrock/scripts/L2Genesis.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ contract L2Genesis is Script {
// script didn't set the nonce and we didn't want to change that behavior when
/// migrating genesis generation to Solidity.
function setPredeployProxies(Input memory _input) internal {
bytes memory code = vm.getDeployedCode("Proxy.sol:Proxy");
bytes memory code = vm.getDeployedCode("src/universal/Proxy.sol:Proxy"); // Espresso: disambiguate from OZ v5
// proxy/Proxy.sol artifact
uint160 prefix = uint160(0x420) << 148;

for (uint256 i = 0; i < Predeploys.PREDEPLOY_COUNT; i++) {
Expand Down
33 changes: 33 additions & 0 deletions packages/contracts-bedrock/scripts/checks/interfaces/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ var excludeContracts = []string{
// TODO: Interfaces that need to be fixed
"IInitializable", "IOptimismMintableERC20", "ILegacyMintableERC20",
"KontrolCheatsBase", "IResolvedDelegateProxy",

// Espresso dependencies
"IBatchAuthenticator",
"IEspressoTEEVerifier",
"IEspressoNitroTEEVerifier",
"ICertManager",
"BatchAuthenticator",
"INitroValidator",
// Espresso TEE submodule deep dependency interfaces (vendor-controlled pragma)
"IDaoAttestationResolver",
"IPCCSRouter",
"IQuoteVerifier",
}

// excludeSourceContracts is a list of contracts that are allowed to not have interfaces
Expand Down Expand Up @@ -157,6 +169,17 @@ func processFile(artifactPath string) (*common.Void, []error) {
return nil, []error{fmt.Errorf("%s: Interface does not start with 'I'", contractName)}
}

// Espresso: skip interface artifacts that originate from lib/ (e.g. OZ v5 interfaces
Comment thread
piersy marked this conversation as resolved.
// compiled transitively via espresso-tee-contracts). These are vendor-controlled and
// should not be subject to our interface version or ABI-match requirements.
forgeArtifact, err := common.ReadForgeArtifact(artifactPath)
if err != nil {
return nil, []error{fmt.Errorf("failed to read forge artifact: %w", err)}
}
if strings.HasPrefix(forgeArtifact.Ast.AbsolutePath, "lib/") {
return nil, nil
}

semver, err := getContractSemver(artifact)
if err != nil {
return nil, []error{fmt.Errorf("failed to get contract semver: %w", err)}
Expand All @@ -173,6 +196,16 @@ func processFile(artifactPath string) (*common.Void, []error) {
return nil, nil
}

// Espresso: skip ABI comparison if the corresponding contract artifact is from lib/
// (e.g. OZ v5 Ownable.json replacing OZ v4 Ownable.json as the "main" artifact).
correspondingForgeArtifact, err := common.ReadForgeArtifact(correspondingContractFile)
if err != nil {
return nil, []error{fmt.Errorf("failed to read corresponding forge artifact: %w", err)}
}
if strings.HasPrefix(correspondingForgeArtifact.Ast.AbsolutePath, "lib/") {
return nil, nil
}

contractArtifact, err := readArtifact(correspondingContractFile)
if err != nil {
return nil, []error{fmt.Errorf("failed to read corresponding contract artifact: %w", err)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ var excludedFiles = []string{
"src/periphery/Transactor.sol",
"src/periphery/monitoring/DisputeMonitorHelper.sol",
"src/universal/SafeSend.sol",
// BatchAuthenticator is imported by scripts at =0.8.25 AND by test groups at 0.8.28
// (via OZ v5 ^0.8.20 transitive deps). An exact pragma would break one or the other
// compilation group in Foundry's multi-version resolver.
"src/L1/BatchAuthenticator.sol",
}

func main() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,6 @@ contracts = [
"Constants_Test", # Invalid naming pattern - doesn't specify function or Uncategorized
"LivenessModule2_TestUtils", # Test utils library in LivenessModule2 test file
"L1Block_SetCustomGasToken_Test", # Custom gas token tests hosted in the L1Block test file
"BatchAuthenticator_Fork_Test", # Espresso fork tests - 'Fork' is a descriptor, not a function name
"MockSystemConfig", # Espresso mock helper contract inside BatchAuthenticator.t.sol
]
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,10 @@ library ChainAssertions {
);

Blueprint.Preamble memory proxyPreamble = Blueprint.parseBlueprintPreamble(address(blueprints.proxy).code);
require(keccak256(proxyPreamble.initcode) == keccak256(DeployUtils.getCode("Proxy")), "CHECK-OPCM-170");
require(
keccak256(proxyPreamble.initcode) == keccak256(DeployUtils.getCode("src/universal/Proxy.sol:Proxy")),
"CHECK-OPCM-170"
); // Espresso: disambiguate from OZ v5 proxy/Proxy.sol artifact

Blueprint.Preamble memory proxyAdminPreamble =
Blueprint.parseBlueprintPreamble(address(blueprints.proxyAdmin).code);
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts-bedrock/scripts/deploy/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ contract Deploy is Deployer {
DeployUtils.create2AndSave({
_save: artifacts,
_salt: keccak256(abi.encode(_implSalt(), _name)),
_name: "Proxy",
_name: "src/universal/Proxy.sol:Proxy", // Espresso: disambiguate from OZ v5 proxy/Proxy.sol artifact
_nick: _name,
_args: DeployUtils.encodeConstructor(abi.encodeCall(IProxy.__constructor__, (_proxyOwner)))
})
Expand Down
Loading
Loading