- Sync Errors and User State Desynchronization
- Price Collection Failures
- Transaction Revert Scenarios
- Health Factor Miscalculations
- Diagnostic Procedures
- Error Handling Code Examples
- Mitigation Strategies
- Known Limitations and Workarounds
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]
Diagram sources
- PricesCollector.ts
Section sources
- PricesCollector.ts
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
Diagram sources
- utils.ts
- PricesCollector.ts
Section sources
- utils.ts
- PricesCollector.ts
Transaction reverts can occur due to several conditions:
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.
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.
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]
Diagram sources
- math.ts
- utils.ts
Section sources
- math.ts
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]
Diagram sources
- math.ts
Section sources
- math.ts
Effective diagnostics require systematic analysis of logs and state inspection.
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 lenPrice source errorsign is valid:
Verify critical state elements:
- Principal dictionary: Ensure all keys match pool assets
- Price dictionary: Confirm all required assets have price entries
- Timestamp validation: Check that all prices are within TTL window
- 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
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;
}
}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
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
}
}
}Configure multiple redundant sources:
const sources: PriceSourcesConfig = {
backendEndpoints: [],
icpEndpoints: DefaultPriceSourcesConfig.icpEndpoints,
};
const collector = new PricesCollector({
...config,
sourcesConfig: sources,
additionalPriceSources: [new FakeBackendPriceSource('', ORACLES_MAINNET)],
});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
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.
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.
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