Skip to content

Latest commit

 

History

History
341 lines (256 loc) · 11.3 KB

File metadata and controls

341 lines (256 loc) · 11.3 KB

Troubleshooting

Table of Contents

  1. Sync Errors and User State Desynchronization
  2. Price Collection Failures
  3. Transaction Revert Scenarios
  4. Health Factor Miscalculations
  5. Diagnostic Procedures
  6. Error Handling Code Examples
  7. Mitigation Strategies
  8. Known Limitations and Workarounds

Sync Errors and User State Desynchronization

Sync errors occur when the local user state diverges from the on-chain state, typically due to failed updates or inconsistent data fetching. The EVAA SDK relies on accurate synchronization between user principals, asset configurations, and price data to maintain correct state representation.

The PricesCollector class is responsible for aggregating price data from multiple sources and ensuring consistency across oracle feeds. When synchronization fails, it often stems from mismatched asset lists or outdated principal data.

flowchart TD
A[Start Sync Process] --> B{Check Principal Data}
B --> |Valid| C[Fetch Prices for Assets]
B --> |Invalid| D[Throw Sync Error]
C --> E{Prices Retrieved?}
E --> |Yes| F[Validate Timestamps and Signatures]
E --> |No| G[Retry with Alternative Sources]
F --> H{Valid Prices ≥ Minimal Oracles?}
H --> |Yes| I[Update Local State]
H --> |No| J[Throw Price Collection Failure]
I --> K[Sync Complete]
Loading

Diagram sources

  • PricesCollector.ts

Section sources

  • PricesCollector.ts

Price Collection Failures

Price collection failures can arise from network timeouts, invalid responses, or stale data. The SDK uses multiple price sources including backend endpoints and ICP-based oracles to ensure redundancy.

The collectAndFilterPrices function in utils.ts handles the core logic for collecting and validating price data. It applies two critical filters:

  • Timestamp validation: Ensures prices are not older than TTL_ORACLE_DATA_SEC (120 seconds)
  • Signature verification: Confirms data integrity using public key cryptography

When fewer than the required number of valid oracles (minimalOracles) return acceptable data, the process fails with "Prices are outdated".

sequenceDiagram
participant Client
participant Collector as PricesCollector
participant Source as PriceSource
participant Filter as collectAndFilterPrices
Client->>Collector : getPrices()
Collector->>Source : getPrices(fetchConfig)
Source-->>Collector : RawPriceData[]
Collector->>Filter : collectAndFilterPrices()
Filter->>Filter : verifyPricesTimestamp()
Filter->>Filter : verifyPricesSign()
Filter-->>Collector : Filtered RawPriceData[]
Collector->>Collector : Check count ≥ minimalOracles
alt Sufficient Valid Prices
Collector-->>Client : Prices object
else Insufficient Valid Prices
Collector-->>Client : Error : "Prices are outdated"
end
Loading

Diagram sources

  • utils.ts
  • PricesCollector.ts

Section sources

  • utils.ts
  • PricesCollector.ts

Transaction Revert Scenarios

Transaction reverts can occur due to several conditions:

Insufficient Collateral

When a user attempts to borrow beyond their collateral limit, the transaction will revert. This is calculated using the calculateHealthParams function which compares total debt against the liquidation threshold.

Expired Prices

Price data older than 120 seconds is considered expired and will cause transaction reverts. This is enforced by verifyPricesTimestamp() which checks the difference between current time and price timestamp.

Amount Validation Failures

Invalid amount values (e.g., zero or negative when positive is required) trigger validation failures. The SDK performs these checks before submitting transactions to the blockchain.

flowchart TD
A[Initiate Transaction] --> B{Validate Parameters}
B --> C[Check Amount > 0]
B --> D[Check Collateral Adequacy]
B --> E[Check Price Freshness]
C --> |Invalid| F[Reject Transaction]
D --> |Insufficient| F
E --> |Stale Prices| F
C --> |Valid| G[Submit to Blockchain]
D --> |Sufficient| G
E --> |Fresh Prices| G
G --> H{Transaction Successful?}
H --> |Yes| I[Update State]
H --> |No| J[Handle Revert]
Loading

Diagram sources

  • math.ts
  • utils.ts

Section sources

  • math.ts

Health Factor Miscalculations

Health factor miscalculations typically stem from incorrect input data or flawed assumptions in the calculation logic. The predictHealthFactor function computes the health factor based on projected changes to user balances.

Root causes include:

  • Incorrect price data: Using stale or invalid prices skews the calculation
  • Outdated asset configurations: Changes in collateral factors or liquidation thresholds not reflected
  • Floating-point precision issues: Converting bigints to numbers introduces rounding errors

The formula used is:

Health Factor = max(0, min(1, 1 - totalBorrow / totalLimit))

Where:

  • totalBorrow: Sum of all borrow positions weighted by asset price
  • totalLimit: Sum of supply positions multiplied by their liquidation thresholds
flowchart LR
A[User Action] --> B[Determine Balance Change Type]
B --> C[Calculate New totalBorrow]
B --> D[Calculate New totalLimit]
C --> E[Compute Health Factor]
D --> E
E --> F{Health Factor < 1?}
F --> |Yes| G[Liquidatable Risk]
F --> |No| H[Healthy Position]
Loading

Diagram sources

  • math.ts

Section sources

  • math.ts

Diagnostic Procedures

Effective diagnostics require systematic analysis of logs and state inspection.

Log Analysis

Enable debug logging to trace price collection:

// Enable console debugging in Backend.ts
// console.debug('outputData', outputData);
// console.debug('[FILTERING] before filtering prices len ', priceSource.sourceName, prices.length);

Key log markers to monitor:

  • [FILTERING] before filtering prices len
  • [FILTERING] after filtering prices len
  • Price source error
  • sign is valid:

State Inspection Techniques

Verify critical state elements:

  1. Principal dictionary: Ensure all keys match pool assets
  2. Price dictionary: Confirm all required assets have price entries
  3. Timestamp validation: Check that all prices are within TTL window
  4. Signature verification: Validate oracle signatures match known public keys

Use the following test case pattern for validation:

expect(prices.dict.values().length).toBeGreaterThan(4);
expect(prices.dataCell).not.toEqual(Cell.EMPTY);

Section sources

  • PriceCollector.test.ts
  • utils.ts

Error Handling Code Examples

Detecting and Handling Price Collection Errors

try {
    const prices = await collector.getPrices();
    if (prices.dict.size === 0) {
        throw new Error("No prices collected");
    }
} catch (error) {
    if (error.message.includes("Prices are outdated")) {
        // Implement retry with fallback sources
        await handlePriceCollectionRetry();
    } else if (error.message.includes("Failed to collect sufficient prices")) {
        // Switch to alternative price source
        await useFallbackPriceSource();
    } else {
        // Unknown error - propagate
        throw error;
    }
}

Graceful Handling of Sync Errors

async function safeSyncUserData() {
    try {
        await evaa.getSync();
        return { success: true, data: evaa.userData };
    } catch (error) {
        console.error("Sync failed:", error);
        return {
            success: false,
            error: "Failed to synchronize user data",
            retryable: true
        };
    }
}

Section sources

  • PriceCollector.test.ts
  • PricesCollector.ts

Mitigation Strategies

Retry Mechanisms

Implement exponential backoff for transient failures:

async function proxyFetchRetries(fetchPromise, fetchConfig) {
    const maxRetries = fetchConfig?.maxRetries || 3;
    let delay = 1000; // Start with 1s delay
    
    for (let i = 0; i < maxRetries; i++) {
        try {
            return await fetchPromise;
        } catch (error) {
            if (i === maxRetries - 1) throw error;
            await new Promise(resolve => setTimeout(resolve, delay));
            delay *= 2; // Exponential backoff
        }
    }
}

Fallback Price Sources

Configure multiple redundant sources:

const sources: PriceSourcesConfig = {
    backendEndpoints: [],
    icpEndpoints: DefaultPriceSourcesConfig.icpEndpoints,
};

const collector = new PricesCollector({
    ...config,
    sourcesConfig: sources,
    additionalPriceSources: [new FakeBackendPriceSource('', ORACLES_MAINNET)],
});

User Notifications

Provide clear feedback for common issues:

  • "Price data is stale - please refresh"
  • "Insufficient collateral for this action"
  • "Network congestion detected - transactions may fail"

Section sources

  • PricesCollector.ts
  • utils.ts

Known Limitations and Workarounds

Limitation: Single Asset Withdrawal Without Debt

When a user has only one supplied asset and attempts to withdraw without debt, the system returns empty price data:

if (checkNotInDebtAtAll(realPrincipals) && (realPrincipals.get(withdrawAsset.assetId) ?? 0n) > 0n && !collateralToDebt) {
    return new Prices(Dictionary.empty<bigint, bigint>(), Cell.EMPTY);
}

Workaround: Always check if price data is empty before proceeding with withdrawal calculations.

Limitation: Collateral-to-Debt Mode Constraint

The system prevents debt-only operations on a single supplied asset:

if (collateralToDebt && assets.length == 1) {
    throw new Error("Cannot debt only one supplied asset");
}

Workaround: Ensure at least two assets are supplied before enabling collateral-to-debt mode.

Limitation: Median Price Calculation Edge Cases

When an even number of price points exist, the median is calculated as the average of the two middle values, which may not be representable as an exact bigint.

Workaround: Accept minor precision differences in edge cases where exact median calculation isn't possible with integer arithmetic.

Section sources

  • PricesCollector.ts
  • utils.ts

Referenced Files in This Document

  • PricesCollector.ts
  • utils.ts
  • math.ts
  • Backend.ts
  • PriceCollector.test.ts
  • health_factor_calculation_test.ts