---
title: "Exit Signet"
url: "/docs/build-on-signet/transfers/exit-signet/index.md"
description: "Move assets from Signet back to Ethereum"
--- Create and submit orders to move assets from Signet to Ethereum. For background on the order lifecycle and atomic settlement, see [How orders work](/docs/learn-about-signet/how-orders-work/index.md). ## Order data model Every order has inputs (what the user gives up on Signet) and outputs (what they receive on the destination chain). **Input:** | Field | Description | |-------|-------------| | `token` | ERC-20 address on Signet. Zero address for native USD. | | `amount` | Amount in the token's smallest unit (e.g., wei). | **Output:** | Field | Description | |-------|-------------| | `token` | ERC-20 address on the destination chain. | | `amount` | Amount in the token's smallest unit. | | `recipient` | Delivery address on the destination chain. | | `chainId` | Destination chain ID. | An order can have multiple inputs and multiple outputs across different chains and recipients. The data model is [ERC-7683](https://eips.ethereum.org/EIPS/eip-7683) compliant. ## On-chain vs gasless **On-chain** orders call `RollupOrders.initiate` directly. The user sends a transaction and pays gas. Fillers discover it via `Order` events. **Gasless** orders use Permit2. The user signs an EIP-712 message (no transaction, no gas) and submits it to the [transaction cache](/docs/learn-about-signet/how-orders-work/index.md#the-transaction-cache), where fillers pick it up. Requires a one-time Permit2 token approval. ## Pricing The spread between input and output is the filler's incentive. A user offering 1 WETH on Signet for 0.995 WETH on Ethereum gives the filler 50 basis points (0.5%). If your order isn't getting filled, widen the spread. If it fills instantly every time, you may be offering more than necessary. > **No fill guarantee:** Orders guarantee that if the output isn't delivered, nothing happens on Signet. But there's no guarantee a filler will choose to fill your order. Set competitive spreads to attract fillers. ## FAQ **What happens if nobody fills?** The order expires at the deadline. Tokens stay on Signet. Nothing is lost. **Can an order partially fill?** No. The entire output must be delivered, or nothing happens. **Can my user cancel?** On-chain orders created via `initiate` cannot be cancelled -- the deadline is the only expiry mechanism. Gasless Permit2 orders can be cancelled by consuming the Permit2 nonce. **How long should the deadline be?** Shorter deadlines reduce the time funds are committed but give fillers less time to act. Start with 1 minute for testing. 

## TypeScript

This page assumes you've [set up your clients](/docs/build-on-signet/getting-started/index.md) .
## Contracts Contract Chain Address Permit2 Signet `0x000000000022D473030F116dDEE9F6B43aC78BA3` RollupOrders Signet `0x000000000000007369676e65742d6f7264657273` All contract addresses on this page are for [Parmigiana testnet](/docs/build-on-signet/parmigiana/index.md) .
## Order types The SDK's type definitions for order inputs and outputs:
```typescript type Input = { token: `0x${string}`; amount: bigint; }; type Output = { token: `0x${string}`; amount: bigint; recipient: `0x${string}`; chainId: number; }; ``` ## On-chain exit (costs gas) Call initiate on RollupOrders to create an exit order directly. The inputs array is what the user gives up on Signet; the outputs array is what they want on Ethereum.
Here's an exit that converts 1 USD on Signet to WETH on Ethereum:
```typescript import { rollupOrdersAbi } from '@signet-sh/sdk/abi'; import { PARMIGIANA } from '@signet-sh/sdk/constants'; import { getTokenAddress } from '@signet-sh/sdk/tokens'; import { parseEther } from 'viem'; const hostWeth = getTokenAddress('WETH', PARMIGIANA.hostChainId, PARMIGIANA); const inputAmount = parseEther('1'); const outputAmount = (inputAmount * 995n) / 1000n; // 50bps to fillers const hash = await signetWalletClient.writeContract({ address: PARMIGIANA.rollupOrders, abi: rollupOrdersAbi, functionName: 'initiate', args: [ BigInt(Math.floor(Date.now() / 1000) + 60), // 1 minute deadline [{ token: '0x0000000000000000000000000000000000000000', amount: inputAmount }], [{ token: hostWeth, amount: outputAmount, recipient: userAddress, chainId: Number(PARMIGIANA.hostChainId) }], ], value: inputAmount, }); ``` **Without the SDK** ```typescript import { parseEther } from 'viem'; const initiateAbi = [ { name: 'initiate', type: 'function', stateMutability: 'payable', inputs: [ { name: 'deadline', type: 'uint256' }, { name: 'inputs', type: 'tuple[]', components: [ { name: 'token', type: 'address' }, { name: 'amount', type: 'uint256' }, ], }, { name: 'outputs', type: 'tuple[]', components: [ { name: 'token', type: 'address' }, { name: 'amount', type: 'uint256' }, { name: 'recipient', type: 'address' }, { name: 'chainId', type: 'uint32' }, ], }, ], outputs: [], }, ] as const; const inputAmount = parseEther('1'); const outputAmount = (inputAmount * 995n) / 1000n; const hash = await signetWalletClient.writeContract({ address: '`0x000000000000007369676e65742d6f7264657273` ', abi: initiateAbi, functionName: 'initiate', args: [ BigInt(Math.floor(Date.now() / 1000) + 60), [{ token: '0x0000000000000000000000000000000000000000' as `0x${string}`, amount: inputAmount }], [{ token: '`0xD1278f17e86071f1E658B656084c65b7FD3c90eF` ' as `0x${string}`, amount: outputAmount, recipient: userAddress, chainId: 3151908 }], ], value: inputAmount, }); ``` On-chain orders created via initiate cannot be cancelled. The deadline is the only expiry mechanism. For pricing guidance, see the [pricing section](/docs/build-on-signet/transfers/exit-signet/index.md#pricing) in the shared intro above.
## Gasless exit (Permit2) The gasless flow uses [Permit2](https://docs.uniswap.org/contracts/permit2/overview) for token approvals. The user signs a message (not a transaction) and pays zero gas.
### Step 1: Approve Permit2 One-time per token. ensurePermit2Approval checks the current allowance and only sends a transaction if needed:
```typescript import { ensurePermit2Approval } from '@signet-sh/sdk/permit2'; const { approved, txHash } = await ensurePermit2Approval(signetWalletClient, signetPublicClient, { token: weth, owner: userAddress, amount: parseEther('1'), }); ``` It handles USDT-style tokens that require resetting allowance to zero before setting a new value.
**Without the SDK** ```typescript import { erc20Abi, maxUint256 } from 'viem'; await signetWalletClient.writeContract({ address: inputTokenAddress, abi: erc20Abi, functionName: 'approve', args: ['`0x000000000022D473030F116dDEE9F6B43aC78BA3` ', maxUint256], }); ``` ### Step 2: Build and sign the order UnsignedOrder is a builder. Chain the inputs, outputs, deadline, nonce, and chain config, then call .sign():
```typescript import { UnsignedOrder, randomNonce } from '@signet-sh/sdk/signing'; import { PARMIGIANA } from '@signet-sh/sdk/constants'; const signed = await UnsignedOrder.new() .withInput(weth, parseEther('1')) .withOutput(usdc, 2985_000000n, userAddress, Number(PARMIGIANA.hostChainId)) .withDeadline(BigInt(Math.floor(Date.now() / 1000) + 60)) .withNonce(randomNonce()) .withChain({ chainId: PARMIGIANA.rollupChainId, orderContract: PARMIGIANA.rollupOrders }) .sign(signetWalletClient); ``` .sign() constructs EIP-712 typed data (Permit2's PermitBatchWitnessTransferFrom with the order outputs as the witness) and calls signetWalletClient.signTypedData.
> **Manual Permit2 signing:** Permit2 EIP-712 signing requires constructing a witness type hash with the full order struct. For manual implementation, refer to the [Uniswap Permit2 documentation](https://docs.uniswap.org/contracts/permit2/overview) and the [SDK source](https://github.com/signet-sh/sdk). ### Step 3: Check feasibility Before submitting, verify that the signer has sufficient balance and Permit2 allowance. checkOrderFeasibility reads on-chain state and returns any issues:
```typescript import { checkOrderFeasibility } from '@signet-sh/sdk/signing'; const result = await checkOrderFeasibility(signetPublicClient, signed); if (!result.feasible) { // result.issues contains the specific problems: // - 'insufficient_balance' (token, required, available) // - 'insufficient_allowance' (token, required, available) // - 'nonce_used' (Permit2 nonce already consumed) console.error(result.issues); return; } ``` This catches balance, allowance, and nonce failures before submission. Surface them inline rather than submitting an order that will never fill.
### Step 4: Submit to the tx-cache The [transaction cache](/docs/learn-about-signet/how-orders-work/index.md#the-transaction-cache) is Signet's submission service. Your application posts a signed order to its /orders endpoint; fillers poll the same service to discover and evaluate new orders.
```typescript import { createTxCacheClient } from '@signet-sh/sdk/client'; import { PARMIGIANA } from '@signet-sh/sdk/constants'; const txCache = createTxCacheClient(PARMIGIANA.txCacheUrl); await txCache.submitOrder(signed); ``` submitOrder serializes the signed order and POSTs it to {txCacheUrl}/orders. The tx-cache validates the signature and Permit2 structure before making the order visible to fillers.
## Reading order status Query Order events from RollupOrders using the SDK's ABI:
```typescript import { rollupOrdersAbi } from '@signet-sh/sdk/abi'; import { PARMIGIANA } from '@signet-sh/sdk/constants'; const logs = await signetPublicClient.getLogs({ address: PARMIGIANA.rollupOrders, abi: rollupOrdersAbi, eventName: 'Order', fromBlock: startBlock, toBlock: 'latest', }); ``` Check fills on the Ethereum side:
```typescript import { hostOrdersAbi } from '@signet-sh/sdk/abi'; import { PARMIGIANA } from '@signet-sh/sdk/constants'; const fills = await hostPublicClient.getLogs({ address: PARMIGIANA.hostOrders, abi: hostOrdersAbi, eventName: 'Filled', fromBlock: startBlock, toBlock: 'latest', }); ``` **Without the SDK** These are simplified event definitions. For production use, import the full ABI from `@signet-sh/sdk/abi` or download from the [ABI files](https://signet.sh/abis/). ```typescript const logs = await signetPublicClient.getLogs({ address: '`0x000000000000007369676e65742d6f7264657273` ', event: { type: 'event', name: 'Order', inputs: [ { name: 'deadline', type: 'uint256', indexed: false }, { name: 'inputs', type: 'tuple[]', indexed: false }, { name: 'outputs', type: 'tuple[]', indexed: false }, ], }, fromBlock: startBlock, toBlock: 'latest', }); const fills = await hostPublicClient.getLogs({ address: '`0x96f44ddc3Bc8892371305531F1a6d8ca2331fE6C` ', event: { type: 'event', name: 'Filled', inputs: [ { name: 'outputs', type: 'tuple[]', indexed: false }, ], }, fromBlock: startBlock, toBlock: 'latest', }); ``` ### Detecting unfilled orders Orders expire at their deadline (see the [FAQ](/docs/build-on-signet/transfers/exit-signet/index.md#faq) above). To programmatically detect whether a gasless order was filled, check whether its Permit2 nonce has been consumed:
```typescript import { PARMIGIANA, PERMIT2_ADDRESS } from '@signet-sh/sdk/constants'; const orderDeadline = 1710000000n; // from the Order event const orderNonce = 42n; // the nonce used when creating the order const block = await signetPublicClient.getBlock(); // Check if the Permit2 nonce has been consumed const wordPos = orderNonce / 256n; const bitPos = orderNonce % 256n; const bitmap = await signetPublicClient.readContract({ address: PERMIT2_ADDRESS, abi: [{ name: 'nonceBitmap', type: 'function', stateMutability: 'view', inputs: [{ name: 'owner', type: 'address' }, { name: 'wordPos', type: 'uint256' }], outputs: [{ name: '', type: 'uint256' }] }], functionName: 'nonceBitmap', args: [userAddress, wordPos], }); const nonceConsumed = (bitmap & (1n << bitPos)) !== 0n; if (block.timestamp > orderDeadline && !nonceConsumed) { // Order expired unfilled, prompt user to retry with a wider spread } else if (nonceConsumed) { // Order was filled (possibly batched with other orders) } ``` For error handling and common issues, see [Troubleshooting](/docs/build-on-signet/advanced/troubleshooting/index.md) .
## Cross-chain swap in 15 lines Swap 1 WETH on Signet for ~2,985 USDC on Ethereum, gasless. In production, add the [feasibility check](#step-3-check-feasibility) before submission.
```typescript import { UnsignedOrder, randomNonce } from '@signet-sh/sdk/signing'; import { PARMIGIANA } from '@signet-sh/sdk/constants'; import { getTokenAddress } from '@signet-sh/sdk/tokens'; import { ensurePermit2Approval } from '@signet-sh/sdk/permit2'; import { createTxCacheClient } from '@signet-sh/sdk/client'; import { parseEther } from 'viem'; const weth = getTokenAddress('WETH', PARMIGIANA.rollupChainId, PARMIGIANA); const usdc = getTokenAddress('USDC', PARMIGIANA.hostChainId, PARMIGIANA); // 1. One-time: let Permit2 spend your WETH await ensurePermit2Approval(signetWalletClient, signetPublicClient, { token: weth, owner: userAddress, amount: parseEther('1'), }); // 2. Build and sign the order const signed = await UnsignedOrder.new() .withInput(weth, parseEther('1')) .withOutput(usdc, 2985_000000n, userAddress, Number(PARMIGIANA.hostChainId)) .withDeadline(BigInt(Math.floor(Date.now() / 1000) + 60)) .withNonce(randomNonce()) .withChain({ chainId: PARMIGIANA.rollupChainId, orderContract: PARMIGIANA.rollupOrders }) .sign(signetWalletClient); // 3. Submit to the tx-cache, fillers pick it up from here const txCache = createTxCacheClient(PARMIGIANA.txCacheUrl); await txCache.submitOrder(signed); ``` ## Next steps For Ethereum-to-Ethereum swaps using Signet invisibly, see [Enter Signet](/docs/build-on-signet/transfers/enter-signet/index.md) for the Passage deposit flow.
For advanced use cases, exits can also be constructed as part of a [Bundle](/docs/build-on-signet/advanced/bundles/index.md) , giving you full control over transaction ordering and atomic multi-step operations.
For Permit2 data structures, see the [Uniswap Permit2 docs](https://docs.uniswap.org/contracts/permit2/overview) .

## Rust

Create and submit off-chain Orders using Rust and the Signet SDK.
This page assumes you've completed the [getting started](/docs/build-on-signet/getting-started/index.md) setup. Ensure your account has approved [Permit2](https://docs.uniswap.org/contracts/permit2/overview) to spend your input tokens.
## Creating an Order The [signet-types](https://docs.rs/signet-types/latest/signet_types/) crate provides a simple order builder via the [UnsignedOrder](https://docs.rs/signet-types/latest/signet_types/signing/order/struct.UnsignedOrder.html) struct, which can be used to build orders. Create a simple order that swaps 1 WETH on Signet for 1 WETH on Ethereum:
```rust use signet_types::signing::order::{UnsignedOrder}; use signet_constants::parmigiana as constants; let order = UnsignedOrder::default() .with_input( constants::RU_WETH, U256::from(1_000_000_000_000_000_000u128), // 1 WETH ).with_output( constants::HOST_WETH, U256::from(1_000_000_000_000_000_000u128), your_address, 3151908, // Parmigiana host chain ); ``` The UnsignedOrder struct also provides methods to sign orders, using any alloy signer. The signer requires that you provide the constants object, so that the permit2 signer can correctly derive the domain separator.
```rust use signet_types::signing::order::{UnsignedOrder}; use signet_constants::parmigiana as constants; let signed = UnsignedOrder::default() .with_input(token_address, amount) .with_output(token_address, amount, recipient, chain_id) .with_chain(constants::system_constants()) .with_nonce(permit2_nonce) .sign(&signer).await?; ``` ## Submitting an Order Once signed, the order can be submitted to the Signet network via the Signet tx cache. The tx cache makes the order available to fillers, who will include it in execution bundles.
```rust use signet_tx_cache::TxCache; let tx_cache = TxCache::parmigiana(); tx_cache.forward_order(signed_order).await?; ``` ## Using OrderSender For long-running services or bots, the [signet-orders](https://docs.rs/signet-orders/latest/signet_orders/) crate provides [OrderSender](https://docs.rs/signet-orders/latest/signet_orders/struct.OrderSender.html) , a reusable struct that wraps signing and submission into a single interface.
```bash cargo add signet-orders ``` OrderSender is generic over any alloy [Signer](https://docs.rs/alloy/latest/alloy/signers/trait.Signer.html) and any [OrderSubmitter](https://docs.rs/signet-orders/latest/signet_orders/trait.OrderSubmitter.html) backend. A ready-made OrderSubmitter implementation is provided for TxCache.
```rust use signet_constants::parmigiana; use signet_orders::OrderSender; use signet_tx_cache::TxCache; // Any alloy Signer works: LocalSigner, AwsSigner, LedgerSigner, etc. let signer = /* your signer */; let order_sender = OrderSender::new( signer, TxCache::parmigiana(), parmigiana::system_constants(), ); // Sign and submit in one call let signed = order_sender.sign_and_send_order(order).await?; ``` You can also sign and send separately for more control:
```rust let signed = order_sender .sign_unsigned_order( UnsignedOrder::default() .with_input(token_address, amount) .with_output(token_address, amount, recipient, chain_id), ) .await?; // Submit when ready order_sender.send_order(signed).await?; ``` If you need to submit orders somewhere other than the tx cache, implement the OrderSubmitter trait:
```rust use signet_orders::OrderSubmitter; use signet_types::SignedOrder; struct MySubmitter; impl OrderSubmitter for MySubmitter { type Error = MyError; async fn submit_order(&self, order: SignedOrder) -> Result<(), Self::Error> { // Forward the order to your backend todo!() } } ``` 

## Solidity

Example contracts that create and fill Signet Orders. View the full set at [signet-sol](https://github.com/init4tech/signet-sol) .
This page assumes you've completed the [getting started](/docs/build-on-signet/getting-started/index.md) setup.
## Example contracts **SignetL2.sol - System configuration** Base contract that auto-resolves Signet system addresses by chain ID. Inherit `SignetL2` for automatic access to `rollupOrders`, `permit2`, and other system addresses. ```solidity import {SignetL2} from "./Signet.sol"; contract YourContract is SignetL2 { function createOrder() external { // rollupOrders, permit2, etc. are available } } ``` Source: [Signet.sol](https://github.com/init4tech/signet-sol/blob/main/src/l2/Signet.sol) **Flash.sol - Flash loans** Borrow an asset for the duration of a single function call using an Order where the output repays the input. ```solidity flashBorrow(wethAddress, amount, arbitrageCalldata); ``` The `flash` modifier emits an Order with both an output (asset received before execution) and an input (asset returned after). The filler provides the liquidity on either side. Source: [Flash.sol](https://github.com/init4tech/signet-sol/blob/main/src/l2/examples/Flash.sol) **GetOut.sol - Quick exit** Exit Signet by offering fillers a 50 basis point fee. ```solidity getOut(tokenAddress, amount, recipient); ``` Locks tokens on Signet and creates an Order offering 99.5% on Ethereum. Fillers fill for the 0.5% spread. Source: [GetOut.sol](https://github.com/init4tech/signet-sol/blob/main/src/l2/examples/GetOut.sol) **PayMe.sol - Payment gating** Gate execution behind a payment Order with no inputs. Any third party can fill. ```solidity function executeWithPayment() external { payMe(usdcAddress, 10e6); // Require 10 USDC payment // Your logic here -- only executes if payment Order is filled } ``` The contract creates an Order with only outputs (payment required). Unlike `msg.value` checks, the payer can be anyone. Source: [PayMe.sol](https://github.com/init4tech/signet-sol/blob/main/src/l2/examples/PayMe.sol) **PayYou.sol - Execution bounties** Offer MEV by emitting an Order with inputs and no outputs, creating a bounty for calling your contract. ```solidity function executeAndPay() external { performArbitrage(); payYou(wethAddress, 0.01 ether); // 0.01 ETH bounty } ``` Searchers compete to call the function and capture the bounty. Useful for liquidations, automated rebalancing, and scheduled maintenance. Source: [PayYou.sol](https://github.com/init4tech/signet-sol/blob/main/src/l2/examples/PayYou.sol) ## Composing patterns These patterns compose: a single transaction can combine an automated exit with an execution bounty, or a flash loan with a payment gate. All of these are [conditional transactions](/docs/learn-about-signet/cross-chain-transfers/index.md#moving-from-signet-to-ethereum) , and any can be triggered from Ethereum via the [Transactor](/docs/build-on-signet/advanced/execute-from-ethereum/index.md) .
> **Building your own contract:** Use these patterns as building blocks: 1. **Inherit SignetL2** for automatic configuration 2. **Combine patterns** (e.g., PayMe + PayYou for payment flows) 3. **Test thoroughly** with `forge test` 4. **Deploy** to Signet testnet first 

## Terminal

This page assumes you've completed the [getting started](/docs/build-on-signet/getting-started/index.md) setup.
The Terminal variant covers the on-chain exit flow only (initiate). The gasless Permit2 flow requires EIP-712 typed data signing, which cast doesn't support. For gasless orders, use the [TypeScript](/docs/build-on-signet/transfers/exit-signet/index.md) or [Rust](/docs/build-on-signet/transfers/exit-signet/index.md) SDK.
## Create an exit order Call initiate on RollupOrders to create an exit order on Signet. This example exits 1 USD (native asset, sent as value) for ~0.995 WETH on Ethereum:
```bash cast send `0x000000000000007369676e65742d6f7264657273` \ "initiate(uint256,(address,uint256)[],(address,uint256,address,uint32)[])" \ $(( $(date +%s) + 60 )) \ "[(0x0000000000000000000000000000000000000000,1000000000000000000)]" \ "[(`0xD1278f17e86071f1E658B656084c65b7FD3c90eF` ,995000000000000000,$YOUR_ADDRESS,3151908)]" \ --value 1ether \ --rpc-url $SIGNET_RPC \ --private-key $PRIVATE_KEY ``` The arguments:
deadline: Unix timestamp (here, 60 seconds from now) inputs: array of (token, amount) tuples. address(0) means native USD, sent via --value outputs: array of (token, amount, recipient, chainId) tuples. 3151908 is Parmigiana's host chain ID The spread between input (1.0) and output (0.995) is the filler's incentive.
All contract addresses on this page are for [Parmigiana testnet](/docs/build-on-signet/parmigiana/index.md) .
## Read order events Query Order events from RollupOrders:
```bash cast logs \ --from-block $START_BLOCK \ --rpc-url $SIGNET_RPC \ --address `0x000000000000007369676e65742d6f7264657273` \ "Order(uint256,(address,uint256)[],(address,uint256,address,uint32)[])" ``` ## Check if an order was filled Poll for Filled events on HostOrders on Ethereum:
```bash cast logs \ --from-block $ORDER_BLOCK \ --rpc-url $HOST_RPC \ --address `0x96f44ddc3Bc8892371305531F1a6d8ca2331fE6C` \ "Filled((address,uint256,address,uint32)[])" ``` Or check the recipient's balance on Ethereum directly, since fills settle in the same block as the order:
```bash cast balance $YOUR_ADDRESS --rpc-url $HOST_RPC ``` ## What if nobody fills? The order expires at the deadline. Tokens stay on Signet, nothing is lost. Create a new order with a wider spread or longer deadline.

