// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.24; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; import {IUSDR, IReserveManager} from "./interfaces/IUSDR.sol"; /// @title PSM — Peg Stability Module (White Paper v3.2 §3, §4.4, S10) /// @notice 1:1 stable onramp: deposit USDT/USDC, mint USDR. Redemption is BEST-EFFORT /// from free slack only — "a 1:1 onramp, not a guaranteed 1:1 exit." /// Winning-trader settlement (the escrow) is never touchable from here. contract PSM is ReentrancyGuard, Pausable { error NotGuardian(); error PauseTooLong(); error ZeroAmount(); uint256 private constant SCALE = 1e12; // 6 dec -> 18 dec uint256 public constant MAX_PAUSE = 14 days; // bounded pause (WP §7) IUSDR public immutable usdr; IReserveManager public immutable reserve; address public immutable guardian; uint256 public pausedUntil; event Minted(address indexed user, address indexed stable, uint256 amount6, uint256 usdrWad); event Redeemed(address indexed user, address indexed stable, uint256 usdrWad, uint256 amount6); constructor(address _usdr, address _reserve, address _guardian) { usdr = IUSDR(_usdr); reserve = IReserveManager(_reserve); guardian = _guardian; } /// @notice Mint USDR 1:1 against USDT/USDC. Stables go straight to the reserve, /// so minting always INCREASES invariant headroom — no solvency check needed. function mint(address stable, uint256 amount6) external nonReentrant whenNotPausedBounded { if (amount6 == 0) revert ZeroAmount(); reserve.depositStable(stable, amount6, msg.sender); uint256 wad = amount6 * SCALE; usdr.mint(msg.sender, wad); emit Minted(msg.sender, stable, amount6, wad); } /// @notice Redeem USDR for stables from FREE SLACK only, first-come (WP §4.4). /// Reverts when slack is exhausted — by design, not by failure. function redeem(address stable, uint256 usdrWad) external nonReentrant whenNotPausedBounded { if (usdrWad == 0) revert ZeroAmount(); // round the payout DOWN and burn the full WAD (protocol-favoring) uint256 amount6 = usdrWad / SCALE; if (amount6 == 0) revert ZeroAmount(); usdr.burn(msg.sender, usdrWad); reserve.redeemFromSlack(stable, amount6 * SCALE, msg.sender); emit Redeemed(msg.sender, stable, usdrWad, amount6); } // -- bounded emergency pause (scope: PSM only; duration-capped) --------------- modifier whenNotPausedBounded() { require(block.timestamp >= pausedUntil, "PSM: paused"); _; } function pause(uint256 duration) external { if (msg.sender != guardian) revert NotGuardian(); if (duration > MAX_PAUSE) revert PauseTooLong(); pausedUntil = block.timestamp + duration; } function unpause() external { if (msg.sender != guardian) revert NotGuardian(); pausedUntil = block.timestamp; } }