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.
// On Ethereum (L1)
IPassage(PASSAGE).enter{value: 1 ether}(rollupRecipient);What happens:
- Your ETH is held by Passage on L1
- Passage emits
Enter(from, to, value) - 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:
// Fallback/receive — uses msg.sender as recipient
payable(PASSAGE).transfer(1 ether);enterToken()
Same pattern, but for ERC-20s.
// 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.
// 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^160EOAs 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:
// 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:
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:
- Regular Signet transactions execute first
- Passage deposits are credited
- 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 for exit patterns.
Get Started
Contracts (Parmigiana Testnet):
- Passage:
0x28524D2a753925Ef000C3f0F811cDf452C6256aF - Transactor:
0x0B4fc18e78c585687E01c172a1087Ea687943db9
Bindings:
# Solidity
forge install https://github.com/init4tech/zenith
# Rust
cargo add signet-zenithDocs:
- Ethereum to Signet — Full reference
- Transactor details — Address aliasing, gas costs
Questions? Get in touch.