---
title: "Passage and Transactor"
url: "/updates/passage-and-transactor/index.md"
date: "2025-11-12"
---
Bridges are complicated. Lockups, relayers, waiting periods, wrapped assets. Users learn to dread the "bridging" step of any cross-chain interaction.

Deposits shouldn't be this hard.

Signet has two entry contracts: **Passage** for asset transfers, **Transactor** for arbitrary execution. Together they cover every L1→L2 pattern you'll need—and they all settle in the same block.

---

## Passage: Asset Entry

Passage handles moving assets from Ethereum into Signet. The contract emits events that Signet observes and acts on in the same block.

### enter()

The simplest pattern. Send ETH, receive USD on Signet.

```solidity
// On Ethereum (L1)
IPassage(PASSAGE).enter{value: 1 ether}(rollupRecipient);
```

What happens:
1. Your ETH is held by Passage on L1
2. Passage emits `Enter(from, to, value)`
3. Signet observes the event and mints USD to `rollupRecipient`

No waiting. No claim step. The recipient can spend on Signet immediately.

You can also just send ETH directly:

```solidity
// Fallback/receive — uses msg.sender as recipient
payable(PASSAGE).transfer(1 ether);
```

### enterToken()

Same pattern, but for ERC-20s.

```solidity
// Approve first
IERC20(token).approve(PASSAGE, amount);

// Then enter
IPassage(PASSAGE).enterToken(rollupRecipient, token, amount);
```

Emits `EnterToken(from, to, token, amount)`. The token must be on Signet's allowlist—currently WETH, WBTC, and USDC. Signet credits the corresponding native token.

---

## Transactor: Arbitrary Execution

Transactor lets L1 contracts trigger execution on Signet without transferring assets. Your L1 transaction causes an L2 transaction to execute.

```solidity
// On Ethereum (L1)
ITransactor(TRANSACTOR).transact{value: ethForGas}(
    to,           // Target contract on Signet
    data,         // Encoded function call
    value,        // USD to attach (in wei)
    gas,          // Gas limit for Signet execution
    maxFeePerGas  // Max fee per gas in USD
);
```

Signet observes the `Transact` event and executes the call at the **end of the Signet block**. The `msg.sender` on Signet is derived from the L1 caller.

### Address Aliasing

When a smart contract calls Transactor, the Signet-side `msg.sender` is aliased:

```
signet_address = (l1_address + 0x1111000000000000000000000000000000001111) % 2^160
```

EOAs are **not** aliased—their address is unchanged. This prevents L1 contracts from impersonating L2 EOAs.

### Cross-Chain Contract Control

An L1 contract can own and operate an L2 contract:

```solidity
// L1 Controller
contract L1Controller {
    address constant TRANSACTOR = 0x0B4fc18e78c585687E01c172a1087Ea687943db9;
    address public l2Contract;

    function updateL2Config(uint256 newValue) external payable onlyOwner {
        ITransactor(TRANSACTOR).transact{value: msg.value}(
            l2Contract,
            abi.encodeCall(IL2Contract.setConfig, (newValue)),
            0,        // no USD value
            100000,   // gas limit
            1 gwei    // max fee per gas
        );
    }
}
```

The L2 contract sees `msg.sender` as the aliased L1 controller address. Same-block execution means L2 state updates before the L1 transaction completes.

### Cross-Chain Oracles

Push data from L1 to L2 without oracle infrastructure:

```solidity
contract PricePusher {
    function pushPrice(address feed, address l2Oracle) external payable {
        int256 price = IChainlinkFeed(feed).latestAnswer();
        ITransactor(TRANSACTOR).transact{value: msg.value}(
            l2Oracle,
            abi.encodeCall(IL2Oracle.updatePrice, (feed, price)),
            0, 100000, 1 gwei
        );
    }
}
```

The L2 oracle receives L1 Chainlink prices in the same block they're published. No latency. No third-party relayers.

---

## Execution Ordering

Transactor calls execute at the **end** of the Signet block. This matters for composability:

1. Regular Signet transactions execute first
2. Passage deposits are credited
3. Transactor calls execute last

Your Transactor call can depend on state set earlier in the block, but earlier transactions can't depend on Transactor results.

---

## When to Use What

| Pattern | Use Case | Assets Move? |
|---------|----------|--------------|
| `enter()` | Deposit ETH → USD | Yes |
| `enterToken()` | Deposit ERC-20 → Signet token | Yes |
| `transact()` | L1 controls L2, oracles, governance | No (gas only) |

---

## The Reverse Direction

Exiting Signet (L2→L1) uses the Orders system instead of a mirror Passage contract. Users post orders expressing "I want X on L1 for Y on L2," and fillers compete to satisfy them.

This asymmetry is intentional:
- **Entry**: Direct, trustless, immediate (Passage/Transactor)
- **Exit**: Market-based, competitive, also immediate (Orders + Fillers)

See [The Filler Economy](/updates/the-filler-economy/index.md) for exit patterns.

---

## Get Started

**Contracts (Parmigiana Testnet):**
- Passage: `0x28524D2a753925Ef000C3f0F811cDf452C6256aF`
- Transactor: `0x0B4fc18e78c585687E01c172a1087Ea687943db9`

**Bindings:**
```bash
# Solidity
forge install https://github.com/init4tech/zenith

# Rust
cargo add signet-zenith
```

**Docs:**
- [Enter Signet](/docs/build-on-signet/transfers/enter-signet/index.md) — Full reference
- [Execute from Ethereum](/docs/build-on-signet/advanced/execute-from-ethereum/index.md) — Address aliasing, gas costs

Questions? [Get in touch](https://t.me/anthspezzano).
