PYUSDx

Deploy a PYUSDx Extension (Sepolia)

Deploy your own branded, yield-bearing PYUSDx extension on the Sepolia testnet, via Etherscan or a Foundry script.

Deploy your own branded, yield-bearing PYUSDx extension on the Sepolia testnet. By the end you will have a branded ERC-20 token, backed 1:1 by PYUSDx, that earns yield on its underlying PYUSDx balance. You can deploy either from the Etherscan "Write Contract" interface (no local tooling) or with a Foundry script.

Overview

PYUSDx extensions let partners create branded stablecoins backed by PYUSDx. Your extension will:

  • Wrap PYUSDx 1:1 into your branded token (e.g. "Acme Yield USD").
  • Earn yield on the underlying PYUSDx balance (once enabled by the M0 earner manager).
  • Distribute yield to your treasury (yieldRecipient) while users keep their principal.
  • Provide full liquidity: anyone can swap PYUSDx ↔ your extension anytime through the SwapFacility.

Prerequisites

1. Get Sepolia ETH

You need ETH for gas:

2. Get testnet PYUSDx (via the PYUSDx Faucet)

The PYUSDx Faucet is the entry point for testing on Sepolia.

  • Faucet address: 0x17c4DE4e1f3ba848F447151635E1BC2e120bEE26
  • How to use: open the faucet on Etherscan, go to Contract → Write Contract, call requestPYUSDX(recipient) with your address (or click Self), then Write.
  • Amount: each call sends 100 PYUSDx (100000000, 6 decimals) until the faucet runs dry.

Architecture

PYUSDx and every extension use 6 decimals. Users acquire PYUSDx, approve the SwapFacility, and swap into your extension. Wrapping is 1:1 and can only be performed by the SwapFacility (the extension's wrap / unwrap are onlySwapFacility).

Extensions deployed through the ExtensionFactory are auto-approved in the SwapFacility the moment they are deployed. There is no separate "add to earners list" admin step just to enable swapping.

Extension types

Choose the type that matches your model. Both are deployed through the same ExtensionFactory.

TypeBest forBacking
YieldToOnePartners who want all yield to flow to one treasury address. Simple and recommended for most.PYUSDx (1:1)
MultiMintPartners who want to back the token with multiple assets under per-asset caps, with yield to one recipient.PYUSDx + approved assets (capped)

Deploy your extension

There are two ways to deploy, and both go through the same ExtensionFactory and produce the same deterministic (CREATE3) address. Use the Etherscan path for a no-tooling deploy, or the Foundry path for a reproducible, scripted deploy.

Path 1: Deploy via Etherscan

All protocol contracts (PYUSDx, SwapFacility, ExtensionFactory) are transparent proxies, so use the Contract → Write as Proxy tab.

1. Open the ExtensionFactory. Go to the ExtensionFactory on Etherscan, open Contract → Write as Proxy, and Connect to Web3 with your wallet:

2. Deploy your extension. Call deployYieldToOne (or deployMultiMint). Etherscan renders the params tuple as individual fields. Fill them in and click Write.

extensionName (string) is not your token name. It is a unique label that, together with your deployer address, determines the extension's deterministic (CREATE3) address. Pick something unique like acme-yield-v1.

deployYieldToOne takes these params fields:

FieldTypeWhat to enter
namestringERC-20 token name, e.g. Acme Yield USD
symbolstringERC-20 symbol, e.g. ayUSD
yieldRecipientaddressTreasury that receives all yield
adminaddressDEFAULT_ADMIN_ROLE holder
freezeManageraddressCan freeze / unfreeze accounts
pauseraddressCan pause / unpause the extension
yieldRecipientManageraddressCan change the yieldRecipient
versionManageraddressCan pin / unpin the implementation version

deployMultiMint takes the same params fields, plus one extra:

FieldTypeWhat to enter
assetCapManageraddressManages per-asset deposit caps for the MultiMint extension
For quick testing you can use your own address for every role field (admin, freezeManager, pauser, yieldRecipientManager, versionManager, assetCapManager) and your treasury for yieldRecipient. Every field must be a non-zero address or deployment reverts (ZeroAdmin, ZeroFreezeManager, ZeroPauser, ZeroYieldRecipientManager, ZeroVersionManager, ZeroYieldRecipient, ZeroAssetCapManager).

Path 2: Deploy via Foundry (script)

This path requires access to the private PYUSDX source repository. If you need it, contact the M0 team. Otherwise use the Etherscan path above, which needs no repository access.

Prefer a reproducible, command-line deploy? The repo ships Makefile targets that deploy through the same ExtensionFactory, so you get the same deterministic (CREATE3) address as the Etherscan flow. The Sepolia factory address is already baked into deployments/11155111.json, so you do not need to provide it.

1. Clone the repo and install Foundry.

# install Foundry if you don't have it
curl -L https://foundry.paradigm.xyz | bash && foundryup

git clone <pyusdx-repo-url> && cd PYUSDx
forge install

2. Configure your .env. Create a .env in the repo root with your deploy key, an RPC URL, and your extension config:

# Secrets. In the make path `op run` resolves op:// references and passes plain
# values through, so you can mix the two; only the private key really needs vaulting.
# Not using 1Password? Use a plain value for PRIVATE_KEY too and run forge directly (see note below).
PRIVATE_KEY=op://<vault>/<item>/<field>
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/<key>    # any Sepolia RPC
ETHERSCAN_API_KEY=<key>                               # only needed for --verify

# Extension config
EXTENSION_NAME=acme-yield-v1          # unique CREATE3 label (deployer + name -> address); NOT the token name
EXTENSION_TOKEN_NAME=Acme Yield USD   # ERC20 name
EXTENSION_TOKEN_SYMBOL=ayUSD          # ERC20 symbol
YIELD_RECIPIENT=0x...                 # treasury that receives all yield
ADMIN=0x...                           # DEFAULT_ADMIN_ROLE holder
FREEZE_MANAGER=0x...
PAUSER=0x...
YIELD_RECIPIENT_MANAGER=0x...
VERSION_MANAGER=0x...

# MultiMint only, additionally:
ASSET_CAP_MANAGER=0x...
As with the Etherscan flow, every address field must be non-zero or the deploy reverts. For quick testing you can use your own address for every role field and your treasury for YIELD_RECIPIENT.

3. Deploy.

# YieldToOne
make deploy-yield-to-one-sepolia

# MultiMint
make deploy-multi-mint-sepolia

The script logs the implementation and proxy addresses, records the proxy in deployments/11155111.json, and verifies the contract on Etherscan.

The make targets inject secrets with the 1Password CLI (op run), so by default PRIVATE_KEY in .env is an op://... reference. If you do not use 1Password, put plain values in .env and run the script directly (Foundry auto-loads .env): forge script script/deploy/DeployYieldToOne.s.sol:DeployYieldToOne --rpc-url sepolia --broadcast --verify (use DeployMultiMint.s.sol:DeployMultiMint for MultiMint).

Find your extension's address

You have two ways to get the deployed proxy address (both paths produce the same address):

  • Predict it (before or after): ExtensionFactory → Read as Proxy → getExtensionAddress(deployer, extensionName), using your deployer address and the exact extensionName you used.
  • Read the receipt: open your deploy transaction on Etherscan → Logs → find the ExtensionDeployed event; proxy is your extension address.

Confirm it is registered: ExtensionFactory → Read as Proxy → getExtensionType(extension) returns 1 (YieldToOne) or 2 (MultiMint), and isApprovedExtension(extension) returns true.

Enable yield (M0 action)

Your extension can be swapped into immediately, but it will not earn PYUSDx yield until the PYUSDx earner manager registers it as an earner.

  • What: the earner manager calls setAccountInfo(yourExtension, earnerRate, feeRate, claimRecipient) on PYUSDx with earnerRate > 0 (basis points).
  • Who: the PYUSDx EARNER_MANAGER.
  • How: contact the M0 team with your deployed extension address to have yield enabled.
This step is optional if you only want to test wrapping/swapping. It is required for yield to accrue. Verify with PYUSDx → Read as Proxy → isEarning(yourExtension) (should be true).

Use your extension (via Etherscan)

Amounts use 6 decimals: 100 tokens = 100000000.

  1. Get PYUSDx. Faucet (0x17c4DE4e1f3ba848F447151635E1BC2e120bEE26) → Write Contract → requestPYUSDX(yourAddress).
  2. Approve the SwapFacility to spend your PYUSDx. PYUSDx (0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d) → Write as Proxy → approve(spender = SwapFacility, amount).
  3. Swap PYUSDx → extension. SwapFacility (0x0bC305e7e13113cAEd3f5486849e9518a1cC4173) → Write as Proxy → swapIn(extensionOut = yourExtension, amount, recipient = yourAddress). You receive extension tokens 1:1.
  4. Swap extension → PYUSDx. First approve the SwapFacility on your extension: yourExtension → Write as Proxy → approve(SwapFacility, amount). Then SwapFacility → swapOut(extensionIn = yourExtension, amount, recipient = yourAddress).
  5. Claim yield (YieldToOne). yourExtension → Write as Proxy → claimYield(). Accrued yield is minted to your yieldRecipient.

Test cross-chain transfers (testnet)

The Portal bridges PYUSDx and your extension between testnets over LayerZero. The testnets are peered hub-and-spoke: Arbitrum Sepolia, Monad Testnet, and Base Sepolia each connect to Ethereum Sepolia (and back). There are no direct spoke-to-spoke routes, so every transfer is to or from Ethereum Sepolia.

The Portal has the same address on every chain: 0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7. Testnet chain IDs are Ethereum Sepolia 11155111, Arbitrum Sepolia 421614, Monad Testnet 10143, and Base Sepolia 84532. Full per-chain address lists are on the PYUSDx platform deployments page.

Before you bridge

  • Deploy your extension on both chains. For the recipient to receive your extension on the destination, it must exist there at the same address. Repeat Deploy your extension on the destination testnet with the same extensionName — CREATE3 gives it the same address. If your extension is not deployed on the destination, the recipient receives plain PYUSDx instead.
  • Hold the token and gas on both ends. You need the token you are bridging on the source chain, plus native gas on both chains: the source pays the LayerZero fee and the destination executes delivery.

Bridge your extension (via Etherscan)

Amounts use 6 decimals: 100 tokens = 100000000. Do all of this on the source chain's explorer.

  1. Quote the fee. Portal → Read as Proxy → quote(destinationChainId). It returns the LayerZero fee in wei; you pass this as the transaction value in step 3.
  2. Approve the Portal to spend your token. yourExtension → Write as Proxy → approve(spender = Portal, amount).
  3. Send. Portal → Write as Proxy → sendToken, with the fee from step 1 set as the payable value (in wei):
    FieldValue
    amountamount in 6-decimal base units
    sourceTokenyour extension address
    destinationChainIdthe destination chain ID
    destinationTokenyour extension address as bytes32 — left-pad the 20-byte address with 12 zero bytes: 0x000000000000000000000000 + the 40-hex address
    recipientthe recipient address as bytes32 (same padding)
    refundAddressyour address as bytes32 (receives any leftover fee)
  4. Track and confirm. Paste the transaction hash into LayerZero Scan (testnet); delivery takes a few minutes. On arrival the Portal unwraps your extension to PYUSDx, bridges it, and re-wraps into your extension for the recipient — confirm the recipient's balanceOf on the destination chain's explorer.
To bridge the base token instead of an extension, set both sourceToken and destinationToken to the PYUSDx address 0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d. PYUSDx is deployed on every chain, so no destination deploy is needed.

Verify your extension

Read these (use Read as Proxy on each contract):

ContractCallExpected
Extensionname() / symbol()Your token name / symbol
ExtensiontotalSupply()Sum of all holders' balances
Extensionyield()Accrued, unclaimed yield (> 0 after time passes, once earning)
ExtensionyieldRecipient()Your treasury address
ExtensionFactoryisApprovedExtension(ext)true
PYUSDxisEarning(ext)true (after M0 enables yield)

Common issues

  • The Write tab is missing deployYieldToOne / swapIn / approve. You opened the plain "Write Contract" tab on a proxy. Use Write as Proxy for PYUSDx, SwapFacility, and ExtensionFactory. (The faucet is the exception: plain "Write Contract".)
  • NotApprovedExtension when swapping. The address you passed is not a factory-deployed extension. Double-check the proxy address and isApprovedExtension.
  • transfer amount exceeds allowance / swap reverts. You did not approve the SwapFacility first. Approve PYUSDx (for swapIn) or the extension (for swapOut) to the SwapFacility.
  • yield() is 0. Your extension is not registered as a PYUSDx earner yet. Contact M0 to enable yield (see above), then let some time pass.
  • Wrong amounts. PYUSDx and extensions use 6 decimals. 100 tokens = 100000000.

FAQ

Do my users need any special role?

No. Once the extension is deployed via the factory it is auto-approved, and anyone can swap PYUSDx ↔ your extension via the SwapFacility. Roles (admin, freezeManager, etc.) only govern administration of your extension.

What's the difference between PYUSDx and an extension token?

PYUSDx is the shared non-rebasing base token. Your extension is a branded 1:1 wrapper around PYUSDx that captures the yield on its PYUSDx backing and routes it to your yieldRecipient.

Can users swap directly between two extensions?

Yes. SwapFacility.swap(tokenIn, tokenOut, amount, recipient) accepts PYUSDx or any approved extension on either side.

Why is the deployed address deterministic?

The factory uses CREATE3 with a salt derived from your deployer address plus extensionName. The same deployer and name yield the same address, so pick a unique extensionName per extension.

Sepolia addresses

ContractAddressEtherscan tab
PYUSDx Token0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14dWrite as Proxy
PYUSDx Faucet0x17c4DE4e1f3ba848F447151635E1BC2e120bEE26Write Contract
ExtensionFactory0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2AWrite as Proxy
SwapFacility0x0bC305e7e13113cAEd3f5486849e9518a1cC4173Write as Proxy
Portal0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7Write / Read as Proxy
IssuerGateway0x693CC3305342B02AC1549B509a704ff944Cd9499Write as Proxy
Copyright © M0 Foundation 2026