Files
smom-dbis-138/contracts/vault/Ledger.sol
2026-03-02 12:14:09 -08:00

303 lines
12 KiB
Solidity

// 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);
}
}