Unaudited templates, testnet/fork/local only. Not an audit, not investment advice. VRF code under src/experimental/ is experimental and unsupported.
FeedKit: Foundry integration kit for Plugin (PLI) feeds on XDC
Small, MIT-licensed Solidity templates for reading Plugin oracle data on XDC Network. Testnet / fork / local only. Unaudited. No deploy scripts are included.
| Contract | Purpose |
|---|---|
src/PluginOracleAdapter.sol |
Checked AggregatorV3-like read in the feed's native decimals (decimals(): most live feeds use 16 and some older ones 18, and Plugin's contracts allow others, such as 25; don't hard-code 8). Always-on round checks: answer > 0, updatedAt != 0 (and not in the future), answeredInRound >= roundId, max staleness. Optional min/max bounds (off by default). "No access" reverts → FeedAccessDenied(feed); other reverts → FeedReverted(feed, reason). |
src/ChainlinkScaledAdapter.sol |
Optional wrapper that rescales any AggregatorV3-like source to 8 dp for Chainlink-style consumers. |
src/PluginStalenessDefaults.sol |
Generated per-feed default max staleness (the adapter's revert cutoff; FeedPlumb's status rule is stale after 1.5 × heartbeat) (the 57 feeds that were live on XDC mainnet on 2026-10-21, with defaults from the update gaps observed that day; see the feed defaults table). Pass maxStaleness = 0 to use it; pass any non-zero value to override. Unknown feed + 0 reverts. Daily FX/stock/metal/RWA feeds default to 72 h (weekends) and marketHours(feed) flags them so you can apply a stricter weekday limit. knownReverting(feed) flags proxies that reverted on 2026-10-21. Defaults are a snapshot and will be refreshed from a longer observation window. |
src/automation/PriceSnapshotUpkeep.sol |
Upkeep template (checkUpkeep/performUpkeep). performUpkeep is locked to an owner-set forwarder and re-checks its condition, so extra calls are harmless. |
src/ERC4626RateGuard.sol |
Guarded ERC-4626 share price = asset price × convertToAssets(1 share). Bounds + max deviation vs a reference rate; only keeper/owner can move the reference rate, capped per update. |
src/TwapFallback.sol |
Primary adapter price; falls back to a TWAP (ITwapSource) when the primary reverts or is out of bounds. Fallback is off by default (owner calls setFallbackEnabled(true)); window must be within [min, max]; checkpoint() is callable by anyone (bounded window, no value held). |
src/XSwapV2TwapSource.sol |
ITwapSource over an XSwap V2 pair (UQ112x112 cumulative price), with a minimum quote-token reserve check. |
Usage
forge build
forge test # mock tests (fork tests skip)
# from the published archive (feedkit.tar.gz: src, test, foundry.toml, foundry.lock, LICENSE), first run:
# forge install foundry-rs/forge-std@v1.17.0 --no-git
XDC_FORK_RPC=https://rpc.xinfin.network \
forge test --match-path 'test/fork/*' -vv # read-only XDC mainnet fork (alt: https://erpc.xinfin.network)
// XDC/USDT, 16 dp; maxStaleness 0 = table default (3600 s); no bounds
PluginOracleAdapter xdcUsdt = new PluginOracleAdapter(
IPluginFeed(0x896CC6fE8AD04dF9f481332606c445768e286076), 0, 0, 0);
uint256 p16 = xdcUsdt.latestPrice();
// Chainlink-style 8 dp consumers:
ChainlinkScaledAdapter xdcUsdt8 = new ChainlinkScaledAdapter(IAggregatorV3Like(address(xdcUsdt)));
Note: this proxy's description() returns "0xUSDT", not "XDC/USDT", and it has a different owner from most feeds. Don't assert description() in constructors for this feed; check the proxy address instead.
- Check the download:
curl -sO https://feedplumb.com/kit/feedkit.tar.gz -O https://feedplumb.com/kit/SHA256SUMS && sha256sum -c SHA256SUMS. - Mainnet addresses: the feed proxies listed here are on XDC mainnet (chainId 50). Plugin runs feeds on Apothem (51), but they are unlisted and less reliable: most of them stopped updating on 21 Oct 2026. Treat them as best-effort test data and check
updatedAtfrom latestRoundData; for reliable tests use a mainnet fork or a mock. Ten of the addresses hold unrelated contracts on Apothem, soeth_getCodethere is not a valid "is it deployed?" check. - Quote direction: a pair BASE/QUOTE reports the price of 1 BASE in QUOTE units, scaled by
decimals(). Feeds named USD/X (for example USD/AAVE) are inverted pairs. - RPC endpoints: mainnet
https://erpc.xinfin.networkandhttps://rpc.xinfin.network; Apothemhttps://rpc.apothem.networkandhttps://erpc.apothem.network. The xinfin endpoints may return HTTP 403 to non-browser clients that send no User-Agent.https://rpc.xdcrpc.com(thexdc_altentry infoundry.toml) answers without a key today, but XDC Gateway documents its endpoints as key-authenticated, so don't treat it as a guaranteed keyless endpoint. - Reverts over JSON-RPC: off chain (
eth_call), an access-controlled proxy returns"execution reverted: No access"withError(string)data (0x08c379a0…); a broken proxy such as BTC/USDT returns"execution reverted"with no data. Treat both as "feed unavailable", not as a transport failure, and don't retry them on another RPC. - Feed list as data: feeds.json lists all 90 known proxies with pair, decimals, status, staleness default,
description(), owner and aggregator. - Compile with
evm_version = "cancun"(XDC mainnet has no Prague yet). - Staleness defaults are a snapshot. Daily feeds (FX/stocks/metals/RWA) do not update outside market hours, so their default is 72 h; use
marketHours(feed)plus your own weekday limit where it matters. - Heartbeat not met: Plugin's registry publishes no heartbeat for most crypto feeds, so the kit assumes 30 minutes. ALGO, DOGE, ATOM and POL (and CAKE) were observed with longer gaps (1–3 h), so their defaults follow what was observed (2–5 h); see the feed defaults table.
- Reverting proxies: 33 proxies reverted on 2026-10-21 (status reverting (access-controlled or broken), as on the feed explorer).
"No access"→FeedAccessDenied; a revert with no error message (e.g. BTC/USDT0xEe28…bDCb) →FeedReverted(feed, ""). - TWAP fallback: off by default. The XSwap V2 WXDC/USDC pair (
0xf490…784A) held only ~42 USDC on 2026-10-21; a sensible minimum (e.g. 50,000 USDC quote reserve) makesXSwapV2TwapSource.quoterevert withInsufficientLiquidityon it (fork-tested). Do not enable the fallback on a pool that thin; a thin pool is cheap to manipulate. - Automation: Plugin Automation registry/registrar/forwarder addresses on XDC are unpublished (Plugin docs say "pull from artifacts"). Keep
forwarderunset (nobody can perform) until you have the address from Plugin, thensetForwarder. - Apothem: tests use a mock aggregator, because Plugin's Apothem feeds are unlisted and can stall for days.
Access-controlled feeds ("No access")
Some Plugin feed proxies only serve whitelisted callers and revert with "No access"; the adapter reports this as FeedAccessDenied(feed). Plugin's public docs do not describe a self-service whitelisting step. The routes they do give are the feeds portal (https://feeds.plugin.global, "Integrate Data Feeds" / request a feed) and the "Contact Us" link on the End data consumers pages (https://docs.plugin.global/plugin-data-feeds-platform/end-data-consumers-mainnet). Ask Plugin there to add your consumer contract address to the feed's access list, quoting the feed proxy address.
Licence
MIT (see the licence). Clean-room implementation. Code under src/experimental/ is unsupported.