// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "./interfaces/IRateAccrual.sol"; /** * @title RateAccrual * @notice Applies time-based interest to outstanding debt using continuous compounding * @dev Similar to Aave's interest rate model */ contract RateAccrual is IRateAccrual, AccessControl { bytes32 public constant RATE_MANAGER_ROLE = keccak256("RATE_MANAGER_ROLE"); uint256 public constant BASIS_POINTS = 10000; uint256 public constant SECONDS_PER_YEAR = 365 days; uint256 public constant RAY = 1e27; // Used for precision in calculations // Asset => interest rate (in basis points, e.g., 500 = 5% annual) mapping(address => uint256) private _interestRates; // Asset => rate accumulator (starts at RAY, increases over time) mapping(address => uint256) private _rateAccumulators; // Asset => last update timestamp mapping(address => uint256) private _lastUpdate; constructor(address admin) { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(RATE_MANAGER_ROLE, admin); } /** * @notice Accrue interest for an asset * @param asset Asset address * @return newAccumulator Updated rate accumulator */ function accrueInterest(address asset) external override returns (uint256 newAccumulator) { uint256 oldAccumulator = _rateAccumulators[asset]; uint256 lastUpdate = _lastUpdate[asset]; if (lastUpdate == 0) { // Initialize accumulator _rateAccumulators[asset] = RAY; _lastUpdate[asset] = block.timestamp; return RAY; } if (block.timestamp == lastUpdate) { return oldAccumulator; } uint256 rate = _interestRates[asset]; if (rate == 0) { return oldAccumulator; } // Calculate time elapsed in years uint256 timeElapsed = block.timestamp - lastUpdate; uint256 timeInYears = (timeElapsed * RAY) / SECONDS_PER_YEAR; // Continuous compounding: newAccumulator = oldAccumulator * e^(rate * time) // Approximation: e^(r*t) ≈ 1 + r*t + (r*t)^2/2! + ... // For small rates: e^(r*t) ≈ 1 + r*t (first order approximation) // More accurate: use compound interest formula with high precision // Convert rate from basis points to RAY: rateInRay = (rate * RAY) / BASIS_POINTS uint256 rateInRay = (rate * RAY) / BASIS_POINTS; // Calculate exponent: rateInRay * timeInYears / RAY uint256 exponent = (rateInRay * timeInYears) / RAY; // newAccumulator = oldAccumulator * (1 + exponent) // For better precision, we use: newAccumulator = oldAccumulator + (oldAccumulator * exponent) / RAY newAccumulator = oldAccumulator + (oldAccumulator * exponent) / RAY; _rateAccumulators[asset] = newAccumulator; _lastUpdate[asset] = block.timestamp; emit InterestAccrued(asset, oldAccumulator, newAccumulator); } /** * @notice Get current rate accumulator for an asset (accrues interest if needed) * @param asset Asset address * @return accumulator Current rate accumulator */ function getRateAccumulator(address asset) external view override returns (uint256 accumulator) { accumulator = _rateAccumulators[asset]; uint256 lastUpdate = _lastUpdate[asset]; if (lastUpdate == 0) { return RAY; // Initial value } if (block.timestamp == lastUpdate) { return accumulator; } uint256 rate = _interestRates[asset]; if (rate == 0) { return accumulator; } // Calculate accrued interest (same logic as accrueInterest but view-only) uint256 timeElapsed = block.timestamp - lastUpdate; uint256 timeInYears = (timeElapsed * RAY) / SECONDS_PER_YEAR; uint256 rateInRay = (rate * RAY) / BASIS_POINTS; uint256 exponent = (rateInRay * timeInYears) / RAY; accumulator = accumulator + (accumulator * exponent) / RAY; } /** * @notice Set interest rate for an asset * @param asset Asset address * @param rate Annual interest rate in basis points (e.g., 500 = 5%) */ function setInterestRate(address asset, uint256 rate) external onlyRole(RATE_MANAGER_ROLE) { require(rate <= BASIS_POINTS * 100, "RateAccrual: rate too high"); // Max 100% annual // Accrue interest before updating rate if (_lastUpdate[asset] > 0) { this.accrueInterest(asset); } else { _rateAccumulators[asset] = RAY; _lastUpdate[asset] = block.timestamp; } _interestRates[asset] = rate; emit InterestRateSet(asset, rate); } /** * @notice Get interest rate for an asset * @param asset Asset address * @return rate Annual interest rate in basis points */ function interestRate(address asset) external view override returns (uint256) { return _interestRates[asset]; } /** * @notice Calculate debt with accrued interest * @param asset Asset address * @param principal Principal debt amount * @return debtWithInterest Debt amount with accrued interest */ function calculateDebtWithInterest(address asset, uint256 principal) external view override returns (uint256 debtWithInterest) { uint256 accumulator = this.getRateAccumulator(asset); // debtWithInterest = principal * accumulator / RAY debtWithInterest = (principal * accumulator) / RAY; } }