# Goliath → Ethereum

Bridging **from** Goliath Mainnet (`327`) **to** Ethereum Mainnet (`1`) splits into two flows depending on the asset:

1. **ERC-20 withdraw (ETH, USDC)** — one transaction on Goliath, calling `BridgeMint.burn(...)`.
2. **Native-XCN withdraw** — a signed EIP-712 intent plus a native XCN transfer. Required because XCN is the gas token on Goliath and cannot be "burned" as an ERC-20.

Both flows take **\~1 hour total** because of the one-hour security hold before the relayer releases funds on Ethereum, and both charge a withdrawal fee.

## Fee Summary

| Token    | Fee rate | Minimum fee | Minimum bridge amount |
| -------- | -------- | ----------- | --------------------- |
| **ETH**  | 0.25%    | 0.003 ETH   | 0.01 ETH              |
| **USDC** | 0.25%    | 5 USDC      | 10 USDC               |
| **XCN**  | 0.25%    | 1,000 XCN   | 5,000 XCN             |

`fee = max(amount * 25 / 10000, minFee[token])`. Transfers below the minimum bridge amount are rejected by the API and should be blocked client-side.

**Always call** [**`GET /bridge/fee-quote`**](/developer-guide/bridge/api-reference.md#get-bridge-fee-quote) to render the user-facing breakdown — the canonical numbers come from the API.

***

## ERC-20 Withdraw (ETH, USDC)

For assets that exist as **ERC-20 tokens on Goliath** (bridged ETH, bridged USDC), you call `BridgeMint.burn(...)`.

### Prerequisites

* Wallet on **Goliath Mainnet** (Chain ID `327`).
* Enough native **XCN** for gas.
* Enough of the token you're bridging.
* One-time ERC-20 allowance granted to `BridgeMint`.

### Goliath Bridged Token Addresses

| Token    | Goliath address                              | Decimals |
| -------- | -------------------------------------------- | -------- |
| **ETH**  | `0x9253587505c3B7E7b9DEE118AE1AcB53eEC0E4b6` | 18       |
| **USDC** | `0xC8410270bb53f6c99A2EFe6eD3686a8630Efe22B` | 6        |

BridgeMint contract on Goliath: `0x1d14ae13ca030eb5e9e2857e911af515cf5ffff2`

### The `burn` Function

```solidity
function burn(
    address token,                 // bridged ERC-20 address on Goliath
    uint256 amount,                // atomic units
    address destinationAddress,    // recipient on Ethereum
    uint64  destinationChainId     // 1 for Ethereum Mainnet
) external returns (bytes32 withdrawId);
```

Emits:

```solidity
event Withdraw(
    bytes32 indexed withdrawId,
    address indexed token,
    address indexed sender,
    address destinationAddress,
    uint256 amount,
    uint64  timestamp,
    uint64  sourceChainId,
    uint64  destinationChainId
);
```

Full ABI in [Contracts & Events](/developer-guide/bridge/contracts-and-events.md#bridgemint-abi-user-functions).

### Step 1 — Quote the fee

```
GET https://bridge.goliath.net/api/v1/bridge/fee-quote?token=USDC&amount=100&direction=GOLIATH_TO_ETHEREUM
```

```json
{
  "inputAmount": "100000000",
  "inputFormatted": "100.0",
  "feeAmount": "5000000",
  "feeFormatted": "5.0",
  "feeBps": 25,
  "outputAmount": "95000000",
  "outputFormatted": "95.0",
  "token": "USDC"
}
```

If the amount is below the minimum, the endpoint returns `400 BELOW_MINIMUM` — surface that error to the user before they sign.

### Step 2 — Approve `BridgeMint`

```ts
import { erc20Abi, maxUint256, parseUnits } from "viem";

const BRIDGE_MINT = "0x1d14ae13ca030eb5e9e2857e911af515cf5ffff2";
const USDC_GOLIATH = "0xC8410270bb53f6c99A2EFe6eD3686a8630Efe22B";

const approveTx = await walletClient.writeContract({
  address: USDC_GOLIATH,
  abi: erc20Abi,
  functionName: "approve",
  args: [BRIDGE_MINT, maxUint256],
  chain: goliathMainnet,
});
await publicClient.waitForTransactionReceipt({ hash: approveTx });
```

### Step 3 — Call `burn`

```ts
import { bridgeMintAbi } from "./abi/bridgeMint";

const amount = parseUnits("100", 6);           // 100 USDC
const ETHEREUM_CHAIN_ID = 1n;

const burnTx = await walletClient.writeContract({
  address: BRIDGE_MINT,
  abi: bridgeMintAbi,
  functionName: "burn",
  args: [USDC_GOLIATH, amount, recipientOnEthereum, ETHEREUM_CHAIN_ID],
  chain: goliathMainnet,
});
```

### Step 4 — Poll status

```ts
const url = `https://bridge.goliath.net/api/v1/bridge/status?originTxHash=${burnTx}`;
```

Expect the status to progress `CONFIRMING` → `AWAITING_RELAY` (during the 1 h hold) → `PROCESSING_DESTINATION` → `COMPLETED`.

The response includes:

* `holdUntil` — ISO timestamp when the relayer becomes eligible to release. Render as a countdown.
* `fee` — the fee deducted (atomic + formatted).
* `outputAmount` / `outputAmountFormatted` — what the recipient receives after fee.
* `destinationTxHash` — the Ethereum tx that delivered the funds.

### Complete Example (viem)

```ts
async function withdrawUsdcToEthereum(amountHuman: string, recipient: `0x${string}`) {
  const amount = parseUnits(amountHuman, 6);

  // 1. Quote & validate minimums
  const quote = await fetch(
    `https://bridge.goliath.net/api/v1/bridge/fee-quote` +
    `?token=USDC&amount=${amountHuman}&direction=GOLIATH_TO_ETHEREUM`
  ).then((r) => r.json());
  if (quote.error) throw new Error(quote.message);

  // 2. Approve
  const approveHash = await walletClient.writeContract({
    address: USDC_GOLIATH,
    abi: erc20Abi,
    functionName: "approve",
    args: [BRIDGE_MINT, amount],
    chain: goliath,
  });
  await publicClient.waitForTransactionReceipt({ hash: approveHash });

  // 3. Burn
  const burnHash = await walletClient.writeContract({
    address: BRIDGE_MINT,
    abi: bridgeMintAbi,
    functionName: "burn",
    args: [USDC_GOLIATH, amount, recipient, 1n],
    chain: goliath,
  });

  // 4. Poll until COMPLETED (or user cancels)
  return pollStatus(burnHash);
}
```

***

## Native-XCN Withdraw

XCN is the native gas token on Goliath, so there is no ERC-20 to `burn`. The protocol uses a **signed-intent flow** instead:

1. Sign an EIP-712 withdrawal intent with your Goliath wallet.
2. Register the intent via `POST /bridge/xcn-withdraw-intent`. The API returns a `relayerWalletAddress`.
3. Send a plain native XCN transfer from your wallet to `relayerWalletAddress` with `value = amount`.
4. Pair the resulting tx hash to the intent via `POST /bridge/xcn-withdraw-intent/bind-origin`.
5. After the hold, the relayer releases XCN (as ERC-20) on Ethereum to `recipientAddress`.

All five steps happen inside a single user flow. If the wallet disconnects between steps 3 and 4, the client must retry step 4 (the binding) when it reconnects — the bridge keeps the intent alive for 30 minutes.

{% hint style="info" %}
**Using an ERC-4337 / smart-account wallet** (Safe, Coinbase Smart Wallet, Biconomy, Alchemy AA, ZeroDev, thirdweb, …)? The endpoints, typed-data shape, and status flow below are identical, but `senderAddress` is the **smart-account contract address**, the signature is verified via **ERC-1271** (`isValidSignature(bytes32,bytes)` returning `0x1626ba7e`), and `originTxHash` is the **final execution tx hash** — not the `userOpHash`. See [Smart Account Integration](/developer-guide/bridge/smart-account-integration.md) for the full list of hard rules and worked examples (viem + `permissionless.js`, ethers v6 + `@safe-global/protocol-kit`).
{% endhint %}

### Prerequisites

* Wallet on **Goliath Mainnet** (Chain ID `327`).
* Enough native **XCN** for `amount + gas`.
* Client able to produce EIP-712 signatures (every mainstream wallet SDK supports this).

### Step 1 — Build and sign the EIP-712 intent

```ts
const goliathChainId = 327;

const domain = {
  name: "GoliathBridge",
  version: "1",
  chainId: BigInt(goliathChainId),
} as const;

const types = {
  XcnWithdrawIntent: [
    { name: "senderAddress",    type: "address" },
    { name: "recipientAddress", type: "address" },
    { name: "amountAtomic",     type: "string"  },
    { name: "idempotencyKey",   type: "string"  },
    { name: "deadline",         type: "uint256" },
    { name: "nonce",            type: "string"  },
  ],
} as const;

const amountAtomic = parseEther("5000").toString();      // 18-decimal wei string
const idempotencyKey = crypto.randomUUID();
const deadline = Math.floor(Date.now() / 1000) + 1800;    // 30 min from now
const nonce = Date.now().toString();

const message = {
  senderAddress:    signerAddress,
  recipientAddress: recipientOnEthereum,
  amountAtomic,
  idempotencyKey,
  deadline: BigInt(deadline),
  nonce,
};

const signature = await walletClient.signTypedData({
  domain,
  types,
  primaryType: "XcnWithdrawIntent",
  message,
});
```

{% hint style="warning" %}
The EIP-712 **domain must be `GoliathBridge` / version `1` / `chainId = 327`** (or `8901` on testnet). Signatures from a different domain are rejected with `SIGNATURE_DOMAIN_REJECTED`.
{% endhint %}

{% hint style="info" %}
`amountAtomic` for XCN is an **18-decimal wei string** — the same scale the Goliath JSON-RPC uses for `eth_getBalance` and `value` in `eth_sendTransaction`. You do **not** apply the 8-decimal `tinyxcn` conversion when composing the intent. See [XCN Decimal Handling](/developer-guide/decimal-handling.md) for context.
{% endhint %}

### Step 2 — Register the intent

```ts
const res = await fetch(
  "https://bridge.goliath.net/api/v1/bridge/xcn-withdraw-intent",
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      senderAddress:    signerAddress,
      recipientAddress: recipientOnEthereum,
      amountAtomic,
      idempotencyKey,
      deadline,
      nonce,
      signature,
    }),
  },
);
const { intentId, relayerWalletAddress, expiresAt } = await res.json();
```

Store `intentId` locally — you will need it in Step 4 and for recovery if the user reopens the app.

### Step 3 — Send native XCN to the relayer

```ts
const txHash = await walletClient.sendTransaction({
  to: relayerWalletAddress,              // from Step 2
  value: BigInt(amountAtomic),
  chain: goliathMainnet,
});
```

{% hint style="danger" %}
The `to` address **must** be the `relayerWalletAddress` returned by the API in the same flow. Sending XCN to the old relayer or to `BridgeMint` will **not** trigger a withdrawal — there is no automatic refund. Read `relayerWalletAddress` fresh for every transfer.
{% endhint %}

### Step 4 — Bind the origin tx hash

```ts
await fetch(
  "https://bridge.goliath.net/api/v1/bridge/xcn-withdraw-intent/bind-origin",
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      intentId,
      senderAddress: signerAddress,
      originTxHash:  txHash,
    }),
  },
);
```

**Implement retries with exponential backoff.** The onyx-new-frontend reference client uses 5 attempts with a 2 s base delay (2, 4, 8, 16, 32 s). The intent remains valid until `expiresAt`, so binding can succeed any time inside that window.

Error codes you may need to handle:

| HTTP | Error code             | Meaning                                                           |
| ---- | ---------------------- | ----------------------------------------------------------------- |
| 404  | `INTENT_NOT_FOUND`     | Wrong `intentId` or expired and garbage-collected.                |
| 409  | `INTENT_ALREADY_BOUND` | You already bound this intent — stop retrying.                    |
| 410  | `INTENT_EXPIRED`       | Missed the 30 min window. The user will need to restart the flow. |
| 403  | `SENDER_MISMATCH`      | `senderAddress` does not match the intent signer.                 |

### Step 5 — Poll status

Same as the ERC-20 flow: `GET /bridge/status?originTxHash=<txHash>`. Polling returns the same shape whether the underlying mechanism was a burn or a native XCN transfer.

The status sequence is `PENDING_ORIGIN_TX` → `CONFIRMING` → `AWAITING_RELAY` (during the hold) → `PROCESSING_DESTINATION` → `COMPLETED`. If the intent expires before the tx is bound, the status ends at `EXPIRED`.

### Complete Example (viem)

```ts
import { parseEther } from "viem";

async function withdrawXcnToEthereum(amountHuman: string, recipient: `0x${string}`) {
  const amountAtomic = parseEther(amountHuman).toString(); // 18-decimal wei

  // 1. Sign
  const idempotencyKey = crypto.randomUUID();
  const deadline = Math.floor(Date.now() / 1000) + 1800;
  const nonce = Date.now().toString();

  const signature = await walletClient.signTypedData({
    domain: { name: "GoliathBridge", version: "1", chainId: 327n },
    types: {
      XcnWithdrawIntent: [
        { name: "senderAddress",    type: "address" },
        { name: "recipientAddress", type: "address" },
        { name: "amountAtomic",     type: "string"  },
        { name: "idempotencyKey",   type: "string"  },
        { name: "deadline",         type: "uint256" },
        { name: "nonce",            type: "string"  },
      ],
    },
    primaryType: "XcnWithdrawIntent",
    message: {
      senderAddress:    signerAddress,
      recipientAddress: recipient,
      amountAtomic,
      idempotencyKey,
      deadline: BigInt(deadline),
      nonce,
    },
  });

  // 2. Register intent
  const intent = await fetch(
    "https://bridge.goliath.net/api/v1/bridge/xcn-withdraw-intent",
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        senderAddress:    signerAddress,
        recipientAddress: recipient,
        amountAtomic,
        idempotencyKey,
        deadline,
        nonce,
        signature,
      }),
    },
  ).then((r) => r.json());

  // 3. Transfer native XCN
  const txHash = await walletClient.sendTransaction({
    to: intent.relayerWalletAddress,
    value: BigInt(amountAtomic),
    chain: goliath,
  });

  // 4. Bind (with retries)
  await bindWithRetries({
    intentId: intent.intentId,
    senderAddress: signerAddress,
    originTxHash: txHash,
  });

  // 5. Poll until completion
  return pollStatus(txHash);
}

async function bindWithRetries(body: unknown, attempts = 5) {
  for (let i = 0; i <= attempts; i++) {
    try {
      const res = await fetch(
        "https://bridge.goliath.net/api/v1/bridge/xcn-withdraw-intent/bind-origin",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(body),
        },
      );
      if (res.ok) return;
    } catch {}
    if (i < attempts) await sleep(2000 * 2 ** i);
  }
  throw new Error("bind-origin failed after retries");
}
```

## Common Errors

| Symptom                                                | Cause                                                                                   | Fix                                                                                                                                                                                                                                              |
| ------------------------------------------------------ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `BELOW_MINIMUM` on `/fee-quote`                        | Transfer under the per-token minimum.                                                   | Increase the amount.                                                                                                                                                                                                                             |
| `SIGNATURE_DOMAIN_REJECTED` on intent registration     | Signed with wrong `chainId`.                                                            | Use `327` (mainnet) or `8901` (testnet).                                                                                                                                                                                                         |
| `SIGNATURE_MISMATCH`                                   | `senderAddress` in body ≠ signer recovered from signature.                              | Re-sign with the connected account. For smart-account wallets, confirm `senderAddress` is the **contract** address and see [Smart Account Integration](/developer-guide/bridge/smart-account-integration.md).                                    |
| `SIGNATURE_VERIFICATION_UNAVAILABLE` (503)             | Transient RPC error, or a smart-account `senderAddress` is not yet deployed on Goliath. | Back off 1–5 s and retry. If you're using a counterfactual smart account, deploy it first — ERC-6492 pre-deploy signatures are not supported.                                                                                                    |
| `DEADLINE_EXPIRED`                                     | `deadline` in past.                                                                     | Re-sign with a future deadline.                                                                                                                                                                                                                  |
| `INTENT_EXPIRED` with a smart-account wallet           | Bound a `userOpHash` instead of the execution tx hash; intent never paired.             | Resolve `eth_getUserOperationReceipt(userOpHash).receipt.transactionHash` **before** calling `/bind-origin`. See [Smart Account Integration](/developer-guide/bridge/smart-account-integration.md#userophash-vs-execution-tx-hash-the-1-gotcha). |
| Burn reverts with `BridgeMint: insufficient allowance` | No `approve` step.                                                                      | Approve the token before calling `burn`.                                                                                                                                                                                                         |
| Status stuck at `AWAITING_RELAY` for >1 hour           | Hold period not elapsed.                                                                | Wait until `holdUntil`; it's not an error.                                                                                                                                                                                                       |

## Next

* [REST API Reference](/developer-guide/bridge/api-reference.md) — every endpoint used above.
* [Contracts & Events](/developer-guide/bridge/contracts-and-events.md) — full ABIs and testnet addresses.
* [Smart Account Integration](/developer-guide/bridge/smart-account-integration.md) — ERC-1271 / ERC-4337 variant of the native-XCN flow.
* [Ethereum → Goliath](/developer-guide/bridge/ethereum-to-goliath.md) — the reverse direction.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.goliath.net/developer-guide/bridge/goliath-to-ethereum.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
