// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "./IReserveSystem.sol"; /** * @title ReserveSystem * @notice Core implementation of the GRU Reserve System * @dev Manages reserves in multiple asset classes (XAU, digital assets, sovereign instruments) * Implements XAU triangulation conversion algorithm and redemption mechanisms */ contract ReserveSystem is IReserveSystem, AccessControl, ReentrancyGuard { using SafeERC20 for IERC20; // ============ Constants ============ bytes32 public constant RESERVE_MANAGER_ROLE = keccak256("RESERVE_MANAGER_ROLE"); bytes32 public constant PRICE_FEED_ROLE = keccak256("PRICE_FEED_ROLE"); bytes32 public constant CONVERSION_OPERATOR_ROLE = keccak256("CONVERSION_OPERATOR_ROLE"); // Conversion fee parameters (basis points: 10000 = 100%) uint256 public constant BASE_FEE_BPS = 10; // 0.1% uint256 public constant SLIPPAGE_FEE_BPS = 50; // 0.5% per 1% slippage uint256 public constant LARGE_TRANSACTION_THRESHOLD = 1_000_000 * 1e18; // $1M uint256 public constant LARGE_TRANSACTION_FEE_BPS = 5; // 0.05% // Price staleness threshold (30 seconds) uint256 public constant PRICE_STALENESS_THRESHOLD = 30; // Maximum slippage (0.5% for liquid, 1.0% for less liquid) uint256 public constant MAX_SLIPPAGE_BPS = 50; // 0.5% // ============ Storage ============ struct Reserve { address asset; uint256 balance; uint256 lastUpdated; } struct PriceFeed { uint256 price; uint256 timestamp; bool isValid; } struct Conversion { address sourceAsset; address targetAsset; uint256 sourceAmount; uint256 targetAmount; uint256 fees; address[] path; uint256 timestamp; } // Reserve tracking mapping(address => uint256) public reserveBalances; mapping(bytes32 => Reserve) public reserves; bytes32[] public reserveIds; // Price feeds mapping(address => PriceFeed) public priceFeeds; address[] public supportedAssets; // Conversion tracking mapping(bytes32 => Conversion) public conversions; bytes32[] public conversionIds; // Asset metadata mapping(address => bool) public isSupportedAsset; mapping(address => bool) public isLiquidAsset; // For slippage calculation // ============ Constructor ============ constructor(address admin) { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(RESERVE_MANAGER_ROLE, admin); _grantRole(PRICE_FEED_ROLE, admin); _grantRole(CONVERSION_OPERATOR_ROLE, admin); } // ============ Reserve Management ============ function depositReserve( address asset, uint256 amount ) external override onlyRole(RESERVE_MANAGER_ROLE) nonReentrant returns (bytes32 reserveId) { require(asset != address(0), "ReserveSystem: zero address"); require(amount > 0, "ReserveSystem: zero amount"); require(isSupportedAsset[asset], "ReserveSystem: unsupported asset"); IERC20(asset).safeTransferFrom(msg.sender, address(this), amount); reserveId = keccak256(abi.encodePacked(asset, amount, block.timestamp, reserveIds.length)); reserves[reserveId] = Reserve({ asset: asset, balance: amount, lastUpdated: block.timestamp }); reserveIds.push(reserveId); reserveBalances[asset] += amount; emit ReserveDeposited(asset, amount, msg.sender, reserveId); } function withdrawReserve( address asset, uint256 amount, address recipient ) external override onlyRole(RESERVE_MANAGER_ROLE) nonReentrant returns (bytes32 reserveId) { require(asset != address(0), "ReserveSystem: zero address"); require(recipient != address(0), "ReserveSystem: zero recipient"); require(amount > 0, "ReserveSystem: zero amount"); require(reserveBalances[asset] >= amount, "ReserveSystem: insufficient reserve"); reserveBalances[asset] -= amount; IERC20(asset).safeTransfer(recipient, amount); reserveId = keccak256(abi.encodePacked(asset, amount, block.timestamp, reserveIds.length)); emit ReserveWithdrawn(asset, amount, recipient, reserveId); } function getReserveBalance(address asset) external view override returns (uint256) { return reserveBalances[asset]; } function getReserveById(bytes32 reserveId) external view override returns (address asset, uint256 balance) { Reserve memory reserve = reserves[reserveId]; return (reserve.asset, reserve.balance); } // ============ Conversion ============ function convertAssets( address sourceAsset, address targetAsset, uint256 amount ) external override onlyRole(CONVERSION_OPERATOR_ROLE) nonReentrant returns ( bytes32 conversionId, uint256 targetAmount, uint256 fees ) { require(sourceAsset != address(0), "ReserveSystem: zero source asset"); require(targetAsset != address(0), "ReserveSystem: zero target asset"); require(amount > 0, "ReserveSystem: zero amount"); require(isSupportedAsset[sourceAsset], "ReserveSystem: unsupported source asset"); require(isSupportedAsset[targetAsset], "ReserveSystem: unsupported target asset"); // Calculate conversion (uint256 calculatedAmount, uint256 calculatedFees, address[] memory path) = calculateConversion(sourceAsset, targetAsset, amount); require(reserveBalances[targetAsset] >= calculatedAmount, "ReserveSystem: insufficient target reserve"); // Transfer source asset IERC20(sourceAsset).safeTransferFrom(msg.sender, address(this), amount); reserveBalances[sourceAsset] += amount; // Transfer target asset reserveBalances[targetAsset] -= calculatedAmount; IERC20(targetAsset).safeTransfer(msg.sender, calculatedAmount); conversionId = keccak256(abi.encodePacked(sourceAsset, targetAsset, amount, block.timestamp, conversionIds.length)); conversions[conversionId] = Conversion({ sourceAsset: sourceAsset, targetAsset: targetAsset, sourceAmount: amount, targetAmount: calculatedAmount, fees: calculatedFees, path: path, timestamp: block.timestamp }); conversionIds.push(conversionId); emit ConversionExecuted(sourceAsset, targetAsset, amount, calculatedAmount, conversionId, calculatedFees); return (conversionId, calculatedAmount, calculatedFees); } function calculateConversion( address sourceAsset, address targetAsset, uint256 amount ) public view override returns ( uint256 targetAmount, uint256 fees, address[] memory path ) { require(sourceAsset != address(0), "ReserveSystem: zero source asset"); require(targetAsset != address(0), "ReserveSystem: zero target asset"); require(amount > 0, "ReserveSystem: zero amount"); // Get prices (uint256 sourcePrice, uint256 sourceTimestamp) = getPrice(sourceAsset); (uint256 targetPrice, uint256 targetTimestamp) = getPrice(targetAsset); require(sourceTimestamp > 0 && targetTimestamp > 0, "ReserveSystem: price feed not available"); require(block.timestamp - sourceTimestamp <= PRICE_STALENESS_THRESHOLD, "ReserveSystem: stale source price"); require(block.timestamp - targetTimestamp <= PRICE_STALENESS_THRESHOLD, "ReserveSystem: stale target price"); // Direct conversion targetAmount = (amount * targetPrice) / sourcePrice; // Calculate fees fees = calculateFees(targetAmount, 0); // No slippage for direct conversion // Simple path (direct conversion) path = new address[](2); path[0] = sourceAsset; path[1] = targetAsset; return (targetAmount, fees, path); } function calculateFees(uint256 amount, uint256 slippageBps) internal pure returns (uint256) { uint256 baseFee = (amount * BASE_FEE_BPS) / 10000; uint256 slippageFee = 0; if (slippageBps > 10) { // 0.1% threshold slippageFee = (amount * slippageBps * SLIPPAGE_FEE_BPS) / 1000000; } uint256 largeTransactionFee = 0; if (amount >= LARGE_TRANSACTION_THRESHOLD) { largeTransactionFee = (amount * LARGE_TRANSACTION_FEE_BPS) / 10000; } return baseFee + slippageFee + largeTransactionFee; } // ============ Redemption ============ function redeem( address asset, uint256 amount, address recipient ) external override onlyRole(RESERVE_MANAGER_ROLE) nonReentrant returns (bytes32 redemptionId) { require(asset != address(0), "ReserveSystem: zero address"); require(recipient != address(0), "ReserveSystem: zero recipient"); require(amount > 0, "ReserveSystem: zero amount"); require(reserveBalances[asset] >= amount, "ReserveSystem: insufficient reserve"); reserveBalances[asset] -= amount; IERC20(asset).safeTransfer(recipient, amount); redemptionId = keccak256(abi.encodePacked(asset, amount, block.timestamp, conversionIds.length)); emit RedemptionExecuted(asset, amount, recipient, redemptionId); } // ============ Price Feeds ============ function updatePriceFeed( address asset, uint256 price, uint256 timestamp ) external override onlyRole(PRICE_FEED_ROLE) { require(asset != address(0), "ReserveSystem: zero address"); require(price > 0, "ReserveSystem: zero price"); require(timestamp <= block.timestamp, "ReserveSystem: future timestamp"); if (!isSupportedAsset[asset]) { isSupportedAsset[asset] = true; supportedAssets.push(asset); } priceFeeds[asset] = PriceFeed({ price: price, timestamp: timestamp, isValid: true }); emit PriceFeedUpdated(asset, price, timestamp); } function getPrice(address asset) public view override returns (uint256 price, uint256 timestamp) { PriceFeed memory feed = priceFeeds[asset]; require(feed.isValid, "ReserveSystem: price feed not available"); return (feed.price, feed.timestamp); } function getConversionPrice( address sourceAsset, address targetAsset ) external view override returns (uint256) { (uint256 sourcePrice,) = getPrice(sourceAsset); (uint256 targetPrice,) = getPrice(targetAsset); return (targetPrice * 1e18) / sourcePrice; // Price in 18 decimals } // ============ Admin Functions ============ function addSupportedAsset(address asset, bool isLiquid) external onlyRole(DEFAULT_ADMIN_ROLE) { require(asset != address(0), "ReserveSystem: zero address"); isSupportedAsset[asset] = true; isLiquidAsset[asset] = isLiquid; if (!_isInArray(asset, supportedAssets)) { supportedAssets.push(asset); } } function removeSupportedAsset(address asset) external onlyRole(DEFAULT_ADMIN_ROLE) { isSupportedAsset[asset] = false; } function _isInArray(address item, address[] memory array) internal pure returns (bool) { for (uint256 i = 0; i < array.length; i++) { if (array[i] == item) { return true; } } return false; } function getSupportedAssets() external view returns (address[] memory) { return supportedAssets; } }