Smart Contracts
All 10 UPD smart contracts — their purpose, interfaces, deploy order, and roles.
Smart Contracts
UPD consists of 10 smart contracts plus 3 external libraries. All written in Solidity 0.8.29, deployed with Foundry via CreateX (CREATE2) with safeCreate2() idempotency.
Deploy Order
The contracts must be deployed in this order (no circular dependencies):
1. PriceOracle
2. UPDToken
3. PositionEscrow (implementation)
4. StabilizerEscrow (implementation)
5. UpgradeableBeacon (for PositionEscrow)
6. UpgradeableBeacon (for StabilizerEscrow)
7. BeaconProxyFactory
8. StabilizerNFT
9. OvercollateralizationReporter
10. InsuranceEscrow
11. BridgeEscrow
12. sUPD(Twelve deployment steps; ten distinct contract types — PositionEscrow and StabilizerEscrow are deployed once as implementations and then again as one BeaconProxy per Stabilizer NFT.)
BeaconProxy Infrastructure
Steps 3-7 set up the escrow upgradeability layer. The two implementation contracts are deployed first, then each gets an UpgradeableBeacon pointing to it. BeaconProxyFactory is the factory that StabilizerNFT calls to deploy new escrow proxies. See Escrow Upgradeability for details.
Contracts
UPDToken
Role: Standard ERC20 for the $1 stablecoin.
interface IUPDToken is IERC20, IERC20Permit {
function mint(address to, uint256 amount) external; // onlyRole(CONTROLLER_ROLE)
function burn(uint256 amount) external; // onlyRole(CONTROLLER_ROLE)
}StabilizerNFT holds the single CONTROLLER_ROLE (mint + burn). No other address can mint or burn. No blacklist function exists.
StabilizerNFT
Role: Orchestrates minting, burning, collateral allocation, and liquidation. The single holder of CONTROLLER_ROLE on UPDToken.
interface IStabilizerNFT {
// --- User entry points ---
function mintUPD(address to, PriceAttestationQuery calldata price)
external payable returns (AllocationResult memory);
function mintUPDWithStETH(address to, uint256 stEthAmount, PriceAttestationQuery calldata price)
external returns (AllocationResult memory);
function burnUPD(uint256 updAmount, PriceAttestationQuery calldata price)
external returns (uint256 stEthReturned);
function liquidatePosition(
uint256 liquidatorTokenId, // 0 for default 110% threshold; >0 for ID-based threshold
uint256 positionTokenId,
uint256 updAmountToLiquidate,
PriceAttestationQuery calldata price
) external;
// --- Stabilizer NFT lifecycle & owner-only ops ---
function mint(address to) external returns (uint256 tokenId);
function addUnallocatedFundsEth(uint256 tokenId) external payable;
function addUnallocatedFundsStETH(uint256 tokenId, uint256 stETHAmount) external;
function removeUnallocatedFunds(uint256 tokenId, uint256 stETHAmount) external;
function setMinCollateralizationRatio(uint256 tokenId, uint256 newRatio) external;
function unallocateMyself(uint256 tokenId, uint256 updAmount, PriceAttestationQuery calldata price) external;
function rebalanceToMinRatio(uint256 tokenId, PriceAttestationQuery calldata price) external returns (uint256 stEthFreed);
// --- Internal callback ---
function onCollateralAdded(uint256 tokenId, uint256 stEthAmount) external;
// --- Views ---
function positionEscrows(uint256 tokenId) external view returns (address);
function stabilizerEscrows(uint256 tokenId) external view returns (address);
function minCollateralRatios(uint256 tokenId) external view returns (uint256);
}onCollateralAdded is called by PositionEscrow when collateral is added directly (via addCollateralEth, addCollateralStETH, or plain ETH receive()). It notifies the OvercollateralizationReporter so the snapshot stays current without waiting for syncFromChain().
Escrow instances are deployed via BeaconProxyFactory. Each new Stabilizer gets its own PositionEscrow and StabilizerEscrow proxy, all pointing at the shared beacon implementations.
Uses three external Solidity libraries (each deployed separately, called via DELEGATECALL from StabilizerNFT):
LinkedListLib— sorted doubly-linked list keyed by NFT ID for priority ordering of allocation/burn loopsCollateralMathLib— pure math for allocation ratios, liquidation thresholds, and payout splitsAllocationLib— the shared allocation loop used bymintUPDandmintUPDWithStETH
Splitting these out keeps StabilizerNFT under the EIP-170 24,576-byte contract size limit.
PositionEscrow
Role: Holds stETH backing for a specific Stabilizer's active UPD position.
One PositionEscrow is deployed as a BeaconProxy instance per Stabilizer. Upgrading the UpgradeableBeacon atomically upgrades all existing PositionEscrow instances without redeployment.
StabilizerEscrow
Role: Holds unallocated stETH per Stabilizer — collateral deposited but not yet backing UPD.
Deployed as a BeaconProxy instance per Stabilizer. Like PositionEscrow, upgrading the beacon atomically upgrades all existing StabilizerEscrow instances.
PriceOracle
Role: Validates signed ETH/USD price attestations from Chainlink and Uniswap V3.
interface IPriceOracle {
function validate(PriceAttestation calldata attestation)
external view returns (uint256 price);
}UUPS upgradeable — can add new price sources without redeploying the full system.
OvercollateralizationReporter
Role: Tracks the system-wide collateralization health on-chain using stETH share-based accounting.
The reporter uses stETH shares (not balances) to track collateral. Because stETH rebases daily, a raw balance check would miss accrued yield between snapshots. Share-based tracking means the reporter's view of total collateral automatically includes yield — getTotalStEthCollateral() converts the stored share count to the current stETH value at read time.
interface IOvercollateralizationReporter {
/// @notice Permissionless. Iterates all PositionEscrows, reads actual
/// stETH shares held, and resets the internal snapshot.
function syncFromChain() external;
/// @notice View. Converts the stored share snapshot to the current
/// stETH value (yield-inclusive).
function getTotalStEthCollateral() external view returns (uint256 stEthAmount);
/// @notice Called by StabilizerNFT on mint, burn, and addCollateral
/// to keep the snapshot current between full syncs.
/// @param stEthDelta Positive on collateral increase, negative on decrease.
function updateSnapshot(int256 stEthDelta) external;
}How it stays current:
- On every mint/burn/collateral change:
StabilizerNFTcallsupdateSnapshot(stEthDelta)so the snapshot reflects the delta immediately. - Periodic reconciliation: Anyone can call
syncFromChain()to walk all escrows and reset the snapshot to ground truth. This is permissionless and corrects any drift. - Yield accrual: Because the snapshot stores shares,
getTotalStEthCollateral()returns the yield-inclusive value without requiring an explicit update.
No admin required
syncFromChain() is permissionless — any address can call it. It reads stETH shares directly from each PositionEscrow and resets the snapshot. There is no privileged keeper.
InsuranceEscrow
Role: Emergency buffer for undercollateralization events. Funded by protocol fees or manual injection.
BridgeEscrow
Role: Lock/release mechanism for L1↔L2 bridging of UPD. On L2 chains it holds CONTROLLER_ROLE on UPDToken (instead of StabilizerNFT, which is L1-only).
BeaconProxyFactory
Role: Tiny helper that holds the BeaconProxy creation code. Called by StabilizerNFT.mint() to deploy each new Stabilizer's PositionEscrow and StabilizerEscrow proxies.
This factory exists for one reason: keeping the BeaconProxy bytecode out of StabilizerNFT itself, which would otherwise push the orchestrator over the EIP-170 contract size limit.
sUPD
Role: Yield-bearing staked UPD share token. Owns its own Stabilizer NFT. Stake UPD → it is burned via StabilizerNFT.burnUPD, and the freed stETH becomes sUPD's unallocated overcollateral. Unstake → sUPD gathers stETH from its escrows and mints fresh UPD for the user via mintUPDWithStETH.
shareValue accrues linearly per-second at a configurable APY (capped at 50% by setAnnualYield). Yield is generated off-chain by a REBALANCER_ROLE running a delta-neutral hedge and topped up via depositCollateral(). Four redemption paths (unstakeToUPD, unstakeWithExtraUPD, unstakeProRata, unstakeQueued) handle different liquidity regimes — see sUPD concepts.
ABIs
import {
UPD_TOKEN_ABI,
STABILIZER_NFT_ABI,
PRICE_ORACLE_ABI,
POSITION_ESCROW_ABI,
STABILIZER_ESCROW_ABI,
OVERCOLLATERALIZATION_REPORTER_ABI,
INSURANCE_ESCROW_ABI,
BRIDGE_ESCROW_ABI,
SUPD_ABI,
} from '@permissionless-technologies/upd-sdk/contracts'BeaconProxyFactory is internal infrastructure — its ABI is not exported because user code never calls it directly. StabilizerNFT.mint() is the only consumer.
Contract Addresses
Addresses are available in the SDK for each supported chain:
import { getContractAddresses } from '@permissionless-technologies/upd-sdk/contracts'
const addresses = getContractAddresses(chainId)
// { updToken, stabilizerNFT, priceOracle, ... }Library Contracts
All three libraries — LinkedListLib, CollateralMathLib, and AllocationLib — are deployed separately and called via DELEGATECALL from StabilizerNFT. This keeps the orchestrator under the EIP-170 24,576-byte contract size limit. Each library exposes only public functions (the Solidity marker for external linkage).