// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.24; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; /// @title USDR — Rain Dollar /// @notice Single fungible, basket-backed stablecoin (White Paper v3.2 §3). /// @dev No transfer fee, no hooks, no admin-mint. Minters (PSM, CDPVault) are set /// once at deployment and then finalized FOREVER. There is no owner, no upgrade /// path, and no code path that can extend the minter set after finalization /// (WP §10: "No admin-mint ... nor can a key thief"). contract USDR is ERC20Permit { // --------------------------------------------------------------------- // Errors // --------------------------------------------------------------------- error NotMinter(); error NotDeployer(); error AlreadyFinalized(); error ZeroAddress(); // --------------------------------------------------------------------- // Storage // --------------------------------------------------------------------- address public immutable deployer; bool public mintersFinalized; mapping(address => bool) public isMinter; event MinterAdded(address indexed minter); event MintersFinalized(); constructor() ERC20("Rain Dollar", "USDR") ERC20Permit("Rain Dollar") { deployer = msg.sender; } /// @notice Add a minter (PSM / CDPVault). Only before finalization. function addMinter(address minter) external { if (msg.sender != deployer) revert NotDeployer(); if (mintersFinalized) revert AlreadyFinalized(); if (minter == address(0)) revert ZeroAddress(); isMinter[minter] = true; emit MinterAdded(minter); } /// @notice Permanently lock the minter set. Irreversible. function finalizeMinters() external { if (msg.sender != deployer) revert NotDeployer(); if (mintersFinalized) revert AlreadyFinalized(); mintersFinalized = true; emit MintersFinalized(); } function mint(address to, uint256 amount) external { if (!isMinter[msg.sender]) revert NotMinter(); _mint(to, amount); } function burn(address from, uint256 amount) external { if (!isMinter[msg.sender]) revert NotMinter(); _burn(from, amount); } }