> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://docs.prividium.com/api/mcp` to find what you need.
>
> **Have feedback?** Use `submit_feedback` on the same MCP server.

Bridging moves assets between Ethereum L1 and a Prividium™ network. Enabling it requires configuration across **three
independent layers**.

1. **Permissions-API rules**: function permissions on the L2 system contracts (and per-token L2 ERC-20 contracts) that
   the User Panel calls during bridging.
2. **L1 Transaction Filterer**: an optional L1 contract that controls which L1→L2 deposits are accepted.
3. **Frontend settings**: user-panel environment variables that show or hide the bridge UI.

:::info[Configure all three layers]
Bridging works only after you configure all three layers. Set the permission rules, configure the Transaction
Filterer, and enable the frontend settings. Each layer is independent, so you must set every one of them. Enabling the
frontend toggle, for example, leaves bridging inactive until the permission rules exist as well.
:::

## How bridging uses permissions

Prividium™ applies its [deny-by-default permission model](/configuring/contracts/permissions) to direct calls on system
contracts. Functions without permission rules default to **Forbidden**. [System-contract sync](/configuring/contracts/system-contracts)
registers contracts without creating permissions. Create the rules below to enable bridging for a new environment.

Deposits and withdrawals use permissions differently:

* **Deposits** move funds in through an L1 transaction. The L2 crediting runs as an automatic priority-queue transaction
  that bypasses Prividium and needs **no L2 write permission**. Before it can start a deposit, though, the User Panel
  reads token metadata from `L2NativeTokenVault` on L2. If those reads are Forbidden, the User Panel cannot start the
  deposit. **Deposits therefore require L2 read permissions.**
* **Withdrawals** are user-signed L2 transactions. The User Panel needs both the **read** permissions (same token
  metadata) and **write** permissions on the withdrawal functions.

The four bridge entry-point and token-resolution contracts are registered automatically:

| Contract             | Address                                      |
| -------------------- | -------------------------------------------- |
| `L2BaseToken`        | `0x000000000000000000000000000000000000800A` |
| `L2AssetRouter`      | `0x0000000000000000000000000000000000010003` |
| `L2NativeTokenVault` | `0x0000000000000000000000000000000000010004` |
| `InteropCenter`      | `0x000000000000000000000000000000000001000d` |

Create the permission rules below from each contract's **Function Permissions** tab in the Admin Dashboard, or through
the API.

## Token-resolution read permissions

The User Panel reads token metadata from L2 before every deposit and withdrawal, for both ETH and ERC-20. Grant these
reads as `All Users` (public) because they are read-only view calls. You can also gate them by role if your policy
requires it.

`L2NativeTokenVault` at `0x0000000000000000000000000000000000010004` (read access):

| Function                  | Selector     | Why needed                                                 |
| ------------------------- | ------------ | ---------------------------------------------------------- |
| `L1_CHAIN_ID()`           | `0x2f90b184` | Resolve the chain's asset identifiers                      |
| `BASE_TOKEN_ASSET_ID()`   | `0xcb944dec` | Detect whether the asset is the base token                 |
| `WETH_TOKEN()`            | `0x37d277d4` | Returns the configured wrapped base token address          |
| `originChainId(bytes32)`  | `0x5f3455b5` | Determine where the asset originates                       |
| `l2TokenAddress(address)` | `0xf5f15168` | Map the L1 token to its L2 address                         |
| `assetId(address)`        | `0xfd3f60df` | Resolve the L2 token's asset id (ERC-20 withdrawal on v33) |

Withdrawals on protocol v31 also read one function on `L2AssetRouter` at `0x0000000000000000000000000000000000010003`
(read access). v33 removed it; SDK releases for v33 use `assetId(address)` on the vault instead:

| Function                  | Selector     | Why needed                                           |
| ------------------------- | ------------ | ---------------------------------------------------- |
| `l1TokenAddress(address)` | `0xf54266a2` | Map the L2 token back to its L1 address (withdrawal) |

The User Panel reads the first four `L2NativeTokenVault` functions on **every** deposit and withdrawal, including plain
ETH. Any user who lacks full sequencer access needs all four permitted to bridge in either direction.

## Enabling deposits

A deposit requires:

1. The **token-resolution read permissions** above (the four common functions for ETH; add `l2TokenAddress` for ERC-20).
2. The **L1 Transaction Filterer** to allow the deposit (see [Transaction Filterer](#transaction-filterer)).

The L2 execution of a deposit is a priority-queue transaction and needs **no** L2 write permission. The reads above are
the only L2-side configuration a deposit needs.

## Enabling withdrawals

A withdrawal requires the read permissions above **plus** the following **write** permissions. Grant as `All Users`
(public) or `Check Role`.

The withdrawal entry point depends on the chain's protocol version. `zksync-js` reads the version from L1 and picks
the route, so grant the row that matches your chain. Grant both sets on a chain that is about to upgrade from v31 to
v33, so withdrawals keep working through the upgrade.

| Contract             | Function                                            | Selector     | Asset                 | Protocol |
| -------------------- | --------------------------------------------------- | ------------ | --------------------- | -------- |
| `InteropCenter`      | `sendBundle(bytes,(bytes,bytes,bytes[])[],bytes[])` | `0x5ef7e104` | ETH / base and ERC-20 | v33+     |
| `L2BaseToken`        | `withdraw(address)`                                 | `0x51cff8d9` | ETH / base            | v31      |
| `L2AssetRouter`      | `withdraw(bytes32,bytes)`                           | `0x4a2e35ba` | ERC-20                | v31      |
| `L2NativeTokenVault` | `ensureTokenIsRegistered(address)`                  | `0x19a2a285` | ERC-20                | all      |
| L2 ERC-20 token      | `approve(address,uint256)`                          | `0x095ea7b3` | ERC-20                | all      |

Withdrawals must also be enabled in the frontend with `VITE_WITHDRAWAL_ENABLED=true` (see
[Frontend configuration](#frontend-configuration)).

### After a chain ID change

Changing a chain's ID invalidates every wallet that already has the network saved. Wallets ignore
`wallet_addEthereumChain` for a chain ID they already know, so an entry pointing at the raw `/rpc` URL survives the
change and every send fails with `eth_sendRawTransaction: Unauthorized` even though reads, gas estimation and
transaction authorization all succeed. Users must remove the network from their wallet and add it again from the User
Panel, which registers their personal `/rpc/wallet/{token}` URL.

## ERC-20 considerations

* **Per-token permissions are required.** Each bridgeable ERC-20 has its own L2 address and needs its own rules:
  `approve(address,uint256)` (write) for withdrawals, and `allowance(address,address)` and `balanceOf(address)` as
  reads. Add a rule for every token you intend to bridge, because no wildcard covers them.
* **Beacon registration.** Register the token's deployment-specific beacon and grant read access to `implementation()`
  for token approvals. Startup sync does not register deployment-specific beacons.
* **Token registration.** `ensureTokenIsRegistered(address)` registers the token in `L2NativeTokenVault` and returns its
  asset id; it must be permitted (write) before an ERC-20 withdrawal can be routed.
* **Frontend token list.** Additional ERC-20s shown in the user panel are configured with `VITE_BRIDGE_TOKENS` (JSON).
  The L2 token address for each entry must be resolved correctly; see the private user-panel environment variable docs
  for the schema.

## Umbrella Permissions in Multi-Org Mode

An umbrella permission lets a permitted entry point authorize internal contract calls. Mark the withdrawal entry-point
permission as an umbrella to use this inheritance. With `MULTI_ORG_ENABLED=true`, the policy listener's `/judge`
endpoint still checks those internal calls.

Startup sync registers these six shared dependencies zone-wide. Only the listed methods can inherit umbrella
authorization by default; other methods require their own permissions. Registration and organization ownership checks
still apply. Argument restrictions apply where an administrator has configured a rule; startup sync creates no permission rules.

| Contract                 | Address                                      | Methods Allowed Under an Umbrella                                                                                     |
| ------------------------ | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| L2Bridgehub              | `0x0000000000000000000000000000000000010002` | `baseTokenAssetId(uint256)`                                                                                           |
| L2InteropAttributeParser | `0x0000000000000000000000000000000000010015` | `parseAttributes(bytes[],uint8)`, `parseAtomicSend(bytes[])`                                                          |
| L2BaseTokenHolder        | `0x0000000000000000000000000000000000010011` | `burnAndStartBridging(uint256)`                                                                                       |
| L2AssetTracker           | `0x000000000000000000000000000000000001000f` | `handleInitiateBaseTokenBridgingOnL2(uint256,uint256)`, `handleInitiateBridgingOnL2(uint256,bytes32,uint256,uint256)` |
| SystemContext            | `0x000000000000000000000000000000000000800b` | `currentSettlementLayerChainId()`                                                                                     |
| L1Messenger              | `0x0000000000000000000000000000000000008008` | `sendToL1(bytes)` and the empty-calldata gas-burning fallback                                                         |

The messenger uses its fallback internally to burn message gas. Its on-chain implementation rejects other callers and
always reverts that subcall. Direct empty-calldata calls receive no new permission.

Startup sync also registers `L1MessengerHook` at `0x0000000000000000000000000000000000007001`.
The hook, the four entry-point and token-resolution contracts, and application contracts retain their existing
umbrella behavior.
This method policy covers only the six dependencies in the table.

### Optional Shared Accounting Reads

`ALLOW_UMBRELLA_SYSTEM_READS` enables umbrella inheritance for a code-maintained allowlist of optional system-contract
reads. It defaults to `false`. The current allowlist contains these four `L2AssetTracker` getters:

* `interopInfo(bytes32)`
* `chainBalance(uint256,bytes32)`
* `totalPreV31TotalSupply(bytes32)`
* `isAssetRegistered(bytes32)`

Setting the flag to `true` lets these getters inherit umbrella authorization without their own method permissions.
They expose shared accounting or registration state, which can reveal information about other organizations through an
authorized umbrella call. Enable the flag only when this exposure matches your access policy.

Direct getter calls still require explicit permissions. The flag grants no additional access to unknown selectors or
other write methods. Organization ownership checks and configured argument restrictions remain enforced.
Required bridge operations, including the required reads in the table, work with either setting.

Adding getters to the allowlist expands access in environments where the flag is already enabled.
Review allowlist changes before upgrading those environments.

This protection requires multi-org judge enforcement. Environments without judge or with multi-org disabled retain
their existing authorization behavior. Restart every permissions-api replica with the same flag value after changing it.

## Transaction Filterer

The Transaction Filterer is an **optional L1 contract** that gates which L1→L2 transactions (including deposits) are
queued. It is the deposit-side admission control and is independent of the L2 permission rules above.

Key behaviors:

* `setDepositsAllowed(bool)` toggles whether non-whitelisted addresses may deposit.
* When deposits are allowed, a non-whitelisted EOA may deposit **only to its own address**.
* Whitelisted addresses (`grantWhitelist` / `revokeWhitelist`) may perform arbitrary L1→L2 transactions.
* L1 contract addresses are aliased during deposit, so L1 contracts cannot deposit unless explicitly whitelisted.

If deposits are rejected at L1 (no L2 priority transaction appears) even though permissions are correct, check the
filterer's `depositsAllowed` flag and the sender's whitelist status. For deployment and the full method reference, see
[Transaction Filterer](/configuring/chain/tx-filterer).

## Frontend configuration

These user-panel environment variables control the bridge **UI only**. They do not create permission rules and do not
affect the Transaction Filterer.

| Variable                  | Purpose                                  | Default |
| ------------------------- | ---------------------------------------- | ------- |
| `VITE_BRIDGING_ENABLED`   | Show the bridge UI (deposits)            | `false` |
| `VITE_WITHDRAWAL_ENABLED` | Show withdrawal features in the UI       | `false` |
| `VITE_BRIDGE_TOKENS`      | Additional ERC-20 tokens for bridging    | `[]`    |
| `VITE_L1_RPC_URL`         | L1 RPC endpoint (required when bridging) | –       |

See the private user-panel environment variable docs for the full list.

## Verification checklist

Confirm a permission **rule exists** for each `(contract, function)` below, that its access type matches, and that its
rule is `All Users` (public) or a `Check Role` rule the user holds. None should remain Forbidden.

**Deposit — ETH**

* \[ ] `L2NativeTokenVault.L1_CHAIN_ID()` — read
* \[ ] `L2NativeTokenVault.BASE_TOKEN_ASSET_ID()` — read
* \[ ] `L2NativeTokenVault.WETH_TOKEN()` — read
* \[ ] `L2NativeTokenVault.originChainId(bytes32)` — read
* \[ ] Transaction Filterer allows the sender

**Deposit — ERC-20** (all of the above, plus)

* \[ ] `L2NativeTokenVault.l2TokenAddress(address)` — read

**Withdraw — ETH** (the four common reads, plus)

* \[ ] `InteropCenter.sendBundle(bytes,(bytes,bytes,bytes[])[],bytes[])` — write (protocol v33)
* \[ ] `L2BaseToken.withdraw(address)` — write (protocol v31)
* \[ ] `VITE_WITHDRAWAL_ENABLED=true`

**Withdraw — ERC-20** (the four common reads, plus)

* \[ ] `L2NativeTokenVault.assetId(address)` — read (protocol v33)
* \[ ] `L2AssetRouter.l1TokenAddress(address)` — read (protocol v31)
* \[ ] `L2NativeTokenVault.ensureTokenIsRegistered(address)` — write
* \[ ] `<token>.approve(address,uint256)` — write
* \[ ] `<token>.allowance(address,address)` — read
* \[ ] `InteropCenter.sendBundle(bytes,(bytes,bytes,bytes[])[],bytes[])` — write (protocol v33)
* \[ ] `L2AssetRouter.withdraw(bytes32,bytes)` — write (protocol v31)
* \[ ] `VITE_WITHDRAWAL_ENABLED=true`

## Troubleshooting

| Symptom                                                     | Likely cause                                                                                                          |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Deposit fails immediately, no L1 transaction is sent        | A token-resolution **read** permission on `0x…010004` is Forbidden (e.g. `L1_CHAIN_ID`). Grant the four common reads. |
| Deposit rejected at L1 / no L2 priority transaction appears | Transaction Filterer `depositsAllowed=false`, or the sender is not whitelisted (or is an aliased L1 contract).        |
| Withdrawal "not authorized"                                 | A **write** permission is missing (`withdraw`, `ensureTokenIsRegistered`, or the token's `approve`).                  |
