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)

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): ExtensionFactoryRead as ProxygetExtensionAddress(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: ExtensionFactoryRead as ProxygetExtensionType(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 ProxyisEarning(yourExtension) (should be true).

Use your extension (via Etherscan)

Amounts use 6 decimals: 100 tokens = 100000000.

  1. Get PYUSDX. Faucet (0x17c4DE4e1f3ba848F447151635E1BC2e120bEE26) → Write ContractrequestPYUSDX(yourAddress).
  2. Approve the SwapFacility to spend your PYUSDX. PYUSDX (0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d) → Write as Proxyapprove(spender = SwapFacility, amount).
  3. Swap PYUSDX → extension. SwapFacility (0x0bC305e7e13113cAEd3f5486849e9518a1cC4173) → Write as ProxyswapIn(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 Proxyapprove(SwapFacility, amount). Then SwapFacility → swapOut(extensionIn = yourExtension, amount, recipient = yourAddress).
  5. Claim yield (YieldToOne). yourExtension → Write as ProxyclaimYield(). Accrued yield is minted to your yieldRecipient.

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
IssuerGateway0x693CC3305342B02AC1549B509a704ff944Cd9499Write as Proxy
Copyright © M0 Foundation 2026