Deploy a PYUSDX Extension (Sepolia)
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).
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.
| Type | Best for | Backing |
|---|---|---|
| YieldToOne | Partners who want all yield to flow to one treasury address. Simple and recommended for most. | PYUSDX (1:1) |
| MultiMint | Partners 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:
- ExtensionFactory:
0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A
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:
| Field | Type | What to enter |
|---|---|---|
name | string | ERC-20 token name, e.g. Acme Yield USD |
symbol | string | ERC-20 symbol, e.g. ayUSD |
yieldRecipient | address | Treasury that receives all yield |
admin | address | DEFAULT_ADMIN_ROLE holder |
freezeManager | address | Can freeze / unfreeze accounts |
pauser | address | Can pause / unpause the extension |
yieldRecipientManager | address | Can change the yieldRecipient |
versionManager | address | Can pin / unpin the implementation version |
deployMultiMint takes the same params fields, plus one extra:
| Field | Type | What to enter |
|---|---|---|
assetCapManager | address | Manages per-asset deposit caps for the MultiMint extension |
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...
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.
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 exactextensionNameyou used. - Read the receipt: open your deploy transaction on Etherscan → Logs → find the
ExtensionDeployedevent;proxyis 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 withearnerRate > 0(basis points). - Who: the PYUSDX
EARNER_MANAGER. - How: contact the M0 team with your deployed extension address to have yield enabled.
isEarning(yourExtension) (should be true).Use your extension (via Etherscan)
Amounts use 6 decimals: 100 tokens = 100000000.
- Get PYUSDX. Faucet (
0x17c4DE4e1f3ba848F447151635E1BC2e120bEE26) → Write Contract →requestPYUSDX(yourAddress). - Approve the SwapFacility to spend your PYUSDX. PYUSDX (
0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d) → Write as Proxy →approve(spender = SwapFacility, amount). - Swap PYUSDX → extension. SwapFacility (
0x0bC305e7e13113cAEd3f5486849e9518a1cC4173) → Write as Proxy →swapIn(extensionOut = yourExtension, amount, recipient = yourAddress). You receive extension tokens 1:1. - 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). - Claim yield (YieldToOne). yourExtension → Write as Proxy →
claimYield(). Accrued yield is minted to youryieldRecipient.
Verify your extension
Read these (use Read as Proxy on each contract):
| Contract | Call | Expected |
|---|---|---|
| Extension | name() / symbol() | Your token name / symbol |
| Extension | totalSupply() | Sum of all holders' balances |
| Extension | yield() | Accrued, unclaimed yield (> 0 after time passes, once earning) |
| Extension | yieldRecipient() | Your treasury address |
| ExtensionFactory | isApprovedExtension(ext) | true |
| PYUSDX | isEarning(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, andExtensionFactory. (The faucet is the exception: plain "Write Contract".) NotApprovedExtensionwhen swapping. The address you passed is not a factory-deployed extension. Double-check the proxy address andisApprovedExtension.transfer amount exceeds allowance/ swap reverts. You did not approve theSwapFacilityfirst. Approve PYUSDX (forswapIn) or the extension (forswapOut) to theSwapFacility.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.
100tokens =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
| Contract | Address | Etherscan tab |
|---|---|---|
| PYUSDX Token | 0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d | Write as Proxy |
| PYUSDX Faucet | 0x17c4DE4e1f3ba848F447151635E1BC2e120bEE26 | Write Contract |
| ExtensionFactory | 0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A | Write as Proxy |
| SwapFacility | 0x0bC305e7e13113cAEd3f5486849e9518a1cC4173 | Write as Proxy |
| IssuerGateway | 0x693CC3305342B02AC1549B509a704ff944Cd9499 | Write as Proxy |