Skip to main content
Stenion
Registry

Kinetic

stellar·adapter KineticAdapter

liveScored 2026-10-09 07:30 UTC

Last run 2026-10-09 07:30 UTC · updated on every indexer cycle

Historical oracle staleness
96% stale(48 of 50 observed runs)

Oracle prices were stale in 48 of the last 50 runs (96% of time). Persistent staleness indicates feed refresh gaps or unaligned TTLs.

Verify this yourself

Protocol names, logos and trademarks belong to their respective owners and are shown for identification only. Their presence here and any link to a protocol's own site or documentation does not imply endorsement, partnership, or any relationship with Stenion, in either direction.

Factor breakdown

Why the score is what it is — each factor on a 0–100 scale, higher is safer.

Oracleweight 25%0

3 of 5 reserves tied at the worst score — CCW67T… 15066s old (fresh<30s, dead>3600s); CCCRWH… 15060s old (fresh<30s, dead>3600s); CBSJZE… 14996s old (fresh<30s, dead>3600s); circuit breaker armed on all reserves

  • Price freshness0

    3 of 5 reserves tied at the worst score — CCW67T… 15066s old (fresh<30s, dead>3600s); CCCRWH… 15060s old (fresh<30s, dead>3600s); CBSJZE… 14996s old (fresh<30s, dead>3600s); anchored to K2's own cache TTL (30s) and staleness threshold (3600s)

  • Deviation bound100

    all 5 reserves score the same — breaker armed at 2000bps against a stored baseline

  • Price age by feed (not scored)not scored

    USDC 15066s, PYUSD 15060s, USDT0 14996s, XLM 1560s, SolvBTC_FUNDAMENTAL/USD 140s — 3 of 5 past the protocol's own 3600s staleness limit (USDC, PYUSD, USDT0). Reported, not graded: priceFreshness already scores the worst of these.

  • Bound tightness (not scored)not scored

    max_price_change_bps 2000 (20%), global. Measured against the last price the oracle served, so unlike Blend's per-publish-interval bound this has no fixed time spacing — the two are not comparable as numbers. Reported, not graded — see METHODOLOGY.md §2. Price sources: CCW67T… batchAdapter, CAS3J7… batchAdapter, CCCRWH… batchAdapter, CBIJBD… batchAdapter, CBSJZE… batchAdapter.

Liquidityweight 15%42

worst reserve (CBIJBD…) has 42% of supply as free liquidity

  • Reserves excluded as too smallnot scored

    2 reserve(s) below the minimum scorable size, so not graded for free liquidity: CCCRWH… $4.01 (0.21% of pool), would have scored 0; CBSJZE… $7.00 (0.36% of pool), would have scored 100

Utilizationweight 20%28

worst reserve (CBIJBD…) at 58% util vs 80% optimal-utilization kink

  • Reserves excluded as too smallnot scored

    2 reserve(s) below the minimum scorable size, so not graded for utilization headroom: CCCRWH… $4.01 (0.21% of pool), would have scored 0; CBSJZE… $7.00 (0.36% of pool), would have scored 100

Collateralweight 20%13

top reserve holds 94% of supplied value across 5 reserves (HHI 0.89)

Admin keyweight 20%60

admin is a contract (CC4UQN…) — not introspectable via Horizon; neutral baseline

Findings

What we found reading Kinetic’s contracts that isn’t captured by a factor. These do not affect the score and cannot change the ranking.

The price feed is older than K2's own staleness threshold in most observed runs

Between 2026-08-11 18:16 and 2026-08-18 15:55 UTC, Stenion recorded 1,469 scored runs against K2. In 1,370 of them — 93% — oracleSafety was 0. The distribution is close to all-or-nothing: 1,370 runs at 0 against 3 runs at 100, rather than spread across the range. In every run of that window where the sub-signals were recorded separately, the deviation-bound signal was 100 — so the zeroes are price age, not a missing circuit breaker.

The threshold behind that is 3600 seconds: price_staleness_threshold, K2’s own on-chain limit, not a Stenion constant. The oldest single price observed in the window was 41,777 seconds (11h 36m).

It is not a continuous outage. The feed refreshes to an age of a few hundred seconds, climbs back past the threshold over roughly an hour, and then sits there for hours before refreshing again. That cycle is what makes the overall score oscillate: the oscillation is the price ageing out and being renewed, not the pool’s risk genuinely changing every few minutes. It shows up as a repeating sawtooth in the score history above once enough runs have accumulated since the history was reset — a chart covering only a few hours may catch one excursion, or none.

The runs behind these counts have since been discarded — the stored score history was reset when the methodology was flattened to a single published version, so these figures cannot be re-derived from the API. What is stated here is an observation over a closed window that was made and recorded, not a claim about rows you can still fetch. The condition itself needs none of our history: it is checkable on-chain directly, by the steps below.

What is not being claimed: the circuit breaker was scored 100 throughout, so the bound on a single-step price move is armed — this is purely about freshness. And a price past a staleness threshold does not by itself mean the protocol acts on it. K2’s own code may reject the stale price and revert whatever operation depended on it. What is observable from outside is the age of the price the oracle serves, not which of those two follows. Both matter to a depositor — one means positions can be valued on an hours-old price, the other means borrowing or liquidation may be unavailable for long stretches — but separating them needs a call path we cannot observe.

Recorded here as well as scored because a factor value shows the state right now. An oracleSafety of 0 in this instant and an oracleSafety that has been 0 for most of three days are different facts, and only the second one is a pattern.

Verify it yourself: Read ORACLE from the kinetic_router instance storage (CCTUJZLY…AWNXOJIV6J7) via Soroban RPC, then call get_asset_prices_vec_fresh on that oracle and compare each PriceData.timestamp against the current ledger close time. Read the thresholds from the same contract: get_oracle_config for price_staleness_threshold, and get_asset_config per asset for max_age.

Individual price feeds go stale for hours while others update in seconds

All four of K2’s reserves are priced by the same oracle contract (CCHRZE2K…) through the same batchAdapter source. Their prices do not age together. In one reading on 2026-08-18, SolvBTC was 27 seconds old and XLM 177 seconds old, while USDC was 21,421 seconds old (5h 57m) and PYUSD 41,777 seconds old (11h 36m). Repeated readings minutes apart showed the same split, with the two stale ages advancing in step with wall-clock time — meaning those entries were not being refreshed at all during the observation, rather than being sampled at an unlucky moment.

For scale against K2’s own limits: price_staleness_threshold is 3600 seconds, so the USDC reading exceeded it roughly sixfold and the PYUSD reading elevenfold. The per-asset max_age values are looser — 43,200 seconds for XLM and SolvBTC, 86,400 for USDC and PYUSD — and neither stale reading had reached those.

What this changes about the picture above: the staleness is not the whole oracle going quiet and coming back. The oracle is demonstrably alive and serving some assets within seconds while others sit untouched for hours, through one contract and one source. Freshness here is a property of the individual feed entry, not of the oracle as a whole.

USDC is the reading worth noting rather than PYUSD. PYUSD held about $4 of supplied value at the time and USDC about $54 — both small in absolute terms, but USDC is well above the size line Stenion uses elsewhere, so this is not a question of an abandoned dust market. A lending protocol valuing a stablecoin position off a six-hour-old reading is the observable condition.

What is not being claimed: nothing here says the protocol acted on a stale price. K2’s own code may reject it and revert whatever operation depended on it — that is the same limit noted above, and it applies equally here. What is observable from outside is the age of the price the oracle serves. Nor is any cause implied: an upstream feed pausing, a per-asset configuration, and a deliberate choice all look identical from here.

Every reserve’s price age is now published on each run in the oracleSafety factor’s priceAges component, alongside a count of how many exceed K2’s own threshold. It is reported, not scored — the freshness score already grades the worst of them, and grading the spread again would count the same staleness twice.

Still running as of 2026-08-20. A fresh reading two days after the one above found SolvBTC at 26 seconds, XLM at 71, USDC at 618 and PYUSD at 23,304 (6h 28m) — the same split, from the same oracle, in the same call. Two things are worth taking from the repeat rather than from either reading alone. The condition is a standing property of these feeds and not a moment we happened to catch. And WHICH feed is stale moves: USDC was 21,421 seconds old on 2026-08-18 and 618 seconds old on 2026-08-20, while PYUSD exceeded K2’s 3,600-second threshold on both. So a reader should not take the specific assets named here as the affected set — the pattern is that some entry is hours behind while others are seconds behind, not that it is always this one.

Verify it yourself: Call get_asset_prices_vec_fresh on CCHRZE2K…5BNOMQRMU for the four assets in get_reserves_list on the router (CCTUJZLY…AWNXOJIV6J7) and compare each PriceData.timestamp against the current ledger close time — read them together in one call so the ages are directly comparable. Repeat a few minutes later: an age that grows by the elapsed wall-clock time is an entry that is not being refreshed. Read get_asset_config per asset for source and max_age, and get_oracle_config for price_staleness_threshold.

The deployed price oracle is a superset of its audited version

K2's price oracle contract (CCHRZE2K…) exports 44 functions. Thirty-five of them match the audited source published for the Code4rena review (code-423n4/2026-04-k2), and the deployed contract reports the same version() == 2 that source declares. Nine do not appear in the audited code at all: four *_as_admin variants, get_asset_prices_vec_fresh, and a four-function secondary-feed subsystem.

The live kinetic_router prices its reserves through one of those additions. Scanning the deployed router for the oracle method symbols it references shows get_asset_prices_vec_fresh present and get_asset_price_data — the method whose circuit-breaker enforcement is verifiable in the audited source — absent.

This matters to K2's oracleSafety score. The 20% circuit breaker (max_price_change_bps) is enforced on every return path of get_asset_price_data in the audited source, and its audited sibling get_asset_prices_vec enforces it too. All three methods return identical price data today. On that basis Stenion scores the breaker as enforced — but the path the pool actually uses has no public source, so that is an inference, not a verification, and it is recorded as one.

Stated neutrally: a deployed contract diverging from its audited snapshot is common and the additions may be entirely sound. What is being reported is the divergence and its consequence for what we can and cannot verify — not a claim that anything is wrong.

Verify it yourself: Fetch the contract code for CCHRZE2K…5BNOMQRMU and CCTUJZLY…AWNXOJIV6J7 via Soroban RPC getLedgerEntries and read the wasm export section; compare against contracts/price-oracle/src/contract.rs in code-423n4/2026-04-k2.

Two of K2’s live contracts are missing or out of date in its published contract list

K2 runs its markets as separate router contracts rather than as configurations inside one pool. Three are live on Stellar mainnet, all deployed from byte-identical code (wasm df2831cf…), sharing one price oracle, one pool admin and one treasury, each with its own configurator: the primary pooled market (CCTUJZLY…), a SolvBTC/xSolvBTC isolated market (CCGXGXIL…), and an Earn market for earnUSDC/USDC (CDWPVHKB…) operated by a third party, Gami/Upshift.

The isolated market’s router does not appear on K2’s contracts page. That page lists the xSolvBTC market as a set of reserve token addresses with no router among them, and repeats the primary market’s SolvBTC aToken and debt ledger beside them — which reads as though the market sits inside the primary pool. It does not. The router address above was not read from any documentation: it came from the pool_address field in the xSolvBTC aToken’s own instance storage, whose State also names it "K2 Iso Interest Bearing SolvBTC" (kiSolvBTC). Calling get_reserve_data for xSolvBTC on the primary router returns Error(Contract, #24) — it is not a reserve there.

The Earn market’s contract table is out of date in the other direction. It gives the earnUSDC aToken and debt ledger as "TBA" and says they "will be added once deployed". Both are deployed and wired: the Earn router’s get_reserves_list returns earnUSDC alongside USDC, and get_current_reserve_data resolves an aToken at CCOPG2ZQ… and a debt ledger at CBO4TOFT….

What this changes for Stenion’s coverage, stated plainly: this entry scores the primary market only. It is not a score for K2 as a whole, and the two other markets are unscored rather than assessed and passed over. Neither holds enough to be scorable — the isolated market held $3.62 and the Earn market held nothing at all when read on 2026-08-20 — so they fall below the market-size floor in the methodology, which is why there is no entry for either.

What is not being claimed: nothing here says the documentation is wrong about what the contracts do, that anything is hidden, or that any market is unsafe. Documentation lagging deployment is ordinary, and an empty market is an empty market rather than a defective one. What is reported is the divergence itself — a live market reachable only by reading contract storage, and a deployed pair described as pending — because a reader who takes the published list as complete gets a different picture of the protocol than the chain gives.

Verify it yourself: Read the instance storage of the xSolvBTC aToken (CBMGL7ZL…HGYJ6JALVY) via Soroban RPC getLedgerEntries and take pool_address from its State; call get_reserves_list and is_paused on the router it names. Compare against the tables at docs.k2lend.com/contracts and docs.k2lend.com/third-party-markets/contract-addresses. For the Earn market, call get_reserves_list on CDWPVHKB…KTPF6TZE and get_current_reserve_data for each asset, and compare the a_token_address it returns against the "TBA" row. Balances come from total_supply on each aToken and debt ledger.

get_price_with_protection provides no protection

In K2's audited oracle source, oracle.rs defines get_price_with_protection and get_price_with_protection_fallback. Both ignore their _config parameter and call the underlying query function directly — neither applies a circuit breaker or any other check.

This is not a hole: the real validation runs one level up, in validate_price_change inside get_asset_price_data, so prices are still checked. It is recorded because the name asserts a guarantee the function does not provide, which is the kind of thing that misleads anyone reading the source to understand how the oracle is defended.

Verify it yourself: Read contracts/price-oracle/src/oracle.rs in code-423n4/2026-04-k2 and follow the call sites of validate_price_change in contract.rs.

Score history

Every indexer run, on a fixed 0–100 axis and a real time axis. The line breaks wherever the score is unknown — a failed run, an indexing gap, or a methodology change — rather than drawing through it.

ExportCSVJSON
034671008 Oct 21:009 Oct 00:0003:0006:00
27High risk
9 Oct 07:30 UTC · methodology v1
safety score
Showing 50 runs · 8 Oct 19:15 → 9 Oct 07:30 UTC · 12h 15m · typically every 15m

Recent runs

The last 50 scoring runs, newest first. Open a scored run to see the factor breakdown it produced.