Skip to content

Commit 0606e22

Browse files
feat(oracles): implement complete production-grade oracle adapter suite
- Add ChainlinkAdapter, PythAdapter, TWAPAdapter, ComputedOracle, and TrustedOracle - Add unified IPriceFeed interface - Add OracleRegistry – central asset → primary/fallback oracle mapping - Add OracleSecurity – final price validation layer with staleness, deviation, confidence, and circuit-breaker protection - ComputedOracle enables inverse (USD/NGN) and cross-price synthetics (MTNN/USD, DANGCEM/USD) - TrustedOracle provides secure manual feeds for African macro data (CBN rates, treasury yields, elections) - Full NatSpec, events, access control, and mainnet-ready safety checks This completes BAOBAB’s unbreakable multi-oracle system – the only stack in DeFi capable of safely pricing Nigerian stocks, parallel-market NGN, and real-world events on-chain.
1 parent f9ece72 commit 0606e22

20 files changed

Lines changed: 3099 additions & 1025 deletions

README.md

Lines changed: 323 additions & 447 deletions
Large diffs are not rendered by default.

src/README.md

Lines changed: 155 additions & 100 deletions
Large diffs are not rendered by default.

src/access/AccessManager.sol

Lines changed: 232 additions & 99 deletions
Large diffs are not rendered by default.

src/access/ProtocolOwner.sol

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity ^0.8.24;
33

44
import {AccessManager} from "./AccessManager.sol";
55
import {RoleRegistry} from "./RoleRegistry.sol";
6+
import {AddressUtils} from ".././libraries/utils/AddressUtils.sol";
67

78
/**
89
* @title ProtocolOwner
@@ -15,6 +16,7 @@ import {RoleRegistry} from "./RoleRegistry.sol";
1516
* ═══════════════════════════════════════════════════════════════════════════════════════════════════
1617
*/
1718
contract ProtocolOwner {
19+
using AddressUtils for *;
1820
// ═══════════════════════════════════════════════════════════════════════════════════════════════
1921
// STATE VARIABLES
2022
// ═══════════════════════════════════════════════════════════════════════════════════════════════
@@ -69,6 +71,11 @@ contract ProtocolOwner {
6971
revert ProtocolOwner__InvalidAddress();
7072
}
7173

74+
_accessManager.validateNotZero();
75+
_treasury.validateNotZero();
76+
_insuranceVault.validateNotZero();
77+
_feeRecipient.validateNotZero();
78+
7279
accessManager = AccessManager(_accessManager);
7380
treasury = _treasury;
7481
insuranceVault = _insuranceVault;
@@ -87,6 +94,7 @@ contract ProtocolOwner {
8794
}
8895

8996
modifier whenNotPaused() {
97+
// protocolPaused.validateNotZero();
9098
if (protocolPaused) revert ProtocolOwner__ProtocolPaused();
9199
_;
92100
}
@@ -100,6 +108,7 @@ contract ProtocolOwner {
100108
* @param newTreasury New treasury address
101109
*/
102110
function setTreasury(address newTreasury) external onlyOwner {
111+
newTreasury.validateNotZero();
103112
if (newTreasury == address(0)) revert ProtocolOwner__InvalidAddress();
104113

105114
address oldTreasury = treasury;
@@ -113,6 +122,7 @@ contract ProtocolOwner {
113122
* @param newVault New insurance vault address
114123
*/
115124
function setInsuranceVault(address newVault) external onlyOwner {
125+
newVault.validateNotZero();
116126
if (newVault == address(0)) revert ProtocolOwner__InvalidAddress();
117127

118128
address oldVault = insuranceVault;
@@ -126,6 +136,7 @@ contract ProtocolOwner {
126136
* @param newRecipient New fee recipient address
127137
*/
128138
function setFeeRecipient(address newRecipient) external onlyOwner {
139+
newRecipient.validateNotZero();
129140
if (newRecipient == address(0)) revert ProtocolOwner__InvalidAddress();
130141

131142
address oldRecipient = feeRecipient;
@@ -179,6 +190,7 @@ contract ProtocolOwner {
179190
if (!emergencyWithdrawalEnabled) {
180191
revert ProtocolOwner__EmergencyWithdrawalNotEnabled();
181192
}
193+
182194
if (to == address(0)) revert ProtocolOwner__InvalidAddress();
183195

184196
// Transfer tokens

src/core/data/OrderStorage.sol

Lines changed: 534 additions & 3 deletions
Large diffs are not rendered by default.

src/core/trading/FundingRateCalculator.sol

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
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

Comments
 (0)