Summary
check_rebalance_needed computes each asset's current portfolio weight using sequential integer division:
// contracts/src/lib.rs
let current_asset_value = (current_balance * price_data.price) / 10i128.pow(14);
let current_percent = (current_asset_value * 100) / total_value;
let drift = (current_percent as i128 - target_percent as i128).abs();
if drift > portfolio.rebalance_threshold as i128 {
return true;
}
Both intermediate divisions truncate toward zero before accumulating further computation, producing systematic rounding error that scales with the number of assets and their relative magnitudes.
Concrete Example
Portfolio: 3 assets, target 33%/33%/34%, threshold = 2%.
| Asset |
Balance |
Price (scaled ×10^7) |
Actual value |
current_asset_value (after /10^14) |
current_percent (after ×100/total) |
| XLM |
10_000_000 |
3_540_000 |
35.40 |
0 (lost in division) |
0 |
| USDC |
10_000_000 |
10_000_000 |
100.00 |
1 |
100% |
| BTC |
100 |
1_100_000_000_000 |
110_000.00 |
110_000 |
~100% |
XLM's entire value is zeroed out by integer truncation (10_000_000 × 3_540_000 = 3.54×10^13, divided by 10^14 = 0). The contract then reports XLM drift of 33% — a phantom rebalance trigger every single check, even when the portfolio is perfectly balanced.
Impact
- Phantom rebalance storms: XLM-heavy portfolios trigger
execute_rebalance every cooldown cycle due to computed drift of ~33% when true drift is 0%.
- Missed real drift: Conversely, assets with large balances but small prices can have their drift absorbed into rounding noise, suppressing legitimate rebalances.
- Cooldown exploitation: Phantom triggers burn the 3600-second cooldown, preventing real rebalances when the market actually drifts.
Root Cause
The Reflector oracle scales prices by 10^7. Balances stored as raw token amounts (typically 7-decimal Stellar tokens = 10^7 scale). current_balance × price = 10^7 × 10^7 = 10^14, which survives division by 10^14 — but only for assets with balance ≥ 10^7. Sub-unit balances are zeroed.
The 10^14 divisor hardcoding does not account for assets with different decimal precisions.
Suggested Fix
Defer all division to the final percentage comparison and use basis points (10_000) instead of percent (100) to preserve 2 decimal places of drift resolution:
// Compute in basis points (1 bp = 0.01%) to avoid early truncation
let current_asset_value_scaled = current_balance * price_data.price; // keep full precision
let current_bp = (current_asset_value_scaled * 10_000) / (total_value_scaled);
let target_bp = target_percent as i128 * 100; // convert % to bp
let drift_bp = (current_bp - target_bp).abs();
if drift_bp > portfolio.rebalance_threshold as i128 * 100 {
return true;
}
Also parameterize the price scale per-asset rather than hardcoding 10^14.
References
contracts/src/lib.rs — check_rebalance_needed
backend/src/services/reflector.ts — REFLECTOR_PRICE_SCALE = 1e7
Severity: High — silent precision loss triggers incorrect rebalances on every cycle for common asset combinations
Summary
check_rebalance_neededcomputes each asset's current portfolio weight using sequential integer division:Both intermediate divisions truncate toward zero before accumulating further computation, producing systematic rounding error that scales with the number of assets and their relative magnitudes.
Concrete Example
Portfolio: 3 assets, target 33%/33%/34%, threshold = 2%.
current_asset_value(after /10^14)current_percent(after ×100/total)XLM's entire value is zeroed out by integer truncation (
10_000_000 × 3_540_000 = 3.54×10^13, divided by10^14 = 0). The contract then reports XLM drift of 33% — a phantom rebalance trigger every single check, even when the portfolio is perfectly balanced.Impact
execute_rebalanceevery cooldown cycle due to computed drift of ~33% when true drift is 0%.Root Cause
The Reflector oracle scales prices by
10^7. Balances stored as raw token amounts (typically 7-decimal Stellar tokens =10^7scale).current_balance × price = 10^7 × 10^7 = 10^14, which survives division by10^14— but only for assets with balance ≥ 10^7. Sub-unit balances are zeroed.The
10^14divisor hardcoding does not account for assets with different decimal precisions.Suggested Fix
Defer all division to the final percentage comparison and use basis points (10_000) instead of percent (100) to preserve 2 decimal places of drift resolution:
Also parameterize the price scale per-asset rather than hardcoding
10^14.References
contracts/src/lib.rs—check_rebalance_neededbackend/src/services/reflector.ts—REFLECTOR_PRICE_SCALE = 1e7Severity: High — silent precision loss triggers incorrect rebalances on every cycle for common asset combinations