// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "../../../reserve/IReserveSystem.sol"; import "./IISOCurrencyManager.sol"; /** * @title ISOCurrencyManager * @notice Manages all ISO-4217 currencies with XAU triangulation support * @dev All currency conversions go through XAU: CurrencyA → XAU → CurrencyB */ contract ISOCurrencyManager is IISOCurrencyManager, Ownable, ReentrancyGuard { IReserveSystem public immutable reserveSystem; // XAU address (gold) - base anchor for all triangulations address public xauAddress; struct Currency { string currencyCode; // ISO-4217 code (USD, EUR, GBP, etc.) address tokenAddress; // Token contract address (if tokenized) uint256 xauRate; // Rate: 1 oz XAU = xauRate units of currency (in 18 decimals) bool isActive; bool isTokenized; // Whether currency has on-chain token representation } mapping(string => Currency) public currencies; string[] public supportedCurrencies; // Example rates (in 18 decimals): // USD: 1 oz XAU = 2000 USD, so xauRate = 2000e18 // EUR: 1 oz XAU = 1800 EUR, so xauRate = 1800e18 event CurrencyRegistered( string indexed currencyCode, address tokenAddress, uint256 xauRate, bool isTokenized ); event CurrencyConverted( string fromCurrency, string toCurrency, uint256 fromAmount, uint256 toAmount ); error ZeroAddress(); error CurrencyNotRegistered(); error InvalidXauRate(); error XauNotSet(); error InvalidCurrencyCode(); /** * @notice Constructor * @param _reserveSystem ReserveSystem contract address */ constructor(address _reserveSystem) Ownable(msg.sender) { if (_reserveSystem == address(0)) revert ZeroAddress(); reserveSystem = IReserveSystem(_reserveSystem); } /** * @notice Set XAU (gold) address * @param _xauAddress XAU token address */ function setXAUAddress(address _xauAddress) external onlyOwner { if (_xauAddress == address(0)) revert ZeroAddress(); xauAddress = _xauAddress; } /** * @notice Register ISO-4217 currency * @param currencyCode ISO-4217 currency code (USD, EUR, GBP, JPY, etc.) * @param tokenAddress Token contract address (address(0) if not tokenized) * @param xauRate Rate: 1 oz XAU = xauRate units of currency (in 18 decimals) * @return success Whether registration was successful */ function registerCurrency( string memory currencyCode, address tokenAddress, uint256 xauRate ) external override onlyOwner returns (bool) { if (bytes(currencyCode).length == 0) revert InvalidCurrencyCode(); if (xauRate == 0) revert InvalidXauRate(); if (xauAddress == address(0)) revert XauNotSet(); bool isTokenized = tokenAddress != address(0); currencies[currencyCode] = Currency({ currencyCode: currencyCode, tokenAddress: tokenAddress, xauRate: xauRate, isActive: true, isTokenized: isTokenized }); // Add to supported currencies if not already present bool alreadyAdded = false; for (uint256 i = 0; i < supportedCurrencies.length; i++) { if (keccak256(bytes(supportedCurrencies[i])) == keccak256(bytes(currencyCode))) { alreadyAdded = true; break; } } if (!alreadyAdded) { supportedCurrencies.push(currencyCode); } emit CurrencyRegistered(currencyCode, tokenAddress, xauRate, isTokenized); return true; } /** * @notice Convert between currencies via XAU triangulation * @param fromCurrency Source currency code * @param toCurrency Target currency code * @param amount Amount to convert * @return targetAmount Amount in target currency */ function convertViaXAU( string memory fromCurrency, string memory toCurrency, uint256 amount ) external view override returns (uint256 targetAmount) { Currency memory from = currencies[fromCurrency]; Currency memory to = currencies[toCurrency]; if (bytes(from.currencyCode).length == 0) revert CurrencyNotRegistered(); if (bytes(to.currencyCode).length == 0) revert CurrencyNotRegistered(); if (xauAddress == address(0)) revert XauNotSet(); // Step 1: Convert fromCurrency to XAU // amount / from.xauRate = XAU amount uint256 xauAmount = (amount * 1e18) / from.xauRate; // Step 2: Convert XAU to toCurrency // xauAmount * to.xauRate / 1e18 = targetAmount targetAmount = (xauAmount * to.xauRate) / 1e18; return targetAmount; } /** * @notice Get exchange rate for currency pair * @param fromCurrency Source currency code * @param toCurrency Target currency code * @return rate Exchange rate (toCurrency per fromCurrency, in 18 decimals) */ function getCurrencyRate( string memory fromCurrency, string memory toCurrency ) external view override returns (uint256 rate) { Currency memory from = currencies[fromCurrency]; Currency memory to = currencies[toCurrency]; if (bytes(from.currencyCode).length == 0) revert CurrencyNotRegistered(); if (bytes(to.currencyCode).length == 0) revert CurrencyNotRegistered(); // Rate = (to.xauRate / from.xauRate) * 1e18 // This gives: 1 fromCurrency = rate toCurrency rate = (to.xauRate * 1e18) / from.xauRate; return rate; } /** * @notice Get all supported currencies * @return Array of currency codes */ function getAllSupportedCurrencies() external view override returns (string[] memory) { return supportedCurrencies; } /** * @notice Get token address for a currency code * @param currencyCode ISO-4217 currency code * @return Token address (address(0) if not tokenized) */ function getCurrencyAddress( string memory currencyCode ) external view override returns (address) { Currency memory currency = currencies[currencyCode]; if (bytes(currency.currencyCode).length == 0) revert CurrencyNotRegistered(); return currency.tokenAddress; } /** * @notice Update XAU rate for a currency * @param currencyCode ISO-4217 currency code * @param newXauRate New XAU rate */ function updateXauRate(string memory currencyCode, uint256 newXauRate) external onlyOwner { Currency storage currency = currencies[currencyCode]; if (bytes(currency.currencyCode).length == 0) revert CurrencyNotRegistered(); if (newXauRate == 0) revert InvalidXauRate(); currency.xauRate = newXauRate; } /** * @notice Get currency info * @param currencyCode ISO-4217 currency code * @return tokenAddress Token address * @return xauRate XAU rate * @return isActive Whether currency is active * @return isTokenized Whether currency is tokenized */ function getCurrencyInfo( string memory currencyCode ) external view returns ( address tokenAddress, uint256 xauRate, bool isActive, bool isTokenized ) { Currency memory currency = currencies[currencyCode]; if (bytes(currency.currencyCode).length == 0) revert CurrencyNotRegistered(); return ( currency.tokenAddress, currency.xauRate, currency.isActive, currency.isTokenized ); } /** * @notice Batch register currencies * @param currencyCodes Array of currency codes * @param tokenAddresses Array of token addresses * @param xauRates Array of XAU rates */ function batchRegisterCurrencies( string[] memory currencyCodes, address[] memory tokenAddresses, uint256[] memory xauRates ) external onlyOwner { require( currencyCodes.length == tokenAddresses.length && currencyCodes.length == xauRates.length, "ISOCurrencyManager: length mismatch" ); for (uint256 i = 0; i < currencyCodes.length; i++) { // Call internal registration logic directly bool isTokenized = tokenAddresses[i] != address(0); currencies[currencyCodes[i]] = Currency({ currencyCode: currencyCodes[i], tokenAddress: tokenAddresses[i], xauRate: xauRates[i], isActive: true, isTokenized: isTokenized }); // Add to supported currencies if not already present bool alreadyAdded = false; for (uint256 j = 0; j < supportedCurrencies.length; j++) { if (keccak256(bytes(supportedCurrencies[j])) == keccak256(bytes(currencyCodes[i]))) { alreadyAdded = true; break; } } if (!alreadyAdded) { supportedCurrencies.push(currencyCodes[i]); } emit CurrencyRegistered(currencyCodes[i], tokenAddresses[i], xauRates[i], isTokenized); } } }