|
| 1 | +// SPDX-License-Identifier: LGPL-3.0-only |
| 2 | +pragma solidity 0.8.28; |
| 3 | + |
| 4 | +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; |
| 5 | +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; |
| 6 | +import {IOracle} from "./interfaces/IOracle.sol"; |
| 7 | + |
| 8 | +/// @title Oracle valuing Paxos-issued stablecoins (USDG, PYUSD) at a hardcoded 1:1 ratio to USDC. |
| 9 | +/// @author Sprinter |
| 10 | +/// @notice Implements {IOracle} for an admin-managed set of Paxos stablecoins, each pegged exactly 1:1 to USDC. |
| 11 | +/// `getAssetValue` returns the USDC-denominated value of a given amount, adjusting only for any |
| 12 | +/// decimal mismatch between the stablecoin and USDC. The price ratio is immutable at 1:1. |
| 13 | +/// @dev `assetId` is the token address left-padded to bytes32 to allow non evm tokens in the future |
| 14 | +contract PaxosOracle is IOracle, AccessControl { |
| 15 | + struct AssetConfig { |
| 16 | + bool supported; |
| 17 | + uint8 decimals; |
| 18 | + } |
| 19 | + |
| 20 | + struct AssetParams { |
| 21 | + bytes32 assetId; |
| 22 | + uint8 decimals; |
| 23 | + } |
| 24 | + |
| 25 | + /// @notice USDC token used as the value denomination. |
| 26 | + IERC20Metadata public immutable USDC; |
| 27 | + /// @notice Decimals of USDC, fetched once at deployment and used to scale returned values. |
| 28 | + uint8 public immutable USDC_DECIMALS; |
| 29 | + |
| 30 | + uint8 internal constant MAX_DECIMALS = 18; |
| 31 | + |
| 32 | + mapping(bytes32 assetId => AssetConfig) public assetConfig; |
| 33 | + |
| 34 | + event AssetAdded(bytes32 indexed assetId, uint8 decimals); |
| 35 | + event AssetRemoved(bytes32 indexed assetId); |
| 36 | + |
| 37 | + error ZeroAddress(); |
| 38 | + error AssetAlreadySupported(bytes32 assetId); |
| 39 | + error AssetNotSupported(bytes32 assetId); |
| 40 | + error DecimalsTooLarge(uint8 decimals); |
| 41 | + |
| 42 | + /// @param admin Super-admin able to add/remove supported stablecoins. |
| 43 | + /// @param usdc Address of the USDC token, used as the value denomination (its decimals). |
| 44 | + /// @param initialAssets Initial set of stablecoins to support (e.g. USDG, PYUSD). |
| 45 | + constructor(address admin, address usdc, AssetParams[] memory initialAssets) { |
| 46 | + require(admin != address(0), ZeroAddress()); |
| 47 | + require(usdc != address(0), ZeroAddress()); |
| 48 | + _grantRole(DEFAULT_ADMIN_ROLE, admin); |
| 49 | + USDC = IERC20Metadata(usdc); |
| 50 | + USDC_DECIMALS = IERC20Metadata(usdc).decimals(); |
| 51 | + for (uint256 i = 0; i < initialAssets.length; ++i) { |
| 52 | + _addAsset(initialAssets[i].assetId, initialAssets[i].decimals); |
| 53 | + } |
| 54 | + } |
| 55 | + |
| 56 | + /// @notice Registers a stablecoin to be valued 1:1 against USDC. |
| 57 | + /// @param assetId The asset identifier (an EVM token address left-padded to bytes32). |
| 58 | + /// @param decimals The number of decimals the stablecoin uses. Provided explicitly so that |
| 59 | + /// non-EVM assets, whose `decimals()` cannot be queried on-chain, can also be supported. |
| 60 | + function addAsset(bytes32 assetId, uint8 decimals) external onlyRole(DEFAULT_ADMIN_ROLE) { |
| 61 | + _addAsset(assetId, decimals); |
| 62 | + } |
| 63 | + |
| 64 | + /// @notice Removes a previously registered stablecoin. |
| 65 | + /// @param assetId The asset identifier to remove. |
| 66 | + function removeAsset(bytes32 assetId) external onlyRole(DEFAULT_ADMIN_ROLE) { |
| 67 | + require(assetConfig[assetId].supported, AssetNotSupported(assetId)); |
| 68 | + delete assetConfig[assetId]; |
| 69 | + emit AssetRemoved(assetId); |
| 70 | + } |
| 71 | + |
| 72 | + /// @notice Returns the USDC-denominated value of `amount` units of the supported stablecoin `assetId`. |
| 73 | + /// @dev Reverts for any asset that is not currently supported. |
| 74 | + function getAssetValue(bytes32 assetId, uint256 amount) external view returns (uint256) { |
| 75 | + AssetConfig memory config = assetConfig[assetId]; |
| 76 | + require(config.supported, AssetNotSupported(assetId)); |
| 77 | + if (config.decimals == USDC_DECIMALS) { |
| 78 | + return amount; |
| 79 | + } |
| 80 | + if (config.decimals > USDC_DECIMALS) { |
| 81 | + return amount / (10 ** (config.decimals - USDC_DECIMALS)); |
| 82 | + } |
| 83 | + return amount * (10 ** (USDC_DECIMALS - config.decimals)); |
| 84 | + } |
| 85 | + |
| 86 | + /// @notice Returns true if `assetId` is currently valued by this oracle. |
| 87 | + function isSupported(bytes32 assetId) external view returns (bool) { |
| 88 | + return assetConfig[assetId].supported; |
| 89 | + } |
| 90 | + |
| 91 | + function _addAsset(bytes32 assetId, uint8 decimals) internal { |
| 92 | + require(!assetConfig[assetId].supported, AssetAlreadySupported(assetId)); |
| 93 | + require(decimals <= MAX_DECIMALS, DecimalsTooLarge(decimals)); |
| 94 | + assetConfig[assetId] = AssetConfig({supported: true, decimals: decimals}); |
| 95 | + emit AssetAdded(assetId, decimals); |
| 96 | + } |
| 97 | +} |
0 commit comments