// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "./interfaces/ILedger.sol"; import "./interfaces/IXAUOracle.sol"; import "./interfaces/IRateAccrual.sol"; /** * @title Ledger * @notice Core ledger for tracking collateral and debt balances * @dev Single source of truth for all vault accounting * * COMPLIANCE NOTES: * - All valuations are normalized to XAU (gold) as the universal unit of account * - eMoney tokens are XAU-denominated (1 eMoney = 1 XAU equivalent) * - GRU (Global Reserve Unit) is a NON-ISO 4217 synthetic unit of account, NOT legal tender * - All currency conversions MUST go through XAU triangulation * - ISO 4217 currency codes are validated where applicable */ contract Ledger is ILedger, AccessControl { bytes32 public constant VAULT_ROLE = keccak256("VAULT_ROLE"); bytes32 public constant PARAM_MANAGER_ROLE = keccak256("PARAM_MANAGER_ROLE"); // Collateral balances: vault => asset => amount mapping(address => mapping(address => uint256)) public override collateral; // Debt balances: vault => currency => amount mapping(address => mapping(address => uint256)) public override debt; // Risk parameters per asset mapping(address => uint256) public override debtCeiling; mapping(address => uint256) public override liquidationRatio; // in basis points mapping(address => uint256) public override creditMultiplier; // in basis points (50000 = 5x) mapping(address => uint256) public override rateAccumulator; // debt interest accumulator // System contracts IXAUOracle public xauOracle; IRateAccrual public rateAccrual; // Track registered assets mapping(address => bool) public isRegisteredAsset; constructor(address admin, address xauOracle_, address rateAccrual_) { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(PARAM_MANAGER_ROLE, admin); xauOracle = IXAUOracle(xauOracle_); rateAccrual = IRateAccrual(rateAccrual_); } /** * @notice Modify collateral balance for a vault * @param vault Vault address * @param asset Collateral asset address * @param delta Amount to add (positive) or subtract (negative) */ function modifyCollateral(address vault, address asset, int256 delta) external override onlyRole(VAULT_ROLE) { require(isRegisteredAsset[asset], "Ledger: asset not registered"); uint256 currentBalance = collateral[vault][asset]; if (delta > 0) { collateral[vault][asset] = currentBalance + uint256(delta); } else if (delta < 0) { uint256 decrease = uint256(-delta); require(currentBalance >= decrease, "Ledger: insufficient collateral"); collateral[vault][asset] = currentBalance - decrease; } emit CollateralModified(vault, asset, delta); } /** * @notice Modify debt balance for a vault * @param vault Vault address * @param currency Debt currency address (eMoney token) * @param delta Amount to add (positive) or subtract (negative) */ function modifyDebt(address vault, address currency, int256 delta) external override onlyRole(VAULT_ROLE) { // Accrue interest before modifying debt rateAccrual.accrueInterest(currency); uint256 accumulator = rateAccrual.getRateAccumulator(currency); rateAccumulator[currency] = accumulator; uint256 currentDebt = debt[vault][currency]; if (delta > 0) { uint256 addAmount = uint256(delta); debt[vault][currency] = currentDebt + addAmount; _totalDebtForCurrency[currency] += addAmount; _trackCurrency(currency); } else if (delta < 0) { uint256 decrease = uint256(-delta); require(currentDebt >= decrease, "Ledger: insufficient debt"); debt[vault][currency] = currentDebt - decrease; _totalDebtForCurrency[currency] -= decrease; } emit DebtModified(vault, currency, delta); } /** * @notice Get vault health (collateralization ratio in XAU) * @param vault Vault address * @return healthRatio Collateralization ratio in basis points (10000 = 100%) * @return collateralValue Total collateral value in XAU (18 decimals) * @return debtValue Total debt value in XAU (18 decimals) */ function getVaultHealth(address vault) external view override returns ( uint256 healthRatio, uint256 collateralValue, uint256 debtValue ) { collateralValue = _calculateCollateralValue(vault); debtValue = _calculateDebtValue(vault); if (debtValue == 0) { // No debt = infinite health healthRatio = type(uint256).max; } else { // healthRatio = (collateralValue / debtValue) * 10000 healthRatio = (collateralValue * 10000) / debtValue; } } /** * @notice Check if a vault can borrow a specific amount * @param vault Vault address * @param currency Debt currency address * @param amount Amount to borrow (in currency units) * @return borrowable True if borrow is allowed * @return reasonCode Reason code if borrow is not allowed */ function canBorrow(address vault, address currency, uint256 amount) external view override returns ( bool borrowable, bytes32 reasonCode ) { // Get current collateral and debt values in XAU uint256 collateralValue = _calculateCollateralValue(vault); uint256 currentDebtValue = _calculateDebtValue(vault); // Calculate new debt value in XAU // eMoney is XAU-denominated by design: 1 eMoney = 1 XAU equivalent // MANDATORY: If non-XAU currencies are used, they MUST be triangulated through XAU uint256 newDebtValue = currentDebtValue + amount; // Check debt ceiling uint256 totalDebt = _getTotalDebtForCurrency(currency); if (totalDebt + amount > debtCeiling[currency]) { return (false, keccak256("DEBT_CEILING_EXCEEDED")); } // Check collateralization ratio // Apply credit multiplier: maxBorrowValue = collateralValue * creditMultiplier / 10000 uint256 maxBorrowValue = (collateralValue * creditMultiplier[currency]) / 10000; if (newDebtValue > maxBorrowValue) { return (false, keccak256("INSUFFICIENT_COLLATERAL")); } // Check minimum collateralization ratio uint256 healthRatio = (collateralValue * 10000) / newDebtValue; uint256 minRatio = liquidationRatio[currency] + 100; // Add 1% buffer above liquidation ratio if (healthRatio < minRatio) { return (false, keccak256("BELOW_MIN_COLLATERALIZATION")); } return (true, bytes32(0)); } /** * @notice Set risk parameters for an asset * @param asset Asset address * @param debtCeiling_ Debt ceiling * @param liquidationRatio_ Liquidation ratio in basis points * @param creditMultiplier_ Credit multiplier in basis points */ function setRiskParameters( address asset, uint256 debtCeiling_, uint256 liquidationRatio_, uint256 creditMultiplier_ ) external onlyRole(PARAM_MANAGER_ROLE) { require(liquidationRatio_ > 0 && liquidationRatio_ <= 10000, "Ledger: invalid liquidation ratio"); require(creditMultiplier_ > 0 && creditMultiplier_ <= 100000, "Ledger: invalid credit multiplier"); // Max 10x isRegisteredAsset[asset] = true; debtCeiling[asset] = debtCeiling_; liquidationRatio[asset] = liquidationRatio_; creditMultiplier[asset] = creditMultiplier_; emit RiskParametersSet(asset, debtCeiling_, liquidationRatio_, creditMultiplier_); } /** * @notice Calculate total collateral value in XAU for a vault * @param vault Vault address * @return value Total value in XAU (18 decimals) */ function _calculateCollateralValue(address vault) internal view returns (uint256 value) { // For ETH collateral, get price from oracle // In production, would iterate over all collateral assets // For now, assume only ETH is supported // Get ETH balance uint256 ethBalance = collateral[vault][address(0)]; // address(0) represents ETH if (ethBalance == 0) { return 0; } // Get ETH/XAU price from oracle (uint256 ethPriceInXAU, ) = xauOracle.getETHPriceInXAU(); // Calculate value: ethBalance * ethPriceInXAU / 1e18 value = (ethBalance * ethPriceInXAU) / 1e18; } // Track currencies with debt for iteration address[] private _currenciesWithDebt; mapping(address => bool) private _isTrackedCurrency; // Total debt per currency (for debt ceiling check) mapping(address => uint256) private _totalDebtForCurrency; /** * @notice Calculate total debt value in XAU for a vault * @param vault Vault address * @return value Total debt value in XAU (18 decimals) * @dev MANDATORY COMPLIANCE: eMoney tokens are XAU-denominated * All debt is normalized to XAU terms for consistent valuation * If non-XAU currencies are used, they MUST be triangulated through XAU */ function _calculateDebtValue(address vault) internal view returns (uint256 value) { // eMoney tokens are XAU-denominated by design: 1 eMoney = 1 XAU equivalent // This ensures all debt valuations are consistent in XAU terms // If other currencies are used, they MUST be converted via XAU triangulation // Iterate over tracked currencies for (uint256 i = 0; i < _currenciesWithDebt.length; i++) { address currency = _currenciesWithDebt[i]; uint256 debtAmount = debt[vault][currency]; if (debtAmount > 0) { // Apply interest accrual uint256 accumulator = rateAccrual.getRateAccumulator(currency); uint256 debtWithInterest = (debtAmount * accumulator) / 1e27; // eMoney is XAU-denominated: 1:1 conversion // For other currencies, XAU triangulation would be applied here value += debtWithInterest; } } } /** * @notice Get total debt for a currency across all vaults * @param currency Currency address * @return total Total debt */ function _getTotalDebtForCurrency(address currency) internal view returns (uint256 total) { return _totalDebtForCurrency[currency]; } /** * @notice Track a currency when debt is created * @param currency Currency address */ function _trackCurrency(address currency) internal { if (!_isTrackedCurrency[currency]) { _currenciesWithDebt.push(currency); _isTrackedCurrency[currency] = true; } } /** * @notice Set XAU Oracle address * @param xauOracle_ New oracle address */ function setXAUOracle(address xauOracle_) external onlyRole(DEFAULT_ADMIN_ROLE) { require(xauOracle_ != address(0), "Ledger: zero address"); xauOracle = IXAUOracle(xauOracle_); } /** * @notice Set Rate Accrual address * @param rateAccrual_ New rate accrual address */ function setRateAccrual(address rateAccrual_) external onlyRole(DEFAULT_ADMIN_ROLE) { require(rateAccrual_ != address(0), "Ledger: zero address"); rateAccrual = IRateAccrual(rateAccrual_); } /** * @notice Grant VAULT_ROLE to an address (for factory use) * @param account Address to grant role to */ function grantVaultRole(address account) external onlyRole(DEFAULT_ADMIN_ROLE) { _grantRole(VAULT_ROLE, account); } }