303 lines
12 KiB
Solidity
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);
|
|
}
|
|
}
|