Polkadot USD over-collateralised debt token

1d 14hrs ago
0

RFC: https://github.com/polkadot-fellows/RFCs/pull/155

Overview

Polkadot USD over-collateralised debt token (PUSD) is a new DOT‑collateralized stablecoin deployed on Asset Hub. It is an overcollateralized stablecoin backed purely by DOT. The implementation follows the Honzon protocol pioneered by Acala.

Unresolved Questions

  • Financial Fellowship governance model: How should the Financial Fellowship be structured and selected? What specific powers should it have over risk parameters, and what checks and balances should exist to prevent governance capture or misaligned incentives?
  • Oracle infrastructure design: How exactly should we get price data from other parachains (via XCM or state-proofs)? How do we ensure the implementation is not fragile to runtime upgrades?
  • Emergency powers scope: What conditions should trigger emergency shutdown, and who should have the authority to execute it? How can the system balance responsiveness to crises with protection against governance attacks?
  • Treasury integration: Should the Treasury automatically mint PUSD when making a payout? How should the Treasury manage its CDP positions?

Milestones

  1. Core Honzon Pallets into polkadot-sdk — 120h
  • Port Honzon core pallets into polkadot-sdk
  • Update pallets based on PUSD specific requirements
  • Weights/benchmarks + unit tests
  1. PUSD specific pallets - 160h
  • PIB pallet
  • pallet-assets and pallet-asset-conversion integration
  • Incentives pallet
  • Remote oracle pallet
  1. Testnet ready — 120h
  • Westend Asset Hub runtime integration
  • Basic governance configuration
  • End to end tests with WAH runtime
  • Example scripts:
    • auction bidder
    • position health checker
    • stats queryer
  1. Mainnet ready - 120h
  • Polkadot Asset Hub Runtime integration
  • Financial Fellowship
  • Governance origin/track configuration
  • Documentation
  • End to end tests with PAH runtime

Total Estimation: 520 hours

Rate: $250/h
15% slippage + price-volatility margin

50% upfront: 17679 DOT using EMA7 Price 4.22807
Remainder billed based on actual hours after completion

Use cases

There are a number of primary use cases of PUSD:

  • As with any overcollateralized stablecoin, PUSD lets users borrow against their DOT so they can spend PUSD without selling DOT immediately.
  • PUSD is designed to integrate with the Polkadot Treasury so payments can be made in PUSD instead of DOT, avoiding the need for the treasury to manage a stablecoin reserve.
  • In a later phase, staking rewards may be paid in PUSD instead of DOT inflation (details and economics to be finalized by governance).
  • Integration with Proof of Personhood (details TBD).

Protocol description

PUSD is implemented using the Honzon protocol stack used to power aUSD, adapted for DOT‑only collateral on Asset Hub.

Issuances

DOT holders can open a vault (CDP) to lock their DOT and borrow up to a protocol‑defined percentage of its value as PUSD, subject to a required collateral ratio and per‑asset debt ceilings.

Redemption

At any time, the vault owner can repay PUSD (principal plus accrued interest via the debit exchange rate) to unlock DOT, fully or partially.

Liquidation

When a vault’s collateral ratio falls below the liquidation ratio, it becomes unsafe and is liquidated. The system attempts to recover PUSD by selling the DOT in the vault:

  • Sell via on‑chain DEX swap (primary path)
  • Handled Polkadot Issuances Buffer
  • Run collateral auctions (backstop)
  • Future: sell to pre‑registered liquidation smart contracts

Any excess collateral after repaying debt and penalties is refunded to the owner. Shortfalls become bad debt and are handled by CDP Treasury mechanisms.

Polkadot Issuance Buffer (PIB)

The goal of PIB is to improve the stability and reduce market impact during liquidation.
PIB allows the treasury to lock DOT and mint stables to purchase liquidated DOT with a discount price.
There is a quota limit controlled by governance.
Governance may sell purchased PIB to dex or via onchain auction to de-leverage PIB position.

Governance

A Financial Fellowship will govern risk parameters and treasury actions to ensure economic safety, under the broader Polkadot on‑chain governance framework.

Emergency shutdown

As a last resort, an emergency shutdown can be performed to halt minting/liquidation and allow equitable settlement: lock oracle prices, cancel auctions, and let users settle PUSD against collateral at the locked rates.

Protocol Overview

Core Mechanism

The Honzon protocol functions as a lending system where users can:

  1. Deposit Collateral: Lock DOT as collateral in CDPs
  2. Mint PUSD: Generate PUSD stablecoins against collateral value
  3. Accrue Interest: Pay interest over time via the debit exchange rate
  4. Maintain Health: Keep CDPs above the liquidation ratio to avoid liquidation
  5. Liquidation: Underwater CDPs are liquidated via DEX and/or auctions to keep the system solvent

Key Financial Concepts

  • Collateral Ratio (CR): Collateral value / debt value; must stay above required CR; liquidation triggers below the liquidation ratio (LR).
  • Debit Exchange Rate: Grows over time to track accumulated interest on debt positions; debt value = debit units * exchange rate.
  • Liquidation Ratio (LR): Minimum CR below which a CDP is unsafe and can be liquidated.
  • Liquidation Penalty: Additional penalty applied during liquidation to discourage risky leverage.
  • Maximum Total Debit Value: Per‑asset debt ceiling to contain systemic risk.

Architecture

The protocol consists of several interconnected pallets that provide a complete CDP system:

+-----------+     +-------------+     +---------+
|  Honzon   |-----|  CDP Engine |-----|  Loans  |
| (Frontend)|     | (Core Logic)|     |(Positions)
+-----------+     +-------------+     +---------+
      |                   |                   |
      |                   |                   |
+-----------+     +-------------+     +---------+
|  Oracle   |-----| CDP Treasury|-----| Auctions|
|(Prices)   |     |  (Surplus)  |     |(Liquid.)|
+-----------+     +-------------+     +---------+

Position Structure

// From pallet-cdp-engine
pub struct Position<Balance> {
    pub collateral: Balance,
    pub debit: Balance,
}

Risk Management Parameters

// From pallet-cdp-engine
pub struct RiskManagementParams<Balance> {

    /// Liquidation ratio, when the collateral ratio of
    /// CDP under this collateral type is below the liquidation ratio, this
    /// CDP is unsafe and can be liquidated.
    pub liquidation_ratio: Option<Ratio>,

    /// Required collateral ratio, if it's set, cannot adjust the position
    /// of CDP so that the current collateral ratio is lower than the
    /// required collateral ratio.
    pub required_collateral_ratio: Option<Ratio>,
}

Auction Information

// From pallet-auction-manager
pub struct CollateralAuctionItem<AccountId, BlockNumber, Balance> {
    /// Refund recipient for may receive refund
    refund_recipient: AccountId,
    /// Initial collateral amount for sale
    #[codec(compact)]
    initial_amount: Balance,
    /// Current collateral amount for sale
    #[codec(compact)]
    amount: Balance,
    /// Target sales amount of this auction
    /// if zero, collateral auction will never be reverse stage,
    /// otherwise, target amount is the actual payment amount of active
    /// bidder
    #[codec(compact)]
    target: Balance,
    /// Auction start time
    start_time: BlockNumber,
}

Module Documentation

Honzon Module

The main entry point for users to interact with their CDPs.

Calls

/// Adjust the loans of `currency_id` by specific
/// `collateral_adjustment` and `debit_adjustment`
pub fn adjust_loan(
    origin: OriginFor<T>,
    currency_id: <T as pallet_loans::Config>::CurrencyId,
    collateral_adjustment: Amount,
    debit_adjustment: Amount,
) -> DispatchResult

/// Close caller's CDP which has debit but still in safe by use collateral to swap
/// stable token on DEX for clearing debit.
pub fn close_loan_has_debit_by_dex(
    origin: OriginFor<T>,
    currency_id: <T as pallet_loans::Config>::CurrencyId,
    #[pallet::compact] max_collateral_amount: <T as pallet_cdp_engine::Config>::Balance,
) -> DispatchResult

/// Transfer the whole CDP of `from` under `currency_id` to caller's CDP
/// under the same `currency_id`, caller must have the authorization of
/// `from` for the specific collateral type
pub fn transfer_loan_from(
    origin: OriginFor<T>,
    currency_id: <T as pallet_loans::Config>::CurrencyId,
    from: <T::Lookup as StaticLookup>::Source,
) -> DispatchResult

/// Authorize `to` to manipulate the loan under `currency_id`
pub fn authorize(
    origin: OriginFor<T>,
    currency_id: <T as pallet_loans::Config>::CurrencyId,
    to: <T::Lookup as StaticLookup>::Source,
) -> DispatchResult

/// Cancel the authorization for `to` under `currency_id`
pub fn unauthorize(
    origin: OriginFor<T>,
    currency_id: <T as pallet_loans::Config>::CurrencyId,
    to: <T::Lookup as StaticLookup>::Source,
) -> DispatchResult

/// Cancel all authorization of caller
pub fn unauthorize_all(origin: OriginFor<T>) -> DispatchResult

/// Generate new debit in advance, buy collateral and deposit it into CDP.
/// Note: This function is not yet implemented.
pub fn expand_position_collateral(
    origin: OriginFor<T>,
    currency_id: <T as pallet_loans::Config>::CurrencyId,
    increase_debit_value: <T as pallet_cdp_engine::Config>::Balance,
    min_increase_collateral: <T as pallet_cdp_engine::Config>::Balance,
) -> DispatchResult

/// Sell the collateral locked in CDP to get stable coin to repay the debit.
/// Note: This function is not yet implemented.
pub fn shrink_position_debit(
    origin: OriginFor<T>,
    currency_id: <T as pallet_loans::Config>::CurrencyId,
    decrease_collateral: <T as pallet_cdp_engine::Config>::Balance,
    min_decrease_debit_value: <T as pallet_cdp_engine::Config>::Balance,
) -> DispatchResult

/// Adjust the loans of `currency_id` by specific
/// `collateral_adjustment` and `debit_value_adjustment`
/// Note: This function is not yet implemented.
pub fn adjust_loan_by_debit_value(
    origin: OriginFor<T>,
    currency_id: <T as pallet_loans::Config>::CurrencyId,
    collateral_adjustment: Amount,
    debit_value_adjustment: Amount,
) -> DispatchResult

/// Transfers debit between two CDPs
pub fn transfer_debit(
    origin: OriginFor<T>,
    from_currency: <T as pallet_loans::Config>::CurrencyId,
    to_currency: <T as pallet_loans::Config>::CurrencyId,
    debit_transfer: <T as pallet_cdp_engine::Config>::Balance,
) -> DispatchResult

CDP Engine Module

The core module responsible for CDP risk management, liquidation, and interest accumulation.

Calls

/// Update parameters related to risk management of CDP
pub fn set_collateral_params(
    origin: OriginFor<T>,
    liquidation_ratio: Option<Ratio>,
    required_collateral_ratio: Option<Ratio>,
) -> DispatchResult

/// Emergency shutdown the system
pub fn emergency_shutdown(origin: OriginFor<T>) -> DispatchResult

/// Adjust position by adding/removing collateral and debit
pub fn adjust_position(
    origin: OriginFor<T>,
    collateral_adjustment: T::Balance,
    debit_adjustment: T::Balance,
) -> DispatchResult

Loans Module

Manages individual CDP positions and collateral/debit accounting. This pallet has no public extrinsics.

CDP Treasury Module

Manages system surplus, debit, and collateral auctions.

Calls

/// Extract surplus to treasury account
pub fn extract_surplus_to_treasury(origin: OriginFor<T>, #[pallet::compact] amount: T::Balance) -> DispatchResult

/// Auction the collateral not occupied by the auction.
pub fn auction_collateral(
    origin: OriginFor<T>,
    #[pallet::compact] amount: T::Balance,
    #[pallet::compact] target: T::Balance,
    split: bool,
) -> DispatchResultWithPostInfo

/// Update parameters related to collateral auction under specific
/// collateral type
pub fn set_expected_collateral_auction_size(
    origin: OriginFor<T>,
    #[pallet::compact] size: T::Balance,
) -> DispatchResult

/// Update the debit offset buffer
pub fn set_debit_offset_buffer(origin: OriginFor<T>, #[pallet::compact] amount: T::Balance) -> DispatchResult

Auction Manager Module

Handles collateral auctions for liquidated CDPs. This pallet has no public extrinsics.

Oracle Module

Provides oracle price feeds.

Calls

/// Feed the external value.
pub fn feed_values(
    origin: OriginFor<T>,
    values: BoundedVec<(T::OracleKey, T::OracleValue), T::MaxFeedValues>,
) -> DispatchResultWithPostInfo

Issuance Buffer Module

A governance-controlled pallet that provides a protocol-native backstop during liquidations. Governance locks DOT into an internal CDP and configures an issuance quota and a discount factor. During liquidation, the pallet quotes a standing bid at oracle_price * discount and fills up to its remaining PUSD issuance headroom, competing with DEX and auctions. Fills transfer liquidated DOT to the buffer CDP and increase its PUSD debt accordingly, subject to collateralization limits.

Stability fee are not applied to PIB vault as the stability fee goes to treasury and PIB is part of treasury.

Design goals
- Predictable buyer-of-last-resort to smooth liquidation slippage.
- Supply-safe: only mints PUSD when filling a liquidation and accounts it as buffer CDP debt.
- Bounded risk via governance-set quota, discount, per-auction caps, and enable/disable switch.

Calls

/// Fund the buffer by locking DOT as collateral into the buffer CDP.
pub fn fund(
    origin: OriginFor<T>,                         // Governance origin
    #[pallet::compact] amount_dot: T::Balance
) -> DispatchResult

/// Unlock DOT from the buffer CDP.
pub fn defund(
    origin: OriginFor<T>,
    #[pallet::compact] amount_dot: T::Balance
) -> DispatchResult

/// Set the discount factor used to price bids vs oracle. For example,
/// discount = 95% means bid = 0.95 * oracle_price (a 5% discount).
pub fn set_discount(
    origin: OriginFor<T>,
    discount: Permill                              // 1_000_000 == 100%
) -> DispatchResult

/// Set the maximum additional PUSD the buffer may issue (as debt on its CDP).
pub fn set_issuance_quota(
    origin: OriginFor<T>,
    #[pallet::compact] quota_pdd: T::Balance
) -> DispatchResult

Storage

#[pallet::storage]
pub type Discount<T> = StorageValue<_, Permill, ValueQuery>; // default: Permill::from_percent(100)

#[pallet::storage]
pub type IssuanceQuota<T: Config> = StorageValue<_, T::Balance, ValueQuery>; // max PUSD debt

#[pallet::storage]
pub type IssuanceUsed<T: Config> = StorageValue<_, T::Balance, ValueQuery>;  // current PUSD debt

Future Directions

Smart-Contract Liquidation Participation

Future versions of the system will allow smart contracts to register as liquidation participants, enabling:

  • Automated liquidation bots: Smart contracts can participate automatically, providing more reliable liquidation services and potentially better prices for distressed positions.
  • DeFi protocol integration: Other DeFi protocols can integrate directly with the liquidation system, allowing for more sophisticated strategies and cross-protocol arbitrage.
  • Custom logic: Registered smart contracts can implement custom liquidation strategies, such as gradual liquidation over time or cross-chain liquidation via XCM bridges.

Treasuries integration

  • Allow main treasury and sub treasuries (e.g. The Core Fellowship treasury) to issue PUSD directly as part of spend call. So there is no need to keep a PUSD reserve. They are only minted when needed.
  • (Semi) auto way to manage the CDP. e.g. Sell DOT at conditions ( price > $X or collateral ratio < Y% ) to ensure the position is healthy.

Staking integration

  • Similar to treasury integration but integrated with staking system
  • We could have a staking treasury to unify the code

Improve liquidation triggering process

  • Currently offchain worker is used to check position healthy and liquidate unhealthy positions. This requires some trust of collators as it is not easy to know if they are running offchain worker or not. In order to improve this, we could have an onchain mechanism to check position in on_initialize/on_idle hook.
  • We could use something like priority queue to know the most unhealthy positions and only check them upon every oracle update.
Reply
Up
Share
Comments
No comments here