Appearance
Integrating (Solidity)
Integration is two vendored files and one call. ISentinelRegistry.sol and SentinelFlags.sol are byte-stable: vendor them verbatim (flag bits are append-only and never reused), optionally alongside the SentinelGuard helper.
your-project/
src/vendor/sentinel/
ISentinelRegistry.sol ← consumer interface (byte-stable)
SentinelFlags.sol ← flag bit definitions (byte-stable)
SentinelGuard.sol ← optional revert-helperWith SentinelGuard
Call check at the top of any price-sensitive entrypoint:
solidity
import {ISentinelRegistry} from "./vendor/sentinel/ISentinelRegistry.sol";
import {SentinelFlags} from "./vendor/sentinel/SentinelFlags.sol";
import {SentinelGuard} from "./vendor/sentinel/SentinelGuard.sol";
contract StockTokenLendingMarket {
using SentinelGuard for ISentinelRegistry;
ISentinelRegistry immutable sentinel;
// A lending market whose LLTV already prices weekend gaps can tolerate
// MARKET_CLOSED; a scheduled multiplier is already reflected in the official
// Chainlink feed price, so MULTIPLIER_PENDING is tolerable too.
uint256 constant TOLERATED = SentinelFlags.MARKET_CLOSED | SentinelFlags.MULTIPLIER_PENDING;
uint256 constant MAX_HEARTBEAT_AGE = 15 minutes;
function borrow(address stockToken, uint256 amount) external {
sentinel.check(stockToken, TOLERATED, MAX_HEARTBEAT_AGE); // reverts unless operational
// ...
}
}check reverts with typed errors:
Sentinel__FlagsRaised(uint256 offending)— only the non-tolerated raised bits, so reverts are directly diagnosable.Sentinel__HeartbeatStale(uint256 age)— the watcher went quiet longer than you allow.
A non-reverting variant exists for callers that branch instead of reverting:
solidity
(bool ok, uint256 offending) = sentinel.isOk(stockToken, TOLERATED, MAX_HEARTBEAT_AGE);Without the guard
The raw pattern is two checks — flags and heartbeat. Both are required; the heartbeat bound is what makes a dead watcher fail closed:
solidity
(uint256 flags,) = sentinel.statusOf(stockToken);
require(flags & ~TOLERATED == 0, "sentinel: flags raised");
require(block.timestamp - sentinel.lastHeartbeat() <= MAX_HEARTBEAT_AGE, "sentinel: watcher dead");Choosing the parameters
Tolerated flags. Start from zero tolerance and justify each bit against your risk model (see the flag reference). The recommended mask for a lending market is MARKET_CLOSED | MULTIPLIER_PENDING — the first because gap-aware LLTVs already price closures, the second because official feed prices already include scheduled multipliers. Everything else is the issuer or data layer doing something your model didn't price.
Coverage is explicit — register before you integrate. statusOf synthesizes the NOT_MONITORED bit (0x20) for any token the registry owner has not registered via setMonitored(address[], true), and no sane tolerated mask includes it. Practical consequence: wire your market's token into the monitored set (and the watcher's token table) before creating the market, or every price read reverts — which is the point. A token nobody watches must not read as healthy.
Heartbeat age. Balance two failure modes: too tight and routine RPC hiccups freeze your protocol; too loose and a dead watcher protects nobody. The watcher's default cycle is 60s, so 15 minutes tolerates ~15 missed cycles. An oracle that gates a lending market (where a freeze blocks borrows and liquidations) may prefer a looser bound (hours) paired with tighter feed-staleness checks of its own — for example, Gapline (the sibling project) runs a 25h heartbeat bound so a single missed daily ops cycle fails closed, while doing its own feed-staleness verification in the oracle.
Freeze semantics — think it through
When your integration reverts on a Sentinel flag, everything downstream of that call path freezes. For a Morpho-style lending market where the oracle does the checking, that means borrows, collateral withdrawals and liquidations all halt while the flag is raised. That is usually what you want during an issuer pause (no liquidations at unverifiable prices — the Edel failure mode), at the cost of extended exposure: a position that became unhealthy during the freeze is only liquidatable after the flag clears. Document the tradeoff for your own users.
Reading status off-chain
ts
const [flags, updatedAt] = await registry.read.statusOf([token]);
// decode with the same bit table — see the watcher's describeFlags() helperThe registry also emits TokenStatusUpdated / GlobalStatusUpdated (with human-readable reasons) and Heartbeat events for indexing and alerting.