Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
eae9c76
add migration intervals tracking and verifying settlement layer
0xValera Jan 23, 2026
bdb795c
rebuild
0xValera Jan 23, 2026
9184f42
Merge branch 'draft-v31' of https://github.com/matter-labs/era-contra…
0xValera Feb 2, 2026
4c91c90
address various comments
0xValera Feb 5, 2026
fd3c66a
hardening of requires
StanislavBreadless Feb 10, 2026
c6d93b8
remove code duplication
StanislavBreadless Feb 10, 2026
db91381
add tests, solve stack too deep and make message root not immutable i…
StanislavBreadless Feb 10, 2026
1c4420d
make chain asset handler an immutable
StanislavBreadless Feb 10, 2026
89dd172
use correct perm values
StanislavBreadless Feb 10, 2026
078ab55
sync with base
StanislavBreadless Feb 10, 2026
3f40e17
small refactor + comments
StanislavBreadless Feb 10, 2026
516078a
update zkstack out
StanislavBreadless Feb 11, 2026
2ae2c97
delete an unused method
StanislavBreadless Feb 11, 2026
acbb25c
upd artifacts
StanislavBreadless Feb 11, 2026
1a7e25f
fix errors lint
StanislavBreadless Feb 11, 2026
489fa89
fix typos
StanislavBreadless Feb 11, 2026
25f3388
isSet -> isActive for more careful tracking of intervals
StanislavBreadless Feb 11, 2026
92c29f3
remove unused errors
StanislavBreadless Feb 11, 2026
c4698bb
minor renames and cleanup
StanislavBreadless Feb 11, 2026
182885f
regen the local core/ctm
StanislavBreadless Feb 11, 2026
9ed4b81
Merge branch 'draft-v31' into vg/settlement-layer-trust-assumptions
StanislavBreadless Feb 11, 2026
927609d
selectors
StanislavBreadless Feb 12, 2026
4180532
Update l1-contracts/contracts/core/chain-asset-handler/IChainAssetHan…
StanislavBreadless Feb 12, 2026
7b7a453
remove duplicate tests
StanislavBreadless Feb 12, 2026
7a36241
sync with draft v31
StanislavBreadless Feb 12, 2026
cb035e8
fix compile
StanislavBreadless Feb 12, 2026
35f4bfc
fmt
StanislavBreadless Feb 12, 2026
2f0b57a
sync with base
StanislavBreadless Feb 12, 2026
7f1095e
fix lint
StanislavBreadless Feb 12, 2026
8a411c4
chore: Updated hashes from CI
Feb 12, 2026
804d9bd
sync with base
StanislavBreadless Feb 12, 2026
bf43729
lint
StanislavBreadless Feb 12, 2026
ac89918
chore: Updated hashes from CI
Feb 12, 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
792 changes: 396 additions & 396 deletions AllContractsHashes.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions l1-contracts/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ address result = _tryAddress(target, "someFunction()");
- They mask initialization issues and timing problems
- The codebase should fail fast and clearly, not silently return defaults

### Constructors and Immutables

- ONLY contracts deployed on L1 should have immutables. Contracts on L2 are deployed within zksync os environment and so and so DO NOT SUPPORT CONSTRUCTORS ALL (and so no immutable can be set). It is important that the `*Base` contracts that the L2 contracts inherit from dont have immutables or constructors too.
- If you want to add an immutable for L1, always double check whether it is possible to deterministically obtain from other contracts.
- If there is variable that can be an immutable on L1, but we need a similar field on L2, a common pattern is to create a method in the base contract that can be inherited by both. On L2 it can be either a constant (esp if it is an L2 built-in contract address) or a storage variable that must be initialized within during the genesis. For example, look how `initL2` functions are used.

## Debugging Strategies

When debugging Solidity compilation or script failures:
Expand Down
9 changes: 9 additions & 0 deletions l1-contracts/contracts/common/Config.sol
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,15 @@ PubdataPricingMode constant DEFAULT_PUBDATA_PRICING_MODE = PubdataPricingMode.Ro
/// @dev Default maximum gas limit for priority transactions during chain creation.
uint64 constant DEFAULT_PRIORITY_TX_MAX_GAS_LIMIT = 72_000_000;

/// @dev Migration number used when a chain migrates from L1 to a settlement layer.
uint256 constant MIGRATION_NUMBER_L1_TO_SETTLEMENT_LAYER = 1;

/// @dev Migration number used when a chain returns from a settlement layer back to L1.
uint256 constant MIGRATION_NUMBER_SETTLEMENT_LAYER_TO_L1 = 2;

/// @dev Iterated migrations are not supported; chain can migrate only to settlement layer and back once.
uint256 constant MAX_ALLOWED_NUMBER_OF_MIGRATIONS = 2;

/// @dev The mask that should be applied to the packed log data containing both the number of L2 and L1 transactions
/// processed in the batch. Applying this mask is equivalent to calculating modulo 2**128.
uint256 constant PACKED_NUMBER_OF_L1_TRANSACTIONS_LOG_MASK = 0xffffffffffffffffffffffffffffffff;
Expand Down
2 changes: 0 additions & 2 deletions l1-contracts/contracts/common/L1ContractErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ error BadTransferDataLength();
error BaseTokenGasPriceDenominatorNotSet();
// 0x55ad3fd3
error BatchHashMismatch(bytes32 expected, bytes32 actual);
// 0x2078a6a0
error BatchNotExecuted(uint256 batchNumber);
// 0xbd4455ff
error BatchNumberMismatch(uint256 expectedBatchNumber, uint256 providedBatchNumber);
// 0x6cf12312
Expand Down
6 changes: 4 additions & 2 deletions l1-contracts/contracts/core/bridgehub/BridgehubBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,10 @@ abstract contract BridgehubBase is IBridgehubBase, ReentrancyGuard, Ownable2Step
/// @dev used to indicate the currently active settlement layer for a given chainId
mapping(uint256 chainId => uint256 activeSettlementLayerChainId) public settlementLayer;

/// @notice shows whether the given chain can be used as a settlement layer.
/// @dev the Gateway will be one of the possible settlement layers. The L1 is also a settlement layer.
/// @notice Shows whether a chain can currently be selected as a migration target settlement layer.
/// @dev This does NOT represent historical settlement layers used in message proof verification.
/// @dev Historical settlement layer assignments are tracked in ChainAssetHandler `_migrationInterval`.
/// @dev The Gateway will be one of the possible settlement layers. L1 is also a settlement layer.
/// @dev Sync layer chain is expected to have .. as the base token.
mapping(uint256 chainId => bool isWhitelistedSettlementLayer) public whitelistedSettlementLayers;

Expand Down
16 changes: 12 additions & 4 deletions l1-contracts/contracts/core/bridgehub/L1BridgehubErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,26 @@ error ChainExists();
error CurrentBatchNumberAlreadySet();
// 0x68d91b49
error DepthMoreThanOneForRecursiveMerkleProof();
// 0xd9d3fc89
error HistoricalSettlementLayerMismatch(uint256 expectedSettlementLayer, uint256 actualSettlementLayer);
// 0x48857c1d
error IncorrectChainAssetId(bytes32 assetId, bytes32 assetIdFromChainId);
// 0xf5e39c1f
error IncorrectSender(address prevMsgSender, address chainAdmin);
// 0x896555dc
error InvalidSettlementLayerForBatch(uint256 chainId, uint256 batchNumber, uint256 claimedSettlementLayer);
// 0x47d42b1b
error IteratedMigrationsNotSupported();
// 0xc3bd3c65
error LocallyNoChainsAtGenesis();
// 0x913183d8
error MessageRootNotRegistered();
// 0x338fe0e7
error MigrationIntervalInvalid();
// 0x81c5808d
error MigrationIntervalNotSet();
// 0x4010a88d
error MigrationNotToL1();
// 0x12b08c62
error MigrationNumberAlreadySet();
// 0xde1362a2
error MigrationNumberMismatch(uint256 _expected, uint256 _actual);
// 0x7f4316f3
Expand All @@ -56,8 +62,6 @@ error NotOwnerViaRouter(address msgSender, address originalCaller);
error NotRelayedSender(address msgSender, address settlementLayerRelaySender);
// 0xb35a7373
error NotSystemContext(address _sender);
// 0xb30ebfd8
error NotWhitelistedSettlementLayer(uint256 chainId);
// 0x3db511f4
error OnlyAssetTracker(address, address);
// 0x527b87c7
Expand All @@ -68,10 +72,14 @@ error OnlyBridgehubOrChainAssetHandler(address sender, address bridgehub, addres
error OnlyChain(address msgSender, address zkChainAddress);
// 0xec76af13
error OnlyGateway();
// 0x8d14ca84
error OnlyL1();
// 0x6b75db8c
error OnlyOnSettlementLayer();
// 0xb78dbaa7
error SecondBridgeAddressTooLow(address secondBridgeAddress, address minSecondBridgeAddress);
// 0xefb272e2
error SettlementLayerMustNotBeL1();
// 0x36917565
error SLHasDifferentCTM();
// 0x90c7cbf1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import {IAssetRouterBase} from "../../bridge/asset-router/IAssetRouterBase.sol";
import {IL1AssetRouter} from "../../bridge/asset-router/IL1AssetRouter.sol";
import {INativeTokenVaultBase} from "../../bridge/ntv/INativeTokenVaultBase.sol";

import {L1_SETTLEMENT_LAYER_VIRTUAL_ADDRESS} from "../../common/Config.sol";
import {IncorrectChainAssetId, IncorrectSender, MigrationNotToL1, MigrationNumberAlreadySet, MigrationNumberMismatch, NotSystemContext, OnlyChain, SLHasDifferentCTM, ZKChainNotRegistered, IteratedMigrationsNotSupported} from "../bridgehub/L1BridgehubErrors.sol";
import {L1_SETTLEMENT_LAYER_VIRTUAL_ADDRESS, MIGRATION_NUMBER_L1_TO_SETTLEMENT_LAYER, MIGRATION_NUMBER_SETTLEMENT_LAYER_TO_L1, MAX_ALLOWED_NUMBER_OF_MIGRATIONS} from "../../common/Config.sol";
import {IncorrectChainAssetId, IncorrectSender, MigrationNotToL1, MigrationNumberMismatch, NotSystemContext, OnlyChain, SLHasDifferentCTM, ZKChainNotRegistered, IteratedMigrationsNotSupported} from "../bridgehub/L1BridgehubErrors.sol";
import {ChainIdNotRegistered, MigrationPaused, NotAssetRouter} from "../../common/L1ContractErrors.sol";
import {L2_SYSTEM_CONTEXT_SYSTEM_CONTRACT_ADDR} from "../../common/l2-helpers/L2ContractAddresses.sol";

Expand Down Expand Up @@ -76,6 +76,9 @@ abstract contract ChainAssetHandlerBase is
IAssetRouterBase internal DEPRECATED_ASSET_ROUTER;

/// @notice Used to track the number of times each chain has migrated.
/// @dev It is assumed that during the release of the v31 upgrade all chains settle on L1,
/// so they will all start with `migrationNumber` equal to 0. Note, that ZKsync Era that used to settle on ZK Gateway
/// will also start with migration number equal to 0.
/// NOTE: this mapping may be deprecated in the future, don't rely on it!
mapping(uint256 chainId => uint256 migrationNumber) public migrationNumber;

Expand Down Expand Up @@ -122,18 +125,6 @@ abstract contract ChainAssetHandlerBase is
_;
}

/// @notice Sets the migration number for a chain on the Gateway when the chain's DiamondProxy upgrades.
function setMigrationNumberForV31(uint256 _chainId) external onlyChain(_chainId) {
require(migrationNumber[_chainId] == 0, MigrationNumberAlreadySet());
bool isOnThisSettlementLayer = block.chainid == IBridgehubBase(_bridgehub()).settlementLayer(_chainId);
bool shouldIncrementMigrationNumber = (isOnThisSettlementLayer && block.chainid != _l1ChainId()) ||
(!isOnThisSettlementLayer && block.chainid == _l1ChainId());
/// Note we don't increment the migration number if the chain migrated to GW and back to L1 previously.
if (shouldIncrementMigrationNumber) {
migrationNumber[_chainId] = 1;
}
}

/*//////////////////////////////////////////////////////////////
Chain migration
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -161,21 +152,19 @@ abstract contract ChainAssetHandlerBase is
returns (bytes memory bridgehubMintData)
{
BridgehubBurnCTMAssetData memory bridgehubBurnData = abi.decode(_data, (BridgehubBurnCTMAssetData));
uint256 chainId = bridgehubBurnData.chainId;
require(
_assetId == IBridgehubBase(_bridgehub()).ctmAssetIdFromChainId(bridgehubBurnData.chainId),
IncorrectChainAssetId(
_assetId,
IBridgehubBase(_bridgehub()).ctmAssetIdFromChainId(bridgehubBurnData.chainId)
)
_assetId == IBridgehubBase(_bridgehub()).ctmAssetIdFromChainId(chainId),
IncorrectChainAssetId(_assetId, IBridgehubBase(_bridgehub()).ctmAssetIdFromChainId(chainId))
);
address zkChain = IBridgehubBase(_bridgehub()).getZKChain(bridgehubBurnData.chainId);
address zkChain = IBridgehubBase(_bridgehub()).getZKChain(chainId);

bytes memory ctmMintData;
// to avoid stack too deep
{
address ctm;
(zkChain, ctm) = IBridgehubBase(_bridgehub()).forwardedBridgeBurnSetSettlementLayer(
bridgehubBurnData.chainId,
chainId,
_settlementChainId
);

Expand All @@ -186,10 +175,7 @@ abstract contract ChainAssetHandlerBase is
revert IncorrectSender(_originalCaller, IZKChain(zkChain).getAdmin());
}

ctmMintData = IChainTypeManager(ctm).forwardedBridgeBurn(
bridgehubBurnData.chainId,
bridgehubBurnData.ctmData
);
ctmMintData = IChainTypeManager(ctm).forwardedBridgeBurn(chainId, bridgehubBurnData.ctmData);

// For security reasons, chain migration is temporarily restricted to settlement layers with the same CTM
if (
Expand All @@ -202,23 +188,87 @@ abstract contract ChainAssetHandlerBase is
if (block.chainid != _l1ChainId()) {
require(_settlementChainId == _l1ChainId(), MigrationNotToL1());
}
_setMigrationInProgressOnL1(bridgehubBurnData.chainId);
_setMigrationInProgressOnL1(chainId);
}
bytes memory chainMintData = IZKChain(zkChain).forwardedBridgeBurn(
// to avoid stack too deep
bridgehubMintData = _finalizeBridgeBurn({
_chainId: chainId,
_settlementChainId: _settlementChainId,
_assetId: _assetId,
_zkChain: zkChain,
_originalCaller: _originalCaller,
_ctmMintData: ctmMintData,
_chainData: bridgehubBurnData.chainData
});
}

/// @dev Handles chain burn, migration bookkeeping, and builds the bridgehub mint data.
/// @dev Extracted from bridgeBurn to avoid stack-too-deep.
function _finalizeBridgeBurn(
uint256 _chainId,
uint256 _settlementChainId,
bytes32 _assetId,
address _zkChain,
address _originalCaller,
bytes memory _ctmMintData,
bytes memory _chainData
) internal returns (bytes memory) {
bytes memory chainMintData = IZKChain(_zkChain).forwardedBridgeBurn(
_settlementChainId == _l1ChainId()
? L1_SETTLEMENT_LAYER_VIRTUAL_ADDRESS
: IBridgehubBase(_bridgehub()).getZKChain(_settlementChainId),
_originalCaller,
bridgehubBurnData.chainData
_chainData
);
uint256 currentMigrationNum = migrationNumber[_chainId];
// Iterated migrations are not supported to avoid asset migration number complications related to token balance migration.
// This means a chain can migrate to GW and back to L1 but only once.
require(migrationNumber[bridgehubBurnData.chainId] < 2, IteratedMigrationsNotSupported());
++migrationNumber[bridgehubBurnData.chainId];
require(currentMigrationNum < MAX_ALLOWED_NUMBER_OF_MIGRATIONS, IteratedMigrationsNotSupported());
++currentMigrationNum;
migrationNumber[_chainId] = currentMigrationNum;

uint256 batchNumber = IMessageRoot(_messageRoot()).currentChainBatchNumber(bridgehubBurnData.chainId);
uint256 batchNumber = IMessageRoot(_messageRoot()).currentChainBatchNumber(_chainId);

bytes32 assetId = IBridgehubBase(_bridgehub()).baseTokenAssetId(bridgehubBurnData.chainId);
// Track migration interval for settlement layer validation.
// When migrating FROM L1 TO a settlement layer, record the last L1 batch number and the SL chain ID.
if (block.chainid == _l1ChainId()) {
_recordMigrationToSL(_chainId, _settlementChainId, batchNumber, currentMigrationNum);
}

bytes memory bridgehubMintData = _buildBridgehubMintData({
_chainId: _chainId,
_batchNumber: batchNumber,
_ctmMintData: _ctmMintData,
_chainMintData: chainMintData,
_currentMigrationNum: currentMigrationNum
});

emit MigrationStarted(_chainId, currentMigrationNum, _assetId, _settlementChainId);

return bridgehubMintData;
}

function _recordMigrationToSL(
uint256 _chainId,
uint256 _settlementChainId,
uint256 _batchNumber,
uint256 _currentMigrationNum
) internal virtual;

function _recordMigrationFromSL(
uint256 _chainId,
uint256 _batchNumber,
uint256 _currentMigrationNum
) internal virtual;

function _buildBridgehubMintData(
uint256 _chainId,
uint256 _batchNumber,
bytes memory _ctmMintData,
bytes memory _chainMintData,
uint256 _currentMigrationNum
) internal view returns (bytes memory) {
bytes32 assetId = IBridgehubBase(_bridgehub()).baseTokenAssetId(_chainId);
TokenBridgingData memory baseTokenBridgingData = TokenBridgingData({
assetId: assetId,
originToken: address(0),
Expand All @@ -233,22 +283,17 @@ abstract contract ChainAssetHandlerBase is
baseTokenBridgingData.originChainId = l1Ntv.originChainId(assetId);
}

BridgehubMintCTMAssetData memory bridgeMintStruct = BridgehubMintCTMAssetData({
chainId: bridgehubBurnData.chainId,
baseTokenBridgingData: baseTokenBridgingData,
batchNumber: batchNumber,
ctmData: ctmMintData,
chainData: chainMintData,
migrationNumber: migrationNumber[bridgehubBurnData.chainId]
});
bridgehubMintData = abi.encode(bridgeMintStruct);

emit MigrationStarted(
bridgehubBurnData.chainId,
migrationNumber[bridgehubBurnData.chainId],
_assetId,
_settlementChainId
);
return
abi.encode(
BridgehubMintCTMAssetData({
chainId: _chainId,
baseTokenBridgingData: baseTokenBridgingData,
batchNumber: _batchNumber,
ctmData: _ctmMintData,
chainData: _chainMintData,
migrationNumber: _currentMigrationNum
})
);
}

function _setMigrationInProgressOnL1(uint256 _chainId) internal virtual {}
Expand All @@ -271,11 +316,20 @@ abstract contract ChainAssetHandlerBase is
/// If we are not migrating for the first time, we check that the migration number is correct.
if (currentMigrationNumber != 0 && block.chainid == _l1ChainId()) {
require(
currentMigrationNumber + 1 == bridgehubMintData.migrationNumber,
MigrationNumberMismatch(currentMigrationNumber + 1, bridgehubMintData.migrationNumber)
currentMigrationNumber == MIGRATION_NUMBER_L1_TO_SETTLEMENT_LAYER,
MigrationNumberMismatch(MIGRATION_NUMBER_L1_TO_SETTLEMENT_LAYER, currentMigrationNumber)
);
require(
bridgehubMintData.migrationNumber == MIGRATION_NUMBER_SETTLEMENT_LAYER_TO_L1,
MigrationNumberMismatch(MIGRATION_NUMBER_SETTLEMENT_LAYER_TO_L1, bridgehubMintData.migrationNumber)
);
}
migrationNumber[bridgehubMintData.chainId] = bridgehubMintData.migrationNumber;
_recordMigrationFromSL(
bridgehubMintData.chainId,
bridgehubMintData.batchNumber,
bridgehubMintData.migrationNumber
);

(address zkChain, address ctm) = IBridgehubBase(_bridgehub()).forwardedBridgeMint(
_assetId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ pragma solidity ^0.8.24;

import {IAssetHandler} from "../../bridge/interfaces/IAssetHandler.sol";

/// @notice Tracks migration batch numbers for a chain that migrated to a settlement layer and back.
/// @param migrateToGWBatchNumber The last batch executed on L1 before migrating TO the gateway.
/// @param migrateFromGWBatchNumber The last batch executed on Gateway before migrating back to L1.
/// @param settlementLayerChainId The chain ID of the settlement layer where the chain settled during the time period.
/// @param isActive Whether the chain is actively settling on the settlement layer right now.
struct MigrationInterval {
uint256 migrateToGWBatchNumber;
uint256 migrateFromGWBatchNumber;
uint256 settlementLayerChainId;
bool isActive;
}

/// @author Matter Labs
/// @custom:security-contact security@matterlabs.dev
interface IChainAssetHandler is IAssetHandler {
Expand Down Expand Up @@ -33,8 +45,6 @@ interface IChainAssetHandler is IAssetHandler {

function migrationNumber(uint256 _chainId) external view returns (uint256);

function setMigrationNumberForV31(uint256 _chainId) external;

/// @dev Denotes whether the migrations of chains is paused.
function migrationPaused() external view returns (bool);

Expand Down
Loading
Loading