const { ethers } = require("ethers");
// ─── Configuration ────────────────────────────────────────────────────────────
const ORACLE_ADDRESS = "0xA9F17344689C2c2328F94464998db1d3e35B80dC";
const RPC_URL = "https://mainnet.base.org";
const POLL_INTERVAL = 300_000; // 5 minutes in milliseconds
const ASSETS = {
"USDT/USD": {
id: "0x6ca0cef6107263f3b09a51448617b659278cff744f0e702c24a2f88c91e65a0d",
maxAge: 5400, // 90 minutes — 1.5× the 1hr heartbeat
maxDeviation: 0.005, // 0.5% from peg
expectedPeg: 1.0,
},
"USDC/USD": {
id: "0xf989296bde68043d307a2bc0e59de3445defc5f292eb390b80d78162c8a6b13d",
maxAge: 5400,
maxDeviation: 0.005,
expectedPeg: 1.0,
},
"CNGN/USD": {
id: "0x83a18c73cf75a028a24b79cbedb3b8d8ba363b748a3210ddbcaa95eec3b87b3a",
maxAge: 10800, // 3 hours — 1.5× the 2hr heartbeat
maxDeviation: 0.015, // 1.5% — wider for emerging market asset
expectedPeg: null, // No fixed peg check for CNGN
},
"ZARP/USD": {
id: "0x12373a3b1c4827c84bf6d7b11df100442695d0abfdb7a20d30a41d67d58e75a8",
maxAge: 10800,
maxDeviation: 0.015,
expectedPeg: null,
},
"BRZ/USD": {
id: "0xbc60b55b031dce1ee5679098bf2f35d66a94a566124e2b233324d2bafcc6d5b5",
maxAge: 10800,
maxDeviation: 0.015,
expectedPeg: null,
},
"ETH/USD": {
id: "0x8c3fb07cab369fe230ca4e45d095f796c4c1a30131f1799766d4fec5ee1325c0",
maxAge: 5400,
maxDeviation: null, // No peg check for ETH
expectedPeg: null,
},
};
// ─── Oracle Setup ─────────────────────────────────────────────────────────────
const provider = new ethers.JsonRpcProvider(RPC_URL);
const oracle = new ethers.Contract(ORACLE_ADDRESS, [
"function getAssetsInfo(bytes32[]) view returns ((int256 price, int8 decimal, uint256 lastUpdateTime)[], bool[])"
], provider);
// ─── Price State ──────────────────────────────────────────────────────────────
const previousPrices = {};
// ─── Alert Handler ────────────────────────────────────────────────────────────
function alert(level, symbol, message, data = {}) {
const timestamp = new Date().toISOString();
const entry = { timestamp, level, symbol, message, ...data };
const prefix = level === "CRITICAL" ? "🔴" :
level === "WARNING" ? "🟡" : "🟢";
console.log(`${prefix} [${timestamp}] [${level}] ${symbol}: ${message}`);
if (Object.keys(data).length > 0) {
console.log(" Data:", JSON.stringify(data, null, 2));
}
// Replace this with your alerting integration:
// - PagerDuty: sendPagerDutyAlert(entry)
// - Discord: sendDiscordWebhook(entry)
// - Telegram: sendTelegramMessage(entry)
// - Slack: sendSlackWebhook(entry)
}
// ─── Core Check Logic ─────────────────────────────────────────────────────────
async function checkFeeds() {
const symbols = Object.keys(ASSETS);
const assetIds = symbols.map(s => ASSETS[s].id);
const now = Math.floor(Date.now() / 1000);
let infos, exists;
try {
[infos, exists] = await oracle.getAssetsInfo(assetIds);
} catch (err) {
alert("CRITICAL", "ALL_FEEDS", "RPC call failed — oracle unreachable", {
error: err.message,
});
return;
}
for (let i = 0; i < symbols.length; i++) {
const symbol = symbols[i];
const config = ASSETS[symbol];
const info = infos[i];
const present = exists[i];
// ── Feed existence ───────────────────────────────────────────────────────
if (!present) {
alert("CRITICAL", symbol, "Feed returned exists = false");
continue;
}
const price = Number(info.price) / 10 ** -Number(info.decimal);
const ageSeconds = now - Number(info.lastUpdateTime);
// ── Staleness ────────────────────────────────────────────────────────────
if (ageSeconds > config.maxAge) {
alert("CRITICAL", symbol, "Feed is stale", {
ageSeconds,
maxAge: config.maxAge,
exceededBy: ageSeconds - config.maxAge,
lastUpdate: new Date(Number(info.lastUpdateTime) * 1000).toISOString(),
});
} else if (ageSeconds > config.maxAge * 0.8) {
alert("WARNING", symbol, "Feed approaching staleness threshold", {
ageSeconds,
maxAge: config.maxAge,
percentUsed: `${((ageSeconds / config.maxAge) * 100).toFixed(1)}%`,
});
}
// ── Peg deviation ────────────────────────────────────────────────────────
if (config.expectedPeg !== null) {
const deviation = Math.abs(price - config.expectedPeg) / config.expectedPeg;
if (deviation > config.maxDeviation) {
alert("CRITICAL", symbol, "Price deviation exceeds threshold", {
currentPrice: price,
expectedPeg: config.expectedPeg,
deviation: `${(deviation * 100).toFixed(4)}%`,
maxDeviation: `${(config.maxDeviation * 100).toFixed(2)}%`,
});
}
}
// ── Sudden price movement ─────────────────────────────────────────────────
const previous = previousPrices[symbol];
if (previous !== undefined) {
const movement = Math.abs(price - previous) / previous;
if (movement > 0.01) {
alert("WARNING", symbol, "Sudden price movement detected", {
previousPrice: previous,
currentPrice: price,
movement: `${(movement * 100).toFixed(4)}%`,
});
}
}
previousPrices[symbol] = price;
// ── Healthy feed log ──────────────────────────────────────────────────────
alert("INFO", symbol, `$${price.toFixed(6)} — ${ageSeconds}s old`);
}
}
// ─── Run ──────────────────────────────────────────────────────────────────────
console.log("IFÁ Labs Feed Monitor starting...");
console.log(`Polling interval: ${POLL_INTERVAL / 1000}s`);
console.log(`Oracle: ${ORACLE_ADDRESS}\n`);
checkFeeds();
setInterval(checkFeeds, POLL_INTERVAL);