Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

How the LitePSM Works

Architecture

A USDS ↔ USDC swap through the deployed wrapper flows through the USDS wrapper, the core DAI LitePSM, and the pocket. The wrapper also uses Maker's DAI and USDS join adapters to translate the DAI-denominated core route into a USDS-denominated interface:

caller ──► UsdsPsmWrapper ──► DssLitePsm (LITE-PSM-USDC-A) ──► Pocket
              │                         ▲
              ├──── DaiJoin ────────────┤
              └──── UsdsJoin ───────────┘

The core DssLitePsm is DAI-native: it swaps USDC against a pre-minted DAI balance. To present a USDS interface, the UsdsPsmWrapper wraps the core PSM and uses DaiJoin and UsdsJoin to move between DAI, USDS, and Vat internal balances. USDC custody lives in the Pocket, a separate address whose balance is the maximum USDC available for buy-side liquidity.

The "lite" in LitePSM refers to a design choice: the original Maker DSS PSM touched the Vat on every swap, which is expensive in gas. The LitePSM holds pre-minted DAI inside the core PSM contract, so core user swaps reduce to ERC-20 transfers plus small accounting around fees. Periodic permissionless bookkeeping calls (described below) keep the system aligned with its target accounting state.

Pre-Minted DAI Buffer

The core PSM has a buf parameter, currently 400_000_000e18, representing the target amount of unused pre-minted DAI the system is designed to keep available in most situations. buf is not the same thing as the exact current DAI balance, and fill() / trim() do not simply compare dai.balanceOf(psm) to buf.

The core contract computes its target debt as:

targetArt = gem.balanceOf(pocket) * to18ConversionFactor + buf

Two permissionless functions use this target together with the local and global debt ceilings:

  • fill() — mints rush() DAI from the Maker Vat into the core PSM when additional DAI can be generated under the target and debt-ceiling constraints.
  • trim() — burns gush() DAI and repays Vat debt when the core PSM has excess debt or needs to reduce debt against the target.

Both functions are typically called by automated keeper jobs and are not part of the normal swap path. From an integrator's perspective, the sell-side capacity for USDC → USDS is the actual external DAI balance of the core PSM:

dai.balanceOf(address(wrapper.psm()))

That balance can be below, equal to, or above buf. For large routes, read the live DAI balance and account for same-block fill(), trim(), chug(), and competing swaps.

The Pocket

USDC is not held in the LitePSM contract itself. It is held in a separate Pocket address (0x37305B...D7341), which has granted the core DssLitePsm a max allowance to move USDC during buyGem calls. This decoupling has two consequences:

  1. Buy-side liquidity is exactly gem.balanceOf(pocket). Integrators should read this directly when sizing routes.
  2. The pocket address is immutable in the deployed core PSM. The current PSM cannot reassign pocket through a file call; changing that address would require deploying or migrating to a different PSM/wrapper setup.

The Pocket is a Coinbase Custody account with a private key that was burned with a heavily witnessed key generation and burning ceremony. The idea is that the only transaction made on this Custody account was an infinite approval to the PSM contract, which allows adding and removing funds. After that transaction was complete, both Coinbase's and Sky's keys were permanently destroyed.

Fees

The PSM exposes two fee parameters in WAD precision (where 1e18 = 100%):

  • tin — fee applied when a user sells USDC for DAI/USDS.
  • tout — fee applied when a user buys USDC with DAI/USDS.

Both are currently 0. Accrued fees stay in the contract as DAI and are swept to the Maker Vow (surplus buffer) by a permissionless chug() call.

Halts and Failure Modes

  • Direction halt. Sky governance, or the DssLitePsmMom emergency contract, can set tin or tout to type(uint256).max (the HALTED sentinel). When set, the corresponding direction reverts. Read tin() and tout() before quoting and reject if either equals type(uint256).max.
  • Pocket depletion. If gem.balanceOf(pocket) is below the requested gemAmt, buyGem reverts. Check pocket balance before routing buy-side liquidity.
  • USDC blacklisting. USDC has an admin-controlled blacklist. If Circle blacklists the pocket or the wrapper, swaps in the affected direction revert at the USDC transfer step.
  • Debt ceiling exhaustion. If the Maker Vat debt ceiling for the LitePSM ilk is exhausted, fill() reverts and sell-side liquidity stops replenishing. Swaps continue to work against the existing buffer until it is drained.
  • Vat cage / global settlement assumptions. live() is a compatibility getter that returns vat.live(). The swap functions do not check this flag directly, and the ChainSecurity audit notes that DssLitePsm does not support coordinated Global Settlement. Treat any Vat cage or settlement state as unsupported for normal integrations.

Decimals

USDC has 6 decimals. USDS and DAI have 18 decimals. The PSM uses a constant to18ConversionFactor = 10^12 to translate between them.

Both sellGem and buyGem take gemAmt in USDC decimals (6). The functions return DAI/USDS amounts in WAD (18 decimals).

Worked example — selling 1 USDC for USDS with tin = 0:

gemAmt    = 1_000_000                (1 USDC, 6 decimals)
usdsOut   = gemAmt * 10^12           (scale to WAD)
          = 1_000_000_000_000_000_000  (1 USDS, 18 decimals)

With a non-zero tin, the output is reduced proportionally:

usdsOut = gemAmt * 10^12 * (WAD - tin) / WAD

For buyGem, the input USDS amount is increased by tout:

usdsIn  = gemAmt * 10^12 * (WAD + tout) / WAD