Development of PSM (Peg Stability Module)
MakerDAO introduced PSM in 2020 as an answer to DAI instability. Idea is straightforward: allow 1 DAI ↔ 1 USDC swap with minimal fee, creating built-in arbitrage mechanism. When DAI trades above $1.00 — arbitrageurs deposit USDC, get DAI, sell on market. When below $1.00 — buy DAI, return to PSM, get USDC. Peg restores via market pressure without team involvement. For new stablecoin, PSM is not optional module, it is critical infrastructure — without it, depegging becomes "when", not "if".
Math of Stabilization and Failure Points
How PSM Arbitrage Works
Suppose your stablecoin XUSD trades at $1.02 on DEX. PSM offers: bring $1 in USDC, get 1 XUSD (minus tin — entry fee, e.g., 0.1%). Arbitrageur deposits 1000 USDC, gets 999 XUSD, sells on Uniswap at $1.02, receives $1019.98. Profit $19.98 minus gas. After enough such trades, XUSD supply on market grows, price drops to $1.00.
Reverse: XUSD = $0.98. PSM: bring 1 XUSD, get $1 USDC (minus tout — exit fee). Arbitrageur buys 1000 XUSD at $980 on Uniswap, brings to PSM, gets $999 USDC. Profit $19 minus gas. XUSD bought from market, price rises.
Key parameter: PSM debt ceiling. Maximum amount tradeable via module. Without cap PSM can accumulate 100% reserves in one asset (e.g., USDC) — concentrates regulatory risk (Circle can freeze USDC). MakerDAO at peak held >50% DAI reserves in USDC via PSM, prompting parameter review after 2022–2023 regulatory threats.
Attack via Unbalanced PSM
If tin/tout set too low (<0.01%), PSM becomes free arbitrage tool, draining reserves on any market shock. At XUSD = $0.995 arbitrage profitable at $50K volume — MEV bots kick in automatically.
Optimal tin/tout range for new stablecoin on start: 0.1–0.5%. As liquidity grows — reduce to 0.01–0.1%. These are configurable parameters governance can change via timelock.
PSM Contract Architecture
Core Module Functions
// Entry: user deposits collateral, receives stablecoin
function sellGem(address usr, uint256 gemAmt) external {
uint256 gemAmt18 = gemAmt * (10 ** (18 - dec)); // decimals normalization
uint256 daiAmt = gemAmt18; // 1:1 before fee
uint256 fee = daiAmt * tin / WAD; // tin in WAD (1e18)
require(dai.balanceOf(address(this)) >= daiAmt - fee, "PSM/insufficient-dai");
vow.bump(fee); // fee to treasury
gem.transferFrom(msg.sender, address(this), gemAmt);
dai.transfer(usr, daiAmt - fee);
emit SellGem(usr, gemAmt, fee);
}
// Exit: user deposits stablecoin, receives collateral
function buyGem(address usr, uint256 gemAmt) external {
uint256 gemAmt18 = gemAmt * (10 ** (18 - dec));
uint256 daiAmt = gemAmt18;
uint256 fee = daiAmt * tout / WAD;
require(gem.balanceOf(address(this)) >= gemAmt, "PSM/insufficient-gem");
dai.transferFrom(msg.sender, address(this), daiAmt + fee);
vow.bump(fee);
gem.transfer(usr, gemAmt);
emit BuyGem(usr, gemAmt, fee);
}
Critical moment: decimals normalization. USDC has 6 decimals, most stablecoins — 18. Without gemAmt * (10 ** (18 - dec)) PSM math breaks — user gets 10^12 times fewer tokens. Not hypothetical: even experienced developers make decimal mistakes with non-standard ERC-20s.
Collateral Whitelist and Multi-asset PSM
Basic PSM works with one collateral. Extended version supports multiple: USDC, USDT, DAI — each with own tin/tout and debt ceiling.
Adding new collateral via governance: addGem(address gemAddress, uint256 ceiling) with 48-hour timelock minimum. Prevents adding malicious ERC-20 with custom transferFrom draining PSM via reentrancy.
Rate Limiter Against Flash Loan Attacks
Flash loan attack on PSM: borrow 10M USDC, penetrate PSM via sellGem, get 10M XUSD, sell on market, create artificial price pressure, buy XUSD back cheaper, return to PSM, repay flash loan with profit. To neutralize — RateLimiter: maximum operation volume in PSM per N blocks.
mapping(uint256 => uint256) public volumePerBlock;
uint256 public constant MAX_VOLUME_PER_WINDOW = 1_000_000e18; // 1M stablecoins
uint256 public constant WINDOW_BLOCKS = 50; // ~10 minutes
function _checkRateLimit(uint256 amount) internal {
uint256 windowStart = block.number - (block.number % WINDOW_BLOCKS);
volumePerBlock[windowStart] += amount;
require(volumePerBlock[windowStart] <= MAX_VOLUME_PER_WINDOW, "PSM/rate-limit");
}
Governance and Parameter Management
PSM without governance — static tool. PSM with governance — live market mechanism. Parameters in governance:
| Parameter | Description | Recommended Timelock |
|---|---|---|
tin |
Fee on entry | 24 hours |
tout |
Fee on exit | 24 hours |
line (debt ceiling) |
Max volume | 48 hours |
addGem |
New collateral | 72 hours |
pause |
PSM pause | 0 (emergency) |
For new protocols we recommend Gnosis Safe + OpenZeppelin's TimelockController. Pause is only operation without timelock, but requires multisig (minimum 3-of-5).
Development Process
Analytics and Modeling (2–3 days). Python script models PSM behavior at different volatility: peg deviation 1%, 3%, 5%, flash crash. Find optimal tin/tout and debt ceiling. Analyze target stablecoin liquidity on DEX — determines PSM minimum size.
Development (4–6 days). Core PSM contract, RateLimiter, multi-asset support if needed, governance integration. Foundry tests: unit tests for all functions, fuzz tests on decimal edge values and amounts, fork tests with real USDC/USDT.
Audit (2–3 days). Slither, manual review of decimals handling and overflow/underflow (critical for Solidity < 0.8.0; in 0.8+ SafeMath built-in, but custom unchecked blocks need attention).
Deployment (1–2 days). Testnet first, mainnet via multisig with verification.
Basic PSM with one collateral — 1–1.5 weeks. Multi-asset with governance and dashboard — 2–3 weeks. Cost calculated individually after tokenomics analysis.







