agents/skills/sui/oracle-analysis/SKILL.md
Trigger Pattern ORACLE flag (required) - Inject Into Breadth agents, depth-external, depth-edge-case
npx skillsauth add plamentsv/plamen oracle-analysisInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Trigger Pattern: ORACLE flag (required) Inject Into: Breadth agents, depth-external, depth-edge-case Purpose: Analyze oracle integrations in Sui Move protocols for staleness, decimal handling, failure modes, and manipulation vectors
For every oracle the protocol consumes:
STEP PRIORITY: Steps 6 (Failure Modes) and 5c (Deviation Reference) are where HIGH/CRITICAL severity findings most commonly hide. Do NOT rush these steps. If constrained, skip conditional sections (4a-4d, 5a) before skipping 5c or 6.
Enumerate ALL oracle data sources the protocol reads:
| Oracle | Type | Module / Object | Functions Called | Consumers (protocol functions) | Update Frequency | Heartbeat | |--------|------|-----------------|-----------------|-------------------------------|-----------------|-----------| | {name} | Pyth / Switchboard V2 / Supra / Custom / On-chain TWAP | {module::function or shared object ID} | {get_price / get_price_no_older_than / etc.} | {list all} | {expected} | {documented or UNKNOWN} |
For each oracle: What decision does the protocol make based on this data? (pricing, liquidation threshold, reward rate, share price, swap amount, etc.)
Hardcoded stablecoin pricing: Does the protocol skip oracle lookup for any asset and hardcode its price (e.g., USDC = 1e8)? All assets require dynamic oracle pricing — stablecoins depeg.
Sui-specific inventory checks:
&PriceInfoObject) or read from on-chain state?pyth::price_info::get_price_info_from_price_info_object() or a wrapper?&) or by mutable reference (&mut)?PriceInfoObject are shared objects. During high-volatility periods, multiple transactions compete to read/update the same oracle object, causing transaction ordering dependencies and potential stale reads due to sequencing delays.For each oracle identified in Step 1:
| Oracle | Timestamp Checked? | Max Staleness Enforced? | Staleness Threshold | Clock Source | Appropriate? | |--------|-------------------|------------------------|--------------------:|-------------|-------------| | {name} | YES/NO | YES/NO | {seconds or NONE} | {clock::timestamp_ms / custom} | {analysis} |
Chained feed deviation: If derived prices require multiple feeds (e.g., token_A/USD via token_A/SUI + SUI/USD), sum individual deviation thresholds to compute total worst-case deviation. If total exceeds LTV buffer → FINDING.
CRITICAL -- Sui uses MILLISECONDS: clock::timestamp_ms(clock) returns milliseconds, not seconds. Pyth's price.timestamp returns seconds. If the protocol compares these without unit conversion, the staleness check is 1000x too lenient or too strict. Check for:
publish_time (seconds from Pyth) vs clock::timestamp_ms() (milliseconds) -- MUST convert one to match the otherMAX_STALENESS -- is the value in seconds or milliseconds?Correct Sui staleness pattern:
// Pyth on Sui -- check price freshness
let price = pyth::price_info::get_price_info_from_price_info_object(price_info_object);
let price_data = price_info::get_price_feed(&price);
let current_price = price_feed::get_price(price_data);
let timestamp = price::get_timestamp(¤t_price); // SECONDS
let now_ms = clock::timestamp_ms(clock); // MILLISECONDS -- Clock is shared object at 0x6
let now_s = now_ms / 1000; // Convert to seconds to match Pyth
assert!(now_s - timestamp <= MAX_STALENESS_SECONDS, E_STALE_PRICE);
If NO staleness check: What happens when the oracle returns stale data?
For each consumer function, trace the impact of receiving data that is {heartbeat x 2} old:
| Consumer Function | Data Used | If Stale By {X}: Impact | Severity | |-------------------|-----------|------------------------|----------| | {function} | {price/rate} | {specific impact} | {H/M/L} |
| Check | Code Reference | Status |
|-------|---------------|--------|
| get_price() return values checked? | {location} | YES/NO |
| price.price validated > 0? | {location} | YES/NO |
| price.conf (confidence interval) checked? | {location} | YES/NO |
| price.expo (exponent) handled correctly? | {location} | YES/NO |
| price.timestamp staleness validated? | {location} | YES/NO |
| get_price_no_older_than() used vs manual check? | {location} | YES/NO |
| Pyth price update fee paid via Coin<SUI>? | {location} | YES/NO |
| Check | Code Reference | Status |
|-------|---------------|--------|
| aggregator::latest_value() return validated? | {location} | YES/NO |
| Result timestamp checked for staleness? | {location} | YES/NO |
| Decimal scaling applied correctly? (Switchboard custom decimals per feed) | {location} | YES/NO |
| Min response count checked? | {location} | YES/NO |
| Aggregator authority validated? | {location} | YES/NO |
| Check | Code Reference | Status | |-------|---------------|--------| | Price feed ID validated? | {location} | YES/NO | | Decimal precision from feed metadata used? | {location} | YES/NO | | Timestamp freshness checked? | {location} | YES/NO | | Round completeness verified? | {location} | YES/NO |
For each oracle data flow:
| Oracle | Oracle Decimals / Exponent | Consumer Expects | Normalization Applied? | Correct? | |--------|---------------------------|-----------------|----------------------|----------| | {name} | {e.g., expo = -8 for Pyth} | {expected by math} | YES/NO | {analysis} |
Pyth exponent handling: Pyth returns Price { price: i64, conf: u64, expo: i32, timestamp: u64 }. The expo field is typically negative (e.g., -8 means price has 8 decimal places). Verify:
expo read dynamically or assumed to be a fixed value?expo handled correctly (negative exponent = division, positive = multiplication)?10^|expo| computation overflow for large exponents?MANDATORY GREP: Search all oracle consumer modules for hardcoded decimal constants: 1_000_000_000, 100_000_000, 1_000_000, 10_000, 1e8, 1e6, 1e9, DECIMAL, PRECISION. For each hit: (1) Is this a decimal normalization constant? (2) Does it match the ACTUAL oracle's exponent? (3) If the oracle feed changes exponent, does this constant break?
Decimal chain trace: For each arithmetic operation using oracle data, trace the full decimal chain: oracle_output_decimals -> normalization_step -> consumer_expected_decimals. If any step uses a hardcoded constant rather than reading the exponent dynamically -> FINDING.
Common Sui decimal mismatches:
MIST_PER_SUI = 1_000_000_000)price * amount where price and amount have different decimal basesGrep ALL oracle consumer modules for 10_|decimals|PRECISION|SCALE|expo|pow. For each match, fill:
| File:Line | Pattern | Hardcoded Value | Oracle's Actual Decimals/Expo | Match? | |-----------|---------|-----------------|------------------------------|--------|
If ANY row shows Match=NO or oracle decimals UNKNOWN with hardcoded constant -> FINDING (R16). Skipping this step is a Step Execution violation (x3d).
<!-- LOAD_IF: TWAP -->If protocol uses any TWAP oracle (on-chain pool observation, custom TWAP accumulator, etc.):
| TWAP Oracle | Window Length | Pool Liquidity | Manipulation Cost (est.) | Sufficient? | |-------------|-------------|----------------|-------------------------|-------------| | {oracle} | {seconds} | {USD value} | {estimated} | YES/NO |
Rule of thumb: TWAP window < 30 min AND pool TVL < $10M -> potentially manipulable. Sui-specific: On Sui, TWAP typically relies on CLOB or AMM observation points. Check if the TWAP source is a CLOB (orderbook DEX) or AMM. CLOB TWAPs can be manipulated with limit orders that are never filled.
| Check | Status | Impact if Wrong | |-------|--------|-----------------| | Overflow protection on cumulative price difference? | YES/NO | {impact} | | Geometric vs arithmetic mean -- correct for use case? | {which used} | {impact if wrong} | | Time-weighted vs observation-count-weighted? | {which} | {manipulation vector} | | Empty observation slots handled? | YES/NO | {impact} |
During rapid price movements, TWAP lags spot price. Trace:
Check oracle behavior when history is insufficient: (1) zero snapshots, (2) single snapshot, (3) window period not yet elapsed.
| Cold-Start State | Oracle Return Value | Protocol Behavior | Exploitable? | |------------------|--------------------:|-------------------|-------------|
For each exploitable state: can attacker act during cold-start window at manipulated price? Tag: [BOUNDARY:snapshots=0], [BOUNDARY:snapshots=1]. If TWAP returns 0 or aborts during cold-start with no fallback -> FINDING (R16, minimum Medium).
<!-- END_LOAD_IF: TWAP -->For multi-oracle systems or oracle-based thresholds:
<!-- LOAD_IF: MULTI_ORACLE -->| Oracle System | Aggregation Method | Oracle Count | Agreement Required | What if Disagreement? | |---------------|-------------------|-------------|-------------------|----------------------| | {system} | Median / Mean / Weighted / First-valid | {N} | {M of N} | {fallback behavior} |
Check: What happens at exact threshold boundaries?
| Threshold | Oracle Data Used | Threshold Value | At Exact Boundary | Off-by-One? | |-----------|-----------------|----------------|-------------------|-------------| | {name} | {oracle field} | {value} | {behavior at exact value} | YES/NO |
Check > vs >=: At the exact threshold value, does the protocol behave as intended?
For each deviation check in the protocol (max_deviation, price_deviation, deviation_threshold, etc.):
| Parameter | Measured Against | Reference Source | Reference Manipulable? | Reference Staleable? | |-----------|-----------------|-----------------|----------------------|---------------------|
Checks:
[TRACE:deviation check: current vs {reference} -> reference source: {X} -> manipulable: {Y/N}]For each oracle, model failure scenarios:
| Failure Mode | Oracle Behavior | Protocol Response | Impact | Mitigation Present? | |-------------|-----------------|-------------------|--------|-------------------| | Zero return | Returns price = 0 | {what happens} | {impact} | YES/NO | | Abort | Function call aborts | {what happens} | {impact} | YES/NO -- caught? | | Stale (heartbeat exceeded) | Returns old data | {what happens} | {impact} | YES/NO -- staleness check? | | Extreme value | Returns outlier | {what happens} | {impact} | YES/NO -- bounds check? | | Negative price (Pyth i64) | Returns < 0 | {what happens} | {impact} | YES/NO -- sign check? | | High confidence interval | conf > threshold | {what happens} | {impact} | YES/NO -- conf check? | | Price update not called | PriceInfoObject never refreshed | {what happens} | {impact} | YES/NO -- update enforced? |
Sui-specific failure: Pyth on Sui requires explicit price update transactions (pyth::update_price_feeds). If the protocol does not enforce fresh updates before reading, prices can be arbitrarily stale. Check:
update_price_feeds in the same PTB before reading?get_price_no_older_than() which enforces freshness?For each unmitigated failure mode: What is the worst-case impact? Can it lead to fund loss?
Circuit breaker check: Does the protocol have a mechanism to pause oracle-dependent operations if the oracle enters a failure state?
**ID**: [OR-N]
**Severity**: [based on fund impact and likelihood of oracle failure/manipulation]
**Step Execution**: check1,2,3,4,5,6 | x(reasons) | ?(uncertain)
**Rules Applied**: [R1:check, R4:check, R10:check, R16:check]
**Location**: module::function:LineN
**Title**: Oracle [issue type] in [function] enables [attack/failure]
**Description**: [Specific oracle issue with data flow trace]
**Impact**: [Quantified impact under worst-case oracle scenario]
{CONTRACTS} -- Move modules to analyze
{ORACLE_MODULES} -- Oracle integration modules (pyth, supra, custom)
{PRICE_OBJECTS} -- Shared oracle objects (PriceInfoObject, etc.)
{CONSUMER_FUNCTIONS} -- Functions that read oracle data
{ORACLE_TYPE} -- Pyth / Supra / Switchboard / Custom
{HEARTBEAT} -- Expected update frequency
| Field | Required | Description | |-------|----------|-------------| | oracle_inventory | yes | All oracle sources and consumers | | staleness_analysis | yes | Staleness checks and impact | | decimal_audit | yes | Decimal normalization correctness | | failure_modes | yes | Each oracle's failure scenarios | | finding | yes | CONFIRMED / REFUTED / CONTESTED | | evidence | yes | Code locations with line numbers | | step_execution | yes | Status for each step |
| Section | Required | Completed? | Notes | |---------|----------|------------|-------| | 1. Oracle Inventory | YES | check/x/? | | | 2. Staleness Analysis | YES | check/x/? | For each oracle | | 3. Decimal Normalization Audit | YES | check/x/? | | | 3d. Decimal Grep Sweep | YES | check/x/? | MANDATORY mechanical step | | 4. TWAP-Specific Analysis | IF TWAP used | check/x(N/A)/? | | | 4d. TWAP Cold-Start Analysis | IF TWAP used | check/x(N/A)/? | Zero/single snapshot states | | 5. Oracle Weight / Threshold Boundaries | IF multi-oracle or thresholds | check/x(N/A)/? | | | 5c. Deviation Reference Point Audit | IF deviation checks exist | check/x(N/A)/? | Reference manipulability | | 6. Oracle Failure Modes | YES | check/x/? | For each oracle |
development
Prepare Solidity projects for a security audit — test coverage, test quality, NatSpec docs, code hygiene, dependency health, best-practice enforcement, deployment readiness, and project documentation checks. Generates a scored Audit Readiness Report and optionally runs static analysis. Trigger on: "prepare for audit", "audit readiness", "pre-audit check", "audit prep", "NatSpec check", or any request to review a Solidity codebase before a security review.
development
Launch the Plamen deterministic Web3 security audit pipeline
development
Run the Plamen smart-contract audit wizard in Codex
testing
Launch the Plamen deterministic L1 infrastructure audit pipeline