Auto-Rebalance
Cron
UNISWAP_AUTO_REBALANCEmonitors Uniswap V4 positions and rebalances when the pool price diverges from the oracle price.
Overview
The auto-rebalance cron compares the current Uniswap V4 pool price against the Twin oracle price. When the deviation exceeds a configurable threshold and the oracle price falls outside the position's range, it triggers a rebalance: remove all liquidity, then mint a new position centered on the oracle price.
Bot Parameters
Each Uniswap wallet has configurable bot parameters:
| Parameter | Type | Description |
|---|---|---|
enabled | boolean | Whether auto-rebalance is active for this wallet |
priceBandPercent | number | Range width: oracle price +/- N% (e.g. 5 = +/-5%) |
rebalanceTriggerPercent | number | Deviation threshold to trigger rebalance (e.g. 3 = 3%) |
maxBudgetToken0 | string | Max amount of token0 (USDC) to deploy (base units) |
maxBudgetToken1 | string | Max amount of token1 (ARGT) to deploy (base units) |
Cron Decision Flowchart
Decision Logic Summary
Rebalance triggers when ALL of these are true:
deviation > rebalanceTriggerPercent(ORliquidity = 0for recovery)- Oracle price is outside the current position's tick range
- No pending transaction exists for this wallet (concurrent guard)
Rebalance Sequence
Six Guards
The auto-rebalance has six protective mechanisms:
| # | Guard | Purpose |
|---|---|---|
| 1 | Oracle-in-range | Skip rebalance if oracle price is still within position's tick range, even if pool price has deviated. Prevents unnecessary churn. |
| 2 | Liquidity = 0 bypass | If position has zero liquidity (failed remove, manual intervention), force rebalance without the deviation check. Recovery mode. |
| 3 | Concurrent execution guard | Check for pending TX on this wallet before triggering. Prevents overlapping on-chain operations that would cause nonce conflicts. |
| 4 | Same-RPC balance read | After remove TX, read balances from the same confirmed RPC used for the receipt. Prevents reading stale balances from a different RPC that hasn't indexed the block yet. |
| 5 | maxBudget as cap | amount = min(walletBalance, maxBudget). The maxBudget is a risk cap, not a fixed amount. If balance < maxBudget, use everything available. If balance > maxBudget, cap at maxBudget. Prevents TRANSFER_FROM_FAILED when maxBudget exceeds actual balance after remove. |
| 6 | No poolState fallback | Uses live getPoolState for sqrtPriceX96 instead of a cached/computed value. The pool state may have changed between remove and mint due to other traders. |
Balance Handling Detail
Without this pattern, if maxBudgetToken0 = 1000 USDC but the wallet only has 800 USDC after the remove, the mint TX would revert with TRANSFER_FROM_FAILED.
Tick Calculation
New position ticks are calculated from the oracle price with the configured band:
Code
Example with oracle price = 1466.5 ARGT/USDC, band = 5%:
Code
Troubleshooting
| Symptom | Cause | Solution |
|---|---|---|
| Rebalance never triggers | rebalanceTriggerPercent too high or enabled: false | Lower trigger percent, verify bot is enabled |
| "Calculated liquidity is zero" | Both token balances are 0 after remove, or price completely out of range with insufficient single-side token | Check wallet balances, verify remove TX completed |
TRANSFER_FROM_FAILED on mint | Approval amounts don't include slippage buffer | Verify slippage: amount * (10000 + bps) / 10000 on both desired amounts and approvals |
| Rebalance skipped: "oracle in range" | Oracle price is within position ticks despite pool price deviation | Expected behavior -- oracle is a better price reference than pool. Wait for oracle to move or adjust position manually |
| Rebalance skipped: "pending TX" | Previous rebalance still in progress | Wait for pending TX to complete or fail. Check waitForReceipt timeout (20 x 5s = 100s) |
| Stale balances after remove | Reading balance from different RPC than receipt confirmation | Bug -- must use same confirmedRpcUrl. Check waitForReceipt return value |
| Position has 0 liquidity but no rebalance | Wallet missing BitGo fields (bitgoWalletId, bitgoCoin, passphraseEncrypted) | Update wallet config with BitGo credentials |
| Nonce conflict / duplicate TX | Concurrent execution guard failed | Check for race condition in DB pending TX query. Ensure markJobAsRunning is set |
| Wrong tick alignment (~1.7% off) | Using floor for both ticks | Upper tick must use Math.ceil (alignTickToSpacingCeil) |
| Asymmetric deposit ratio | Expected -- concentrated liquidity ratio depends on price position within range | Not a bug. The ratio only equals spot price at range midpoint |
Testing
To test the auto-rebalance without waiting for natural price divergence:
- Temporarily lower
rebalanceTriggerPercentto a small value (e.g.0.1) - Go to Cron Jobs in dashboard
- Execute
UNISWAP_AUTO_REBALANCEon-demand - Monitor the TX in the Uniswap pools page
- Restore
rebalanceTriggerPercentto production value
Cron Configuration
| Field | Value |
|---|---|
| Key | UNISWAP_AUTO_REBALANCE |
| Registered in | cron-keys.ts, manager.ts, cron-handlers.ts |
| Wallet filter | bot.enabled AND positionNftId AND bitgoWalletId AND bitgoCoin AND passphraseEncrypted |
| Error behavior | Sets lastError on job, auto-clears on next success |