// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import {Initializable} from "@openzeppelin-contracts-upgradeable-5.6.1/proxy/utils/Initializable.sol"; import {AccessControlUpgradeable} from "@openzeppelin-contracts-upgradeable-5.6.1/access/AccessControlUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin-contracts-upgradeable-5.6.1/proxy/utils/UUPSUpgradeable.sol"; import {IFeeRegistry} from "./interfaces/IFeeRegistry.sol"; import {Roles} from "./libraries/Roles.sol"; /// @title FeeRegistry /// @notice Per-token protocol fee parameters and the treasury address. /// Tokens without an explicit configuration fall back to protocol-wide default fees, so a /// loan on an unlisted token is never silently zero-fee. Read at execution: /// `originationFeeBps` funds the one-time borrower fee, `interestTakeRateBps` is snapshotted /// immutably into each loan so later fee changes never affect live loans. /// @dev UUPS-upgradeable singleton. Fees are bounded below 100% by hard caps so the admin cannot /// brick or over-tax loans. Storage uses ERC-7201 namespacing; upgrades are gated by /// `UPGRADER_ROLE` (a Timelock in production, distinct from the DEFAULT_ADMIN/FEE_MANAGER). contract FeeRegistry is IFeeRegistry, Initializable, AccessControlUpgradeable, UUPSUpgradeable { struct FeeParams { uint16 originationFeeBps; uint16 interestTakeRateBps; bool isSet; // distinguishes an explicit (possibly zero) config from "use defaults" } /// @custom:storage-location erc7201:amish.storage.FeeRegistry struct FeeRegistryStorage { address treasury; FeeParams defaultFees; mapping(address token => FeeParams) tokenFees; } // keccak256(abi.encode(uint256(keccak256("amish.storage.FeeRegistry")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant STORAGE_LOCATION = 0xe835dd83c0c4483e40df3e0c501de3df67296b68fba8b61750e37b4b86025000; /// @notice Upper bound on the origination fee (10%). Defensive cap on admin power. uint16 public constant MAX_ORIGINATION_FEE_BPS = 1_000; /// @notice Upper bound on the interest take rate (50%). Defensive cap on admin power. uint16 public constant MAX_INTEREST_TAKE_BPS = 5_000; event TokenFeesUpdated(address indexed token, uint16 originationFeeBps, uint16 interestTakeRateBps); event DefaultFeesUpdated(uint16 originationFeeBps, uint16 interestTakeRateBps); event TreasuryUpdated(address indexed treasury); error FeeRegistry_OriginationFeeTooHigh(uint16 bps); error FeeRegistry_InterestTakeTooHigh(uint16 bps); error FeeRegistry_ZeroAddress(); constructor() { _disableInitializers(); } /// @param admin Holds DEFAULT_ADMIN_ROLE and FEE_MANAGER_ROLE. /// @param upgrader Holds UPGRADER_ROLE (a Timelock in production). /// @param initialTreasury Recipient of protocol fees. /// @param defaultOriginationFeeBps Fallback origination fee for unconfigured tokens. /// @param defaultInterestTakeRateBps Fallback interest take for unconfigured tokens. function initialize( address admin, address upgrader, address initialTreasury, uint16 defaultOriginationFeeBps, uint16 defaultInterestTakeRateBps ) external initializer { if (admin == address(0) || upgrader == address(0)) { revert FeeRegistry_ZeroAddress(); } __AccessControl_init(); _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(Roles.FEE_MANAGER_ROLE, admin); _grantRole(Roles.UPGRADER_ROLE, upgrader); _setTreasury(initialTreasury); _setDefaultFees(defaultOriginationFeeBps, defaultInterestTakeRateBps); } /// @notice Set the fee parameters for a specific token (overrides the defaults for it). function setTokenFees(address token, uint16 originationFeeBps, uint16 interestTakeRateBps) external onlyRole(Roles.FEE_MANAGER_ROLE) { _validateFees(originationFeeBps, interestTakeRateBps); _getStorage().tokenFees[token] = FeeParams(originationFeeBps, interestTakeRateBps, true); emit TokenFeesUpdated(token, originationFeeBps, interestTakeRateBps); } /// @notice Set the protocol-wide default fees applied to tokens with no explicit config. function setDefaultFees(uint16 originationFeeBps, uint16 interestTakeRateBps) external onlyRole(Roles.FEE_MANAGER_ROLE) { _setDefaultFees(originationFeeBps, interestTakeRateBps); } /// @notice Update the protocol treasury address. function setTreasury(address newTreasury) external onlyRole(Roles.FEE_MANAGER_ROLE) { _setTreasury(newTreasury); } /// @inheritdoc IFeeRegistry function feesFor(address token) external view override returns (uint16 originationFeeBps, uint16 interestTakeRateBps) { FeeRegistryStorage storage $ = _getStorage(); FeeParams memory p = $.tokenFees[token]; if (!p.isSet) p = $.defaultFees; return (p.originationFeeBps, p.interestTakeRateBps); } /// @notice The current default (fallback) fees. function defaultFees() external view returns (uint16 originationFeeBps, uint16 interestTakeRateBps) { FeeParams memory p = _getStorage().defaultFees; return (p.originationFeeBps, p.interestTakeRateBps); } /// @inheritdoc IFeeRegistry function treasury() external view override returns (address) { return _getStorage().treasury; } function _setDefaultFees(uint16 originationFeeBps, uint16 interestTakeRateBps) private { _validateFees(originationFeeBps, interestTakeRateBps); _getStorage().defaultFees = FeeParams(originationFeeBps, interestTakeRateBps, true); emit DefaultFeesUpdated(originationFeeBps, interestTakeRateBps); } function _setTreasury(address newTreasury) private { if (newTreasury == address(0)) revert FeeRegistry_ZeroAddress(); _getStorage().treasury = newTreasury; emit TreasuryUpdated(newTreasury); } function _validateFees(uint16 originationFeeBps, uint16 interestTakeRateBps) private pure { if (originationFeeBps > MAX_ORIGINATION_FEE_BPS) revert FeeRegistry_OriginationFeeTooHigh(originationFeeBps); if (interestTakeRateBps > MAX_INTEREST_TAKE_BPS) revert FeeRegistry_InterestTakeTooHigh(interestTakeRateBps); } function _authorizeUpgrade(address) internal override onlyRole(Roles.UPGRADER_ROLE) {} function _getStorage() private pure returns (FeeRegistryStorage storage $) { assembly { $.slot := STORAGE_LOCATION } } }