|
| 1 | +// SPDX-License-Identifier: BUSL-1.1 |
| 2 | +pragma solidity ^0.8.24; |
| 3 | + |
| 4 | +import {PositionManager} from "./PositionManager.sol"; |
| 5 | +import {CommonStructs} from "../../libraries/structs/CommonStructs.sol"; |
| 6 | +import {RoleRegistry} from "../../access/RoleRegistry.sol"; |
| 7 | +import {AccessManager} from "../../access/AccessManager.sol"; |
| 8 | + |
| 9 | +/** |
| 10 | + * @title IAccessManager |
| 11 | + * @notice Interface for checking if an account has a specific role. |
| 12 | + */ |
| 13 | +interface IAccessManager { |
| 14 | + /// @notice Checks if the given account holds the specified role. |
| 15 | + function hasRole(bytes32 role, address account) external view returns (bool); |
| 16 | +} |
| 17 | + |
| 18 | +/** |
| 19 | + * @title FundingEngine |
| 20 | + * @notice Calculates and applies periodic funding rates to open positions based on Open Interest (OI) imbalance. |
| 21 | + * |
| 22 | + * @dev This contract is called by a whitelisted Keeper to execute the funding cycle. |
| 23 | + * It reads market configuration and position data directly from the PositionManager. |
| 24 | + * |
| 25 | + * **Contract Flow:** |
| 26 | + * 1. A whitelisted Keeper calls the `applyFunding(marketId)`. |
| 27 | + * 2. The function first checks the market's configuration (`fundingEnabled`) and enforces the `FUNDING_PERIOD` time lock via `FundingTooSoon` error. |
| 28 | + * 3. It fetches Long and Short Open Interest (OI) from the `PositionManager`. |
| 29 | + * 4. The current funding rate (`rateBps`) is calculated in `_calculateRate` based on the OI skew, capped by the market's `maxFundingRateBps`. |
| 30 | + * 5. The number of full elapsed funding periods is calculated. |
| 31 | + * 6. `_applyToPositions` iterates through all active positions in the market. |
| 32 | + * 7. For each position, the payment is calculated in `_calcPayment` based on size, rate, and periods. |
| 33 | + * 8. The position's `accumulatedFunding` (debt/credit) is updated in the `PositionManager`. |
| 34 | + * 9. The `lastFundingTime` is updated to `block.timestamp`, and a `FundingApplied` event is emitted. |
| 35 | + */ |
| 36 | +contract FundingEngine { |
| 37 | + /// @notice Address of the PositionManager contract, used to access market configuration and position data. |
| 38 | + PositionManager public immutable positionManager; |
| 39 | + /// @notice Address of the AccessManager contract, used to verify keeper permissions. |
| 40 | + AccessManager public accessManager; |
| 41 | + |
| 42 | + /// @notice The absolute maximum funding rate allowed if not overridden by market config (0.3% per 8 hours). |
| 43 | + uint256 public constant MAX_FUNDING_RATE = 300; |
| 44 | + /// @notice The fixed duration for a funding calculation period (8 hours). |
| 45 | + uint256 public constant FUNDING_PERIOD = 8 hours; |
| 46 | + /// @notice Constant for Basis Points (10,000) used for scaling percentages. |
| 47 | + uint256 public constant BASIS_POINTS = 10_000; |
| 48 | + /// @notice Constant for 1e18 precision, used in rate calculation for fixed-point math. |
| 49 | + uint256 public constant PRECISION = 1e18; |
| 50 | + |
| 51 | + /// @notice Maps a market ID to the timestamp when funding was last successfully applied. |
| 52 | + mapping(bytes32 => uint256) public lastFundingTime; |
| 53 | + |
| 54 | + /// @dev Emitted when funding rates are successfully calculated and applied to a market. |
| 55 | + /// @param marketId The ID of the market. |
| 56 | + /// @param rateBps The calculated funding rate in basis points (BPS). |
| 57 | + /// @param longOI The total long open interest at the time of calculation. |
| 58 | + /// @param shortOI The total short open interest at the time of calculation. |
| 59 | + event FundingApplied(bytes32 indexed marketId, int256 rateBps, uint256 longOI, uint256 shortOI); |
| 60 | + /// @dev Emitted when funding is paid to a specific position. |
| 61 | + /// @param positionId The ID of the position. |
| 62 | + /// @param amount The funding amount paid (positive means funding credit, negative means funding debt). |
| 63 | + /// @param isLong True if the position is long, false if short. |
| 64 | + event FundingPaid(bytes32 indexed positionId, int256 amount, bool isLong); |
| 65 | + |
| 66 | + /// @notice Thrown when `applyFunding` is called before `FUNDING_PERIOD` has elapsed. |
| 67 | + error FundingTooSoon(); |
| 68 | + /// @notice Thrown when a market ID is not recognized (currently stubbed). |
| 69 | + error MarketNotFound(); |
| 70 | + |
| 71 | + /** |
| 72 | + * @notice Initializes the FundingEngine with addresses for PositionManager and AccessManager. |
| 73 | + * @param _positionManager The address of the PositionManager contract. |
| 74 | + * @param _accessManager The address of the AccessManager contract. |
| 75 | + */ |
| 76 | + constructor(address _positionManager, address _accessManager) { |
| 77 | + positionManager = PositionManager(_positionManager); |
| 78 | + accessManager = AccessManager(_accessManager); |
| 79 | + } |
| 80 | + |
| 81 | + /** |
| 82 | + * @dev Restricts function execution to addresses with the KEEPER_ROLE, as defined in the RoleRegistry. |
| 83 | + */ |
| 84 | + modifier onlyKeeper() { |
| 85 | + require(accessManager.hasRole(RoleRegistry.KEEPER_ROLE, msg.sender), "Only keeper"); |
| 86 | + _; |
| 87 | + } |
| 88 | + |
| 89 | + /** |
| 90 | + * @notice Applies funding fee to all positions in a market based on the calculated rate. |
| 91 | + * @dev This function can only be called by a whitelisted Keeper. |
| 92 | + * @param marketId The market ID. |
| 93 | + * @return rateBps The calculated funding rate in basis points (BPS). |
| 94 | + */ |
| 95 | + function applyFundingRate(bytes32 marketId) external onlyKeeper returns (int256 rateBps) { |
| 96 | + // Destructure only the required market config components |
| 97 | + ( |
| 98 | + , // maxLev (unused) |
| 99 | + , // mmr (unused) |
| 100 | + uint16 maxFund, |
| 101 | + bool fundEnabled, |
| 102 | + uint256 interval // fundingInterval (unused) |
| 103 | + ) = positionManager.marketConfig(marketId); |
| 104 | + |
| 105 | + // Check if funding is disabled or interval is zero |
| 106 | + if (!fundEnabled || interval == 0) { |
| 107 | + lastFundingTime[marketId] = block.timestamp; |
| 108 | + return 0; |
| 109 | + } |
| 110 | + |
| 111 | + // Enforce the funding period time lock |
| 112 | + if (block.timestamp < lastFundingTime[marketId] + FUNDING_PERIOD) { |
| 113 | + revert FundingTooSoon(); |
| 114 | + } |
| 115 | + |
| 116 | + uint256 longOI = positionManager.openInterest(marketId, CommonStructs.Side.LONG); |
| 117 | + uint256 shortOI = positionManager.openInterest(marketId, CommonStructs.Side.SHORT); |
| 118 | + uint256 totalOI = longOI + shortOI; |
| 119 | + |
| 120 | + // If no open interest, just update the last funding time and exit |
| 121 | + if (totalOI == 0) { |
| 122 | + lastFundingTime[marketId] = block.timestamp; |
| 123 | + return 0; |
| 124 | + } |
| 125 | + |
| 126 | + // Calculate the rate and number of periods |
| 127 | + rateBps = _calculateRate(longOI, shortOI, totalOI, maxFund); |
| 128 | + uint256 periods = (block.timestamp - lastFundingTime[marketId]) / FUNDING_PERIOD; |
| 129 | + |
| 130 | + // Apply funding to all positions |
| 131 | + _applyToPositions(marketId, rateBps, periods); |
| 132 | + |
| 133 | + // Update time and emit event |
| 134 | + lastFundingTime[marketId] = block.timestamp; |
| 135 | + emit FundingApplied(marketId, rateBps, longOI, shortOI); |
| 136 | + } |
| 137 | + |
| 138 | + /** |
| 139 | + * @notice Calculates the periodic funding rate based on open interest imbalance, capped by maxRateBps. |
| 140 | + * @dev Rate calculation: rate = (longOI - shortOI) / totalOI * maxRateBps. |
| 141 | + * A positive rate means Longs pay Shorts. A negative rate means Shorts pay Longs. |
| 142 | + * @param longOI Total open interest on the long side. |
| 143 | + * @param shortOI Total open interest on the short side. |
| 144 | + * @param totalOI The sum of longOI and shortOI. |
| 145 | + * @param maxRateBps The maximum absolute funding rate (in BPS) allowed for this market. |
| 146 | + * @return int256 The calculated funding rate in basis points (BPS). |
| 147 | + */ |
| 148 | + function _calculateRate(uint256 longOI, uint256 shortOI, uint256 totalOI, uint16 maxRateBps) |
| 149 | + internal |
| 150 | + pure |
| 151 | + returns (int256) |
| 152 | + { |
| 153 | + // Calculate imbalance factor scaled by PRECISION: (Long OI - Short OI) / Total OI * 1e18 |
| 154 | + int256 imbalance = (int256(longOI) - int256(shortOI)) * int256(PRECISION) / int256(totalOI); |
| 155 | + // Calculate raw rate: (Imbalance Factor * maxRateBps) / 1e18 |
| 156 | + int256 rate = (imbalance * int256(uint256(maxRateBps))) / int256(PRECISION); |
| 157 | + |
| 158 | + // Enforce max funding rate cap (symmetrically positive and negative) |
| 159 | + if (rate > int256(uint256(maxRateBps))) return int256(uint256(maxRateBps)); |
| 160 | + if (rate < -int256(uint256(maxRateBps))) return -int256(uint256(maxRateBps)); |
| 161 | + return rate; |
| 162 | + } |
| 163 | + |
| 164 | + /** |
| 165 | + * @notice Iterates over all open positions in a market and updates their accumulated funding. |
| 166 | + * @param marketId The market ID. |
| 167 | + * @param rateBps The calculated funding rate in BPS. |
| 168 | + * @param periods The number of full funding intervals that have elapsed since the last funding. |
| 169 | + */ |
| 170 | + function _applyToPositions(bytes32 marketId, int256 rateBps, uint256 periods) internal { |
| 171 | + // Fetch all position IDs for the given market |
| 172 | + bytes32[] memory posIds = positionManager.getMarketPositions(marketId); |
| 173 | + |
| 174 | + // Loop through each position ID |
| 175 | + for (uint256 i = 0; i < posIds.length; i++) { |
| 176 | + bytes32 posId = posIds[i]; |
| 177 | + |
| 178 | + // Destructure the tuple returned from the public PositionManager.positions mapping accessor |
| 179 | + ( |
| 180 | + CommonStructs.Position memory position, |
| 181 | + uint256 lastUpdateTime, |
| 182 | + int256 accumulatedFunding, |
| 183 | + bool isLiquidatable, |
| 184 | + bool inADLQueue |
| 185 | + ) = positionManager.positions(posId); |
| 186 | + |
| 187 | + // Rebuild the PositionData struct in memory (required for the subsequent logic if structs are used) |
| 188 | + PositionManager.PositionData memory data = PositionManager.PositionData({ |
| 189 | + position: position, |
| 190 | + lastUpdateTime: lastUpdateTime, |
| 191 | + accumulatedFunding: accumulatedFunding, |
| 192 | + isLiquidatable: isLiquidatable, |
| 193 | + inADLQueue: inADLQueue |
| 194 | + }); |
| 195 | + |
| 196 | + // Skip positions that are not opened (openedAt == 0 is an empty slot check) |
| 197 | + if (data.position.openedAt == 0) continue; |
| 198 | + |
| 199 | + // Calculate the funding payment |
| 200 | + int256 payment = |
| 201 | + _calcPayment(data.position.size, rateBps, periods, data.position.side == CommonStructs.Side.LONG); |
| 202 | + |
| 203 | + // Accumulate the funding payment |
| 204 | + data.accumulatedFunding += payment; |
| 205 | + |
| 206 | + // Update the accumulated funding in PositionManager |
| 207 | + positionManager.updateAccumulatedFunding(posId, data.accumulatedFunding); |
| 208 | + |
| 209 | + // Emit an event |
| 210 | + emit FundingPaid(posId, payment, data.position.side == CommonStructs.Side.LONG); |
| 211 | + } |
| 212 | + } |
| 213 | + |
| 214 | + /** |
| 215 | + * @notice Calculates the total funding payment for a single position. |
| 216 | + * @dev The payment sign is inverted for Longs because a positive rate means Longs pay Shorts. |
| 217 | + * @param size The size of the position. |
| 218 | + * @param rateBps The calculated funding rate in BPS. |
| 219 | + * @param periods The number of full funding intervals. |
| 220 | + * @param isLong True if the position is long, false if short. |
| 221 | + * @return int256 The funding payment amount. |
| 222 | + */ |
| 223 | + function _calcPayment(uint256 size, int256 rateBps, uint256 periods, bool isLong) internal pure returns (int256) { |
| 224 | + // Base payment calculated: size * rateBps * periods / BASIS_POINTS |
| 225 | + int256 base = int256(size) * rateBps * int256(periods) / int256(BASIS_POINTS); |
| 226 | + // If the rate is positive (Longs pay Shorts), Longs have negative payment, Shorts have positive payment. |
| 227 | + return isLong ? -base : base; |
| 228 | + } |
| 229 | + |
| 230 | + // === VIEW === |
| 231 | + /** |
| 232 | + * @notice Calculates the current funding rate for a given market based on open interest skew. |
| 233 | + * @dev This is the same rate calculation used internally by `applyFunding` but does not apply the funding. |
| 234 | + * @param marketId The identifier for the target market. |
| 235 | + * @return int256 The calculated funding rate in basis points (BPS). |
| 236 | + */ |
| 237 | + function getCurrentRate(bytes32 marketId) external view returns (int256) { |
| 238 | + uint256 longOI = positionManager.openInterest(marketId, CommonStructs.Side.LONG); |
| 239 | + uint256 shortOI = positionManager.openInterest(marketId, CommonStructs.Side.SHORT); |
| 240 | + uint256 total = longOI + shortOI; |
| 241 | + |
| 242 | + // Fetch the max funding rate BPS (3rd component) from market config |
| 243 | + // NOTE: Uses tuple destructuring for efficiency, skipping unused fields. |
| 244 | + (,, uint16 maxRateBps,,) = positionManager.marketConfig(marketId); |
| 245 | + |
| 246 | + return total == 0 ? int256(0) : _calculateRate(longOI, shortOI, total, maxRateBps); |
| 247 | + } |
| 248 | + |
| 249 | + /** |
| 250 | + * @notice Calculates the time remaining until funding can be applied again. |
| 251 | + * @param marketId The market ID. |
| 252 | + * @return uint256 Time in seconds until the next funding period begins, or 0 if funding is overdue. |
| 253 | + */ |
| 254 | + function timeUntilNext(bytes32 marketId) external view returns (uint256) { |
| 255 | + uint256 next = lastFundingTime[marketId] + FUNDING_PERIOD; |
| 256 | + return block.timestamp >= next ? 0 : next - block.timestamp; |
| 257 | + } |
| 258 | +} |
0 commit comments