> ## Documentation Index
> Fetch the complete documentation index at: https://docs.arc.io/llms.txt
> Use this file to discover all available pages before exploring further.

# How to: Process Withdrawals

> Build, sign, and broadcast USDC withdrawal transactions from your exchange hot wallet on Arc.

Send USDC withdrawals from an exchange hot wallet on Arc by validating
addresses, estimating gas, building EIP-1559 transactions, and confirming with
deterministic finality. Send withdrawals as **native USDC** transfers—the
recommended path for exchanges: cheaper gas (\~21,000 vs \~65,000 units) and
receivable by any address. The Memo contract lets you attach compliance metadata
to the same transaction.

## Prerequisites

Before you begin, ensure you have:

* An Arc Testnet RPC endpoint (`https://rpc.testnet.arc.network`)
* A funded hot wallet with USDC for both transfer amounts and gas fees
* The USDC ERC-20 contract address: `0x3600000000000000000000000000000000000000`
* [viem](https://viem.sh) installed (`npm install viem`)

## Steps

### Step 1. Validate the destination address

Verify that the withdrawal address is a valid EIP-55 checksum-validated Ethereum
address before building the transaction. This prevents sending funds to
malformed addresses.

```typescript theme={null}
import { getAddress, isAddress } from "viem";

function validateDestination(address: string): string {
  if (!isAddress(address)) {
    throw new Error(`Invalid address: ${address}`);
  }
  // Return EIP-55 checksum-validated address
  return getAddress(address);
}

const destination = validateDestination(
  "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28",
);
```

The address must be:

* 20 bytes (40 hex characters) with a `0x` prefix
* Valid per EIP-55 checksum rules

### Step 2. Check the blocklist

Arc enforces a blocklist at the protocol level. If the destination address is
blocklisted, the transaction reverts at runtime. Check before sending to avoid
wasted gas.

```typescript theme={null}
import { createPublicClient, http, parseAbi } from "viem";

const client = createPublicClient({
  transport: http("https://rpc.testnet.arc.network"),
});

const USDC_ADDRESS = "0x3600000000000000000000000000000000000000" as const;

async function isBlocklisted(address: string): Promise<boolean> {
  const blocked = await client.readContract({
    address: USDC_ADDRESS,
    abi: parseAbi(["function isBlacklisted(address) view returns (bool)"]),
    functionName: "isBlacklisted",
    args: [address as `0x${string}`],
  });
  return blocked;
}

const blocked = await isBlocklisted(destination);
if (blocked) {
  throw new Error(`Destination ${destination} is blocklisted`);
}
```

### Step 3. Estimate gas units

Call `eth_estimateGas` to determine the gas units required for the native USDC
send. The `value` field is denominated in 18-decimal native wei.

```typescript theme={null}
// 1 USDC in 18-decimal native wei. If you track 6-decimal balances,
// convert with: amount = amount6 * 10n ** 12n
const amount = 1_000_000_000_000_000_000n;

const gasEstimate = await client.estimateGas({
  account: hotWalletAddress,
  to: destination as `0x${string}`,
  value: amount,
});

console.log(`Estimated gas units: ${gasEstimate}`);
// Typical native USDC send: ~21,000 gas units
```

<Note>
  `eth_estimateGas` returns gas units, not a cost in USDC. To calculate the
  cost, multiply by the effective gas price. A native USDC send uses
  approximately 21,000 gas units, while an ERC-20 `transfer()` call uses
  approximately 65,000.
</Note>

### Step 4. Calculate gas cost

Use `eth_gasPrice` to get the current suggested gas price, then compute the
total fee.

```typescript theme={null}
const gasPrice = await client.getGasPrice();

// Gas cost formula: gas_units * gas_price = cost in USDC wei (18 decimals)
const estimatedCost = gasEstimate * gasPrice;

// Convert to human-readable USDC (18 decimals for native gas accounting)
const costInUsdc = Number(estimatedCost) / 1e18;
console.log(`Estimated fee: ${costInUsdc} USDC`);
```

**Gas cost formula:**

```text theme={null}
cost_usdc_wei = gas_used * effective_gas_price
cost_usdc = cost_usdc_wei / 10^18
```

For example, a native USDC send using 21,000 gas at 20 Gwei:

```text theme={null}
21,000 * 20,000,000,000 = 420,000,000,000,000 wei = 0.00042 USDC
```

### Step 5. Build the EIP-1559 transaction

Construct a type-2 (EIP-1559) transaction with `maxFeePerGas` set to at least 20
Gwei. The fee fields and the `value` field are all denominated in USDC wei (18
decimals).

```typescript theme={null}
import { createWalletClient, http, parseGwei } from "viem";

const walletClient = createWalletClient({
  account: hotWalletAccount, // Your signing account
  transport: http("https://rpc.testnet.arc.network"),
});

const txHash = await walletClient.sendTransaction({
  to: destination as `0x${string}`,
  value: amount, // 18-decimal native USDC
  gas: gasEstimate,
  maxFeePerGas: parseGwei("25"), // Must be >= 20 Gwei
  maxPriorityFeePerGas: parseGwei("1"),
  chain: {
    id: 5042002,
    name: "Arc Testnet",
    nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18 },
    rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } },
  },
});

console.log(`Transaction hash: ${txHash}`);
```

<Warning>
  Transactions with `maxFeePerGas` below 20 Gwei may remain pending or fail to
  execute.
</Warning>

<Tip>
  Send withdrawals as native USDC (a plain value transfer). It is cheaper than
  an ERC-20 `transfer()` and any address can receive it. Native sends still emit
  a `Transfer` log from the system emitter (`0xffff…fffe`), so indexers and
  block explorers still capture them. Reach for the ERC-20 `transfer()` only
  when you need 6-decimal exactness or ERC-20 call semantics.
</Tip>

### Step 6. Confirm inclusion

Arc provides deterministic finality. Once a transaction is included in a block,
it is final—no reorgs, no need to wait for additional confirmations.

```typescript theme={null}
const receipt = await client.waitForTransactionReceipt({ hash: txHash });

if (receipt.status === "success") {
  console.log(`Withdrawal confirmed in block ${receipt.blockNumber}`);
  // Credit the withdrawal as complete—no further checks needed
} else {
  console.error("Transaction reverted");
  // Handle failure (see Step 7)
}
```

<Note>
  Unlike other blockchains, you do not need to wait for multiple block
  confirmations. A single block inclusion is final on Arc.
</Note>

### Step 7. Handle failures

Common failure scenarios and how to address them:

| Failure              | Cause                      | Resolution                                                  |
| :------------------- | :------------------------- | :---------------------------------------------------------- |
| Transaction reverts  | Destination is blocklisted | Check the blocklist before sending (Step 2)                 |
| Transaction pending  | `maxFeePerGas` too low     | Resubmit with `maxFeePerGas >= 20 Gwei`                     |
| Out of gas           | Gas estimate too low       | Add a buffer (for example, multiply estimate by 1.2)        |
| Insufficient balance | Hot wallet underfunded     | Top up the hot wallet—USDC covers both the transfer and gas |

```typescript theme={null}
async function processWithdrawal(to: string, amount: bigint): Promise<string> {
  const validAddress = validateDestination(to);

  if (await isBlocklisted(validAddress)) {
    throw new Error(`Address ${validAddress} is blocklisted`);
  }

  // amount is 18-decimal native USDC wei
  const gas = await client.estimateGas({
    account: hotWalletAddress,
    to: validAddress as `0x${string}`,
    value: amount,
  });

  const txHash = await walletClient.sendTransaction({
    to: validAddress as `0x${string}`,
    value: amount,
    gas: (gas * 120n) / 100n, // 20% buffer
    maxFeePerGas: parseGwei("25"),
    maxPriorityFeePerGas: parseGwei("1"),
  });

  const receipt = await client.waitForTransactionReceipt({ hash: txHash });

  if (receipt.status !== "success") {
    throw new Error(`Withdrawal failed: tx ${txHash} reverted`);
  }

  return txHash;
}
```

## Attach memos for compliance

Use the Memo contract to attach metadata (such as internal withdrawal IDs or
compliance references) to transfers. The Memo contract wraps the encoded USDC
transfer call, routes it through CallFrom so the USDC transfer still sees your
hot wallet as `msg.sender`, and emits a `Memo` event for reconciliation.

**Memo contract address:** `0x5294E9927c3306DcBaDb03fe70b92e01cCede505`

For the full viem, ethers, Python, and cURL flow, see
[Send USDC with a transaction memo](/arc/tutorials/send-usdc-with-transaction-memo).

<Info>
  Store a deterministic `memoId` or encoded memo value that links the onchain
  transfer to your internal withdrawal record.
</Info>

## See also

* [Gas and fees](/arc/references/gas-and-fees)—Fee model details and base fee
  mechanics
* [Deterministic finality](/arc/concepts/deterministic-finality)—Why
  single-block confirmation is safe
* [Detect deposits](/integrate/exchanges/deposits)—The corresponding deposit
  detection guide
* [Contract addresses](/arc/references/contract-addresses)—All system contract
  addresses
