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

# PolyUSD Guide

> How to wrap and unwrap PolyUSD — the collateral token for Polymarket V2.

PolyUSD (pUSD) is the collateral token for Polymarket's V2 exchange system. It is backed 1:1 by USDC and replaces USDC.e as the exchange collateral. To trade on V2, you need PolyUSD.

**Token info:** Name: Polymarket USD | Symbol: pUSD | Decimals: 6 | [View on Polygonscan](https://polygonscan.com/token/0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb)

## How PolyUSD Works

According to Polymarket's announcement, PolyUSD is backed 1:1 by USDC. Power users and API traders can wrap either **USDC or USDC.e** into PolyUSD. For most frontend users, the transition is seamless — the Polymarket UI handles wrapping automatically with a one-time approval.

There are two wrapping paths:

| Path                  | Contract                                     | Input Token          | Status                                              |
| --------------------- | -------------------------------------------- | -------------------- | --------------------------------------------------- |
| **Collateral Onramp** | `0x93070a847efef7f70739046a929d47a521f5b8ee` | USDC.e (bridged)     | **Active** — working today                          |
| **PermissionedRamp**  | `0xebc2459ec962869ca4c0bd1e06368272732bcb08` | Native USDC (Circle) | **Not active yet** — deployed but zero transactions |

Both paths produce the same PolyUSD token. The V2 exchange doesn't care how you obtained your PolyUSD.

<Info>
  The **USDC.e → Onramp** path is what's working right now. We've tested it on mainnet and verified the full flow. The native USDC path via the PermissionedRamp exists on-chain but has not processed any transactions yet. When Polymarket activates it, wrapping native USDC will also be supported.
</Info>

## Quick Reference

|                                                | Address                                      |
| ---------------------------------------------- | -------------------------------------------- |
| PolyUSD Token                                  | `0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb` |
| Collateral Onramp (USDC.e → PolyUSD)           | `0x93070a847efef7f70739046a929d47a521f5b8ee` |
| Collateral Offramp (PolyUSD → USDC.e)          | `0x2957922eb93258b93368531d39facca3b4dc5854` |
| PermissionedRamp (native USDC, not yet active) | `0xebc2459ec962869ca4c0bd1e06368272732bcb08` |
| USDC.e (bridged USDC)                          | `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` |
| Backing Vault                                  | `0xC417fD8E9661c0d2120B64a04Bb3278C17E99DB1` |

## Wrapping: USDC.e → PolyUSD (Active Path)

To get PolyUSD today, wrap your USDC.e through the Collateral Onramp contract.

### Steps

1. **Approve** the Onramp to spend your USDC.e
2. **Call** `wrap(underlyingToken, recipient, amount)` on the Onramp

<CodeGroup>
  ```javascript Node.js theme={null}
  import { ethers } from "ethers";

  const provider = new ethers.JsonRpcProvider("YOUR_RPC_URL");
  const wallet = new ethers.Wallet("YOUR_PRIVATE_KEY", provider);

  const USDC_E = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174";
  const ONRAMP = "0x93070a847efef7f70739046a929d47a521f5b8ee";

  // 1. Approve Onramp for USDC.e
  const usdc = new ethers.Contract(USDC_E, [
    "function approve(address,uint256) returns (bool)",
    "function balanceOf(address) view returns (uint256)"
  ], wallet);

  await (await usdc.approve(ONRAMP, ethers.MaxUint256)).wait();

  // 2. Wrap USDC.e → PolyUSD
  const amount = await usdc.balanceOf(wallet.address);
  const wrapData = new ethers.Interface(["function wrap(address,address,uint256)"])
    .encodeFunctionData("wrap", [USDC_E, wallet.address, amount]);

  const tx = await wallet.sendTransaction({ to: ONRAMP, data: wrapData, gasLimit: 300000 });
  await tx.wait();

  console.log("Wrapped", (Number(amount) / 1e6).toFixed(2), "USDC.e → PolyUSD");
  ```

  ```python Python theme={null}
  from web3 import Web3

  w3 = Web3(Web3.HTTPProvider("YOUR_RPC_URL"))
  account = w3.eth.account.from_key("YOUR_PRIVATE_KEY")

  USDC_E = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
  ONRAMP = "0x93070a847efef7f70739046a929d47a521f5b8ee"

  # 1. Approve
  usdc = w3.eth.contract(address=USDC_E, abi=[{
      "name": "approve", "type": "function",
      "inputs": [{"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"}],
      "outputs": [{"name": "", "type": "bool"}]
  }])
  tx = usdc.functions.approve(ONRAMP, 2**256 - 1).build_transaction({
      "from": account.address, "nonce": w3.eth.get_transaction_count(account.address)
  })
  w3.eth.send_raw_transaction(account.sign_transaction(tx).raw_transaction)

  # 2. Wrap USDC.e → PolyUSD
  amount = w3.eth.call({"to": USDC_E, "data": usdc.functions.balanceOf(account.address)._encode_transaction_data()})
  amount = int.from_bytes(amount, "big")

  wrap_data = Web3.keccak(text="wrap(address,address,uint256)")[:4]
  wrap_data += bytes.fromhex(USDC_E[2:].zfill(64))
  wrap_data += bytes.fromhex(account.address[2:].zfill(64))
  wrap_data += amount.to_bytes(32, "big")

  tx = {
      "from": account.address,
      "to": ONRAMP,
      "data": "0x" + wrap_data.hex(),
      "gas": 300000,
      "nonce": w3.eth.get_transaction_count(account.address),
      "gasPrice": w3.eth.gas_price,
      "chainId": 137,
  }
  signed = account.sign_transaction(tx)
  w3.eth.send_raw_transaction(signed.raw_transaction)
  print(f"Wrapped {amount / 1e6:.2f} USDC.e → PolyUSD")
  ```
</CodeGroup>

### Using the polynode SDK

The SDK handles wrapping automatically:

<CodeGroup>
  ```rust Rust theme={null}
  // Wrap 1 USDC.e → PolyUSD
  let tx_hash = trader.wrap_to_polyusd(1_000_000).await?;

  // Check balance
  let balance = trader.get_polyusd_balance().await?;
  ```

  ```typescript TypeScript (SDK) theme={null}
  // Wrap 1 USDC.e → PolyUSD
  const txHash = await trader.wrapToPolyUsd(1_000_000n);

  // Check balance
  const balance = await trader.getPolyUsdBalance();
  ```

  ```python Python theme={null}
  # Wrap 1 USDC.e → PolyUSD (wrap_to_polyusd is async)
  tx_hash = await trader.wrap_to_polyusd(1_000_000)

  # Balance getter is synchronous (returns raw int)
  balance_raw = trader.get_polyusd_balance()
  ```
</CodeGroup>

### Wrap Function Signature

```
function wrap(address underlyingToken, address recipient, uint256 amount)
```

* `underlyingToken` — the USDC.e contract address (`0x2791Bca1...`)
* `recipient` — who receives the PolyUSD (usually yourself)
* `amount` — raw amount in 6-decimal units (1 USDC = 1,000,000)

## Unwrapping: PolyUSD → USDC.e

Unwrapping is designed to be **permissionless** — any EOA can call `CollateralOfframp.unwrap(asset, to, amount)` and burn their pUSD in exchange for USDC.e. The function signature mirrors wrap:

```
function unwrap(address underlyingToken, address recipient, uint256 amount)
```

<Info>
  **Unwrap is live today.** The Collateral Offramp holds `WRAPPER_ROLE` on the pUSD token and processes user unwraps directly. Verified 2026-04-21 via tx [`0x154a906c5790e272cb209146e6716ff255d92791519f642f5c43a4997a47e030`](https://polygonscan.com/tx/0x154a906c5790e272cb209146e6716ff255d92791519f642f5c43a4997a47e030) (0.1 pUSD → 0.1 USDC.e from a Safe wallet via the Polymarket relayer).

  Alternative paths that also still work:

  * **Polymarket's withdraw UI** at `bridge.polymarket.com/withdraw` — routes through a Uniswap v3 pool or off-chain witness flow.
  * **Settlement through the V2 exchange** — `CTFCollateralAdapter` handles unwrapping during `matchOrders`.
  * **Redeeming resolved positions** — ConditionalTokens pays out USDC.e directly.
</Info>

### Steps

1. **Approve** the Offramp to spend your pUSD
2. **Call** `unwrap(USDC.e, recipient, amount)` on the Offramp

Both steps are EOA-callable. For **Safe wallets**, route both through the Polymarket relayer (gasless) — the polynode SDK does this automatically, see below.

### Via the polynode SDK (recommended for Safe wallets)

The SDK's `wrapToPolyUsd()` / `unwrapFromPolyUsd()` automatically detect your wallet type. If you onboarded with `ensureReady(privateKey)` (Safe wallet, the default), both calls route through the Polymarket relayer and execute as gasless Safe transactions signed by your EOA. Amounts are raw 6-decimal integers (`1_000_000` = \$1).

<CodeGroup>
  ```typescript TypeScript theme={null}
  // polynode-sdk >= 0.9.1
  import { PolyNodeTrader } from 'polynode-sdk';

  const trader = new PolyNodeTrader({
    polynodeKey: 'pn_live_...',
    exchangeVersion: 'v2',
    builderCredentials: { key: '...', secret: '...', passphrase: '...' },
  });
  await trader.ensureReady('0xYOUR_EOA_PRIVATE_KEY');

  // Wrap 10 USDC.e → pUSD (gasless via Safe+relayer)
  const wrapTx = await trader.wrapToPolyUsd(10_000_000n);

  // Unwrap 5 pUSD → USDC.e (gasless via Safe+relayer)
  const unwrapTx = await trader.unwrapFromPolyUsd(5_000_000n);
  ```

  ```python Python theme={null}
  # polynode >= 0.9.1
  from polynode.trading import PolyNodeTrader, TraderConfig, ExchangeVersion, BuilderCredentials

  trader = PolyNodeTrader(TraderConfig(
      polynode_key="pn_live_...",
      exchange_version=ExchangeVersion.V2,
      builder_credentials=BuilderCredentials(key="...", secret="...", passphrase="..."),
  ))
  await trader.ensure_ready("0xYOUR_EOA_PRIVATE_KEY")

  # Wrap 10 USDC.e → pUSD
  wrap_tx = await trader.wrap_to_polyusd(10_000_000)

  # Unwrap 5 pUSD → USDC.e
  unwrap_tx = await trader.unwrap_from_polyusd(5_000_000)
  ```

  ```rust Rust theme={null}
  // polynode >= 0.12.1
  use polynode::trading::{
      PolyNodeTrader, TraderConfig, ExchangeVersion, BuilderCredentials, PrivateKeySigner,
  };

  let mut trader = PolyNodeTrader::new(TraderConfig {
      polynode_key: "pn_live_...".into(),
      exchange_version: ExchangeVersion::V2,
      builder_credentials: Some(BuilderCredentials {
          key: "...".into(), secret: "...".into(), passphrase: "...".into(),
      }),
      ..Default::default()
  })?;
  let signer = PrivateKeySigner::from_hex("0xYOUR_EOA_PRIVATE_KEY")?;
  trader.ensure_ready(Box::new(signer), None).await?;

  // Wrap 10 USDC.e → pUSD
  let wrap_tx = trader.wrap_to_polyusd(10_000_000).await?;

  // Unwrap 5 pUSD → USDC.e
  let unwrap_tx = trader.unwrap_from_polyusd(5_000_000).await?;
  ```
</CodeGroup>

**Which wallet type am I using?** `ensureReady(privateKey)` defaults to `POLY_GNOSIS_SAFE` — your EOA controls a Safe at the derived funder address, and SDK methods route through the Polymarket relayer (gasless). If you pass `{ type: SignatureType.EOA }` explicitly, the SDK signs directly from the EOA and you pay MATIC for gas. For Safe mode, `builderCredentials` is required because the SDK uses your Polymarket builder HMAC to authenticate relayer requests — mint one at [polymarket.com/settings?tab=builder](https://polymarket.com/settings?tab=builder).

<Warning>
  **Placing an order right after a wrap?** Call `await trader.refreshBalanceAllowance()` in between. The V2 CLOB caches its view of your balance + allowances per API key; for a few seconds after wrap, that view is stale. If you submit an order before the cache refreshes, the CLOB can return a cryptic `"error parsing fee rate bps"` instead of the real "balance not yet visible" message. A single `refreshBalanceAllowance()` call fixes it. `ensureReady` already does this for you on first setup — `wrapToPolyUsd` does not, because not every wrap is followed by an immediate order.
</Warning>

### Related contracts

| Contract                          | Address                                      | Role on pUSD (today) |
| --------------------------------- | -------------------------------------------- | -------------------- |
| Collateral Onramp (wrap)          | `0x93070a847efef7f70739046a929d47a521f5b8ee` | WRAPPER\_ROLE ✓      |
| Collateral Offramp (unwrap)       | `0x2957922eb93258b93368531d39facca3b4dc5854` | WRAPPER\_ROLE ✓      |
| PermissionedRamp (witness-signed) | `0xebc2459ec962869ca4c0bd1e06368272732bcb08` | WRAPPER\_ROLE ✓      |
| CtfCollateralAdapter              | `0xAdA100Db00Ca00073811820692005400218FcE1f` | WRAPPER\_ROLE ✓      |
| NegRiskCtfCollateralAdapter       | `0xadA2005600Dec949baf300f4C6120000bDB6eAab` | WRAPPER\_ROLE ✓      |

## How It Works Under the Hood

PolyUSD is backed 1:1 by collateral held in a vault contract (`0xC417fD8E...`). When you wrap:

1. Your USDC.e transfers to the vault
2. PolyUSD is minted to your wallet

When you unwrap:

1. Your PolyUSD is burned
2. USDC.e is released from the vault to your wallet

During V2 trade settlements, the exchange automatically handles PolyUSD ↔ USDC.e conversion through the CTFCollateralAdapter. The ConditionalTokens contract underneath still uses USDC.e for position splitting — PolyUSD is the user-facing layer that sits on top.

<Note>
  The vault currently holds USDC.e as the backing collateral. Polymarket has stated that PolyUSD is backed 1:1 by USDC. A PermissionedRamp contract exists for native USDC wrapping but is not yet active. When it activates, the vault may hold a mix of USDC.e and native USDC, but PolyUSD remains 1:1 redeemable regardless of the backing composition.
</Note>

## Tracking PolyUSD Events

polynode detects PolyUSD wrapping and unwrapping through the settlement stream. These appear as `deposit` events:

* **Wrap** (deposit): `direction: "deposit"`, `from` is the Onramp contract
* **Unwrap** (withdrawal): `direction: "withdrawal"`, `to` is the user wallet

Subscribe to `deposits` on the WebSocket to receive these events. Internal settlement wraps/unwraps (between exchange and adapter contracts) are filtered out automatically.

## Key Facts

* **Decimals:** 6 (same as USDC)
* **Backing:** 1:1 by USDC (currently held as USDC.e in the vault)
* **Wrapping:** USDC.e via Onramp (active) or native USDC via PermissionedRamp (not yet active)
* **Proxy:** ERC-1967 upgradeable proxy
* **Chain:** Polygon mainnet (chain ID 137)
* **Minimum wrap:** No minimum (tested with amounts as low as \$0.007)
