> ## Documentation Index
> Fetch the complete documentation index at: https://docs.symmetry.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Rebalancing

> Deposits, withdrawals, vault rebalances, the auction flow, and flash swaps.

Rebalancing is the mechanism by which vaults process deposits, withdrawals, and periodic rebalances to maintain target weights. All three operations use the same rebalance intent flow.

## Rebalance Types

| Type        | Enum | Description                                          |
| ----------- | ---- | ---------------------------------------------------- |
| Deposit     | 0    | User deposits tokens → receives vault tokens         |
| Withdraw    | 1    | User burns vault tokens → receives underlying tokens |
| Vault       | 2    | Keeper-initiated rebalance to restore target weights |
| VaultCustom | 3    | Custom vault rebalance                               |

## Lifecycle

Each rebalance type follows a similar on-chain flow, but with type-specific stages:

**Deposit flow:**

```
[Create] → [Deposit Tokens] → [Lock Deposits] → [Update Prices]
  → [Auction 1] → [Auction 2] → [Auction 3]
  → [Mint] → [Claim Bounty] → [Close Account]
```

**Withdraw flow:**

```
[Create] → [Update Prices]
  → [Auction 1] → [Auction 2] → [Auction 3]
  → [Redeem] → [Claim Bounty] → [Close Account]
```

**Vault / VaultCustom flow:**

```
[Create] → [Update Prices]
  → [Auction 1] → [Auction 2] → [Auction 3]
  → [Claim Bounty] → [Close Account]
```

| Action           | Enum | Description                                 |
| ---------------- | ---- | ------------------------------------------- |
| `not_active`     | 0    | Intent not yet initialized                  |
| `deposit_tokens` | 1    | User is depositing tokens (deposits only)   |
| `update_prices`  | 3    | Oracle prices need to be refreshed          |
| `auction`        | 4    | Auction phase — keepers execute flash swaps |

<Note>
  Mint, redeem, and claim bounty are terminal task stages — they do not appear as separate `current_action` enum values but are handled after the auction phase completes.
</Note>

### Obtaining a Rebalance Intent Key

Many SDK methods require a `rebalance_intent` pubkey. You can obtain it in two ways:

1. **Deterministic PDA** — The rebalance intent address is derived from `[REBALANCE_INTENT_SEED, vault_key, owner_key]`. Use this when you know both the vault and the owner.
2. **Fetch from on-chain** — Query active intents:

```typescript theme={null}
const ownerIntents = await sdk.fetchOwnerRebalanceIntents("<OWNER_PUBKEY>");
const vaultIntents = await sdk.fetchVaultRebalanceIntents("<VAULT_PUBKEY>");
```

## Deposits

<Warning>
  Each user can have only **one** active deposit or withdrawal per vault at a time. Starting a new deposit while a previous one is still processing will fail because the rebalance intent account (a PDA derived from the vault and user addresses) already exists.
</Warning>

### Step 1: Create Deposit Intent and Deposit Tokens

```typescript theme={null}
// Amounts are raw (smallest units): SOL = 9 decimals, USDC = 6 decimals
const tx: TxPayloadBatchSequence = await sdk.buyVaultTx({
  buyer: wallet.publicKey.toBase58(),
  vault_mint: "<VAULT_TOKEN_MINT>",
  contributions: [
    { mint: "So11111111111111111111111111111111111111112", amount: 1_000_000_000 },   // 1 SOL
    { mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", amount: 100_000_000 }, // 100 USDC
  ],
  rebalance_slippage_bps: 100,
  per_trade_rebalance_slippage_bps: 100,
});

await sdk.signAndSendTxPayloadBatchSequence({
  txPayloadBatchSequence: tx,
  wallet,
});
```

This deposits the contributed tokens and starts the deposit process.

The `contributions` array specifies which tokens to deposit and how much. **All amounts are raw values in the token's smallest unit** (e.g., lamports for SOL, 10^6 units for USDC). Users can contribute any supported token mints (including Token Extensions/Token22) — they do NOT need to match the vault's composition. The rebalance auction will swap tokens as needed.

The `vault_mint` parameter is the vault's token mint address (not the vault account address).

### Step 2 (Optional): Deposit More Tokens

Before locking, users can add more contributions:

```typescript theme={null}
const tx = await sdk.depositTokensTx({
  buyer: wallet.publicKey.toBase58(),
  contributions: [{ mint: "<MINT>", amount: 500_000_000 }],
  rebalance_intent: "<REBALANCE_INTENT_PUBKEY>",
});
```

### Step 3: Lock Deposits

Locking freezes contributions and starts the rebalance process:

```typescript theme={null}
const tx = await sdk.lockDepositsTx({
  buyer: wallet.publicKey.toBase58(),
  vault_mint: "<VAULT_TOKEN_MINT>",
});
```

After locking, the intent moves to `update_prices`. From here, keepers typically take over — but users can execute any of these steps themselves using the same SDK methods.

### Steps 4–7: Processing

These steps are typically handled by keepers automatically, but any user can call these methods directly:

1. **Update Prices** — `updateTokenPricesTx` refreshes oracle prices.
2. **Auctions** — Three auction windows where flash swaps are executed via `flashSwapTx`.
3. **Mint** — `mintTx` mints vault tokens to the depositor.
4. **Claim Bounty** — `claimBountyTx` distributes rewards and closes the account.

Any wallet may execute these stage methods directly; keeper is an actor role, not a privilege role.

<Note>
  After vault tokens are minted, any deposited tokens not consumed during minting are returned to the depositor. This happens automatically: the rebalance intent transitions to a withdrawal phase. Since the depositor's contributed tokens are marked as "keep tokens," the return skips the auction entirely and proceeds directly to redemption.
</Note>

## Withdrawals

```typescript theme={null}
const tx: TxPayloadBatchSequence = await sdk.sellVaultTx({
  seller: wallet.publicKey.toBase58(),
  vault_mint: "<VAULT_TOKEN_MINT>",
  withdraw_amount: 1_000_000,     // raw vault token amount to burn
  keep_tokens: [],
  rebalance_slippage_bps: 100,
  per_trade_rebalance_slippage_bps: 100,
});
```

<Note>
  When the vault has **performance fees** configured (any of host/creator/managers performance fee > 0), the withdrawal follows a deferred path:

  1. The vault token burn amount and fee calculations are recorded but token amounts are **not** immediately subtracted from the vault.
  2. The vault's `active_withdraws` counter is incremented.
  3. During the `updateTokenPricesTx` step, performance fees are calculated, token amounts are subtracted from the vault, and `supply_outstanding` is adjusted.

  This means withdrawals in vaults with performance fees cannot proceed concurrently with pending management intents (`active_managements` must be 0).
</Note>

### `keep_tokens` Behavior

The `keep_tokens` parameter controls which tokens the user receives directly without going through auctions:

* **Empty (`[]`)** — Full rebalance flow. Keepers update prices, run 3 auction windows to swap tokens toward a single output, then redeem. This is the slowest path.
* **Partial (`[mint1, mint2]`)** — The specified tokens are sent directly to the user. The remaining tokens still go through price updates and auctions.
* **All vault mints** — **Fast withdrawal.** When every token in the vault's composition (including `active: false` slots) is passed, the protocol skips price updates and auctions entirely. The intent goes directly to the redeem stage. The user receives their proportional share of each underlying token. This still requires two transactions: `sellVaultTx` to create the intent, then `redeemTokensTx` to transfer the tokens. The user can call `redeemTokensTx` themselves — no need to wait for a keeper.

### Fast Withdrawal

When every token in the vault's composition is passed in `keep_tokens`, the protocol skips price updates and auctions. The intent goes directly to the redeem stage. This is two transactions: `sellVaultTx` then `redeemTokensTx`.

```typescript theme={null}
const vault: Vault = await sdk.fetchVault("<VAULT_PUBKEY>");
const allMints: string[] = vault.formatted!.composition.map(asset => asset.mint);

const sellTx: TxPayloadBatchSequence = await sdk.sellVaultTx({
  seller: wallet.publicKey.toBase58(),
  vault_mint: vault.formatted!.mint,
  withdraw_amount: 1_000_000,
  keep_tokens: allMints,
  rebalance_slippage_bps: 100,
});
await sdk.signAndSendTxPayloadBatchSequence({ txPayloadBatchSequence: sellTx, wallet });

const redeemTx: TxPayloadBatchSequence = await sdk.redeemTokensTx({
  keeper: wallet.publicKey.toBase58(),
  rebalance_intent: "<REBALANCE_INTENT_PUBKEY>",
});
await sdk.signAndSendTxPayloadBatchSequence({ txPayloadBatchSequence: redeemTx, wallet });
```

<Note>
  The `keeper` parameter in `redeemTokensTx` is just the signing wallet — any user can call this directly.
</Note>

### Standard Withdrawal Flow

After creating the withdrawal intent, keepers typically process it — but users can also execute each step themselves:

1. **Update Prices** (`updateTokenPricesTx`)
2. **Auctions** (`flashSwapTx`)
3. **Redeem Tokens** (`redeemTokensTx`)
4. **Claim Bounty** (`claimBountyTx`)

Any wallet may execute these stage methods directly; keeper is an actor role, not a privilege role.

## Vault Rebalance (Keeper-Initiated)

Keepers can trigger a rebalance to bring the vault back to its target weights. A rebalance is only allowed when **all** of the following conditions are met:

1. **Automation is enabled** — `automation_settings.enabled` must be `true` (configured via `editAutomationTx`).
2. **No active rebalance** — the vault must not already have a deposit, withdrawal, or rebalance in progress.
3. **Bounty balance** — the vault must have bounty funds to pay the keeper (added via `addBountyTx`).
4. **Within the automation window** — the current time must fall within the vault's schedule automation window (`automation_start` to `automation_end` within the cycle). See [Schedule & Cycles](/concepts/vaults#schedule--cycles) for details.
5. **Cooldown elapsed** — enough time must have passed since the last automated rebalance (`rebalance_activation_cooldown` seconds).
6. **Threshold exceeded** — at least one **non-bounty** token must have drifted beyond **both** the absolute and relative deviation thresholds. The bounty token (typically WSOL) is excluded from this check.
   * `rebalance_activation_threshold_abs_bps` — minimum deviation as a percentage of total vault value (e.g., 500 = 5%). Computed as `|actual_value - target_value| / vault_tvl`.
   * `rebalance_activation_threshold_rel_bps` — minimum deviation relative to the token's own target (e.g., 1000 = 10%). Computed as `|actual_value - target_value| / max(actual_value, target_value)`.

The `isRebalanceRequired()` utility checks all of these conditions:

```typescript theme={null}
import { isRebalanceRequired } from "@symmetry-hq/sdk";

const needed: boolean = await isRebalanceRequired(vault, connection);

if (needed) {
  const tx: TxPayloadBatchSequence = await sdk.rebalanceVaultTx({
    keeper: wallet.publicKey.toBase58(),
    vault_mint: "<VAULT_TOKEN_MINT>",
    rebalance_slippage_bps: 100,            // 1% overall slippage tolerance
    per_trade_rebalance_slippage_bps: 100,  // 1% per-trade slippage
  });

  await sdk.signAndSendTxPayloadBatchSequence({
    txPayloadBatchSequence: tx,
    wallet,
  });
}
```

These thresholds are configured via `editAutomationTx`:

```typescript theme={null}
const tx = await sdk.editAutomationTx(
  { vault: "<VAULT_PUBKEY>", manager: wallet.publicKey.toBase58() },
  {
    enabled: true,
    rebalance_slippage_threshold_bps: 100,              // 1% max slippage during auctions
    per_trade_rebalance_slippage_threshold_bps: 100,     // 1% per-trade slippage
    rebalance_activation_threshold_abs_bps: 500,         // 5% of TVL absolute deviation triggers rebalance
    rebalance_activation_threshold_rel_bps: 1000,        // 10% relative deviation triggers rebalance
    rebalance_activation_cooldown: 3600,                 // minimum 1 hour between rebalances
    modification_delay: 86400,                           // 24h delay for future automation changes
  }
);
```

## Auction System

After price updates, the rebalance enters **three sequential auction stages**. Each stage has a fixed duration configured in the protocol's global config (`rebalance_auction_1_timeframe`, `rebalance_auction_2_timeframe`, `rebalance_auction_3_timeframe`). During each stage, keepers can execute flash swaps to move the vault toward its target weights.

### Auction Pricing

The vault uses **oracle confidence bands** to create a Dutch-auction-style pricing curve that crosses through fair value, incentivizing timely execution:

* **At auction start**: The vault sells tokens at `price + confidence` (expensive for keepers) and buys tokens at `price - confidence` (cheap for keepers). This is the widest unfavorable spread.
* **Over time**: The spread narrows linearly toward mid-price. The price delta follows: `conf × 2 × time_elapsed / auction_duration`.
* **At the midpoint**: Both buy and sell prices converge to the oracle mid-price. The spread is zero.
* **After the midpoint**: The prices cross through mid-price and the vault begins offering better-than-market rates — selling below mid-price and buying above mid-price. This creates increasing profit opportunity for keepers.
* **At auction end**: Sell price reaches `price - confidence` and buy price reaches `price + confidence` — the maximum favorable spread for keepers.

This crossing mechanism means keepers face a trade-off: executing early gets worse pricing but less competition; waiting past the midpoint gets better pricing but risks another keeper taking the swap first. Each new auction stage resets this convergence.

### After Auctions End

Once all three auction stages complete:

* **Deposits**: Vault tokens are minted to the depositor via `mintTx`.
* **Withdrawals**: Underlying tokens are sent to the withdrawer via `redeemTokensTx`.
* **Vault rebalances**: The rebalance completes directly.

After minting or redeeming is complete, the bounty is claimed via `claimBountyTx`, which distributes bounty rewards to all keepers who completed tasks during the rebalance (proportional to the tasks they performed), awards a task bounty to the caller, returns unused bounty and the bounty bond to the depositor, and closes the rebalance intent account.

### Flash Swaps

A flash swap is an atomic operation within a single transaction:

1. **Flash Withdraw** — Tokens are withdrawn from the vault to the keeper.
2. **Jupiter Swap** — Keeper swaps the tokens via Jupiter (or any other DEX).
3. **Flash Deposit** — Keeper deposits the swapped tokens back into the vault.

The vault gives the keeper `mint_out` tokens and expects `mint_in` tokens back. The keeper profits from any spread between the vault's auction price and the market price.

```typescript theme={null}
import { getSwapPairs, getJupTokenLedgerAndSwapInstructions } from "@symmetry-hq/sdk";

const pairs: SwapPair[] = getSwapPairs(rebalanceIntent.chain_data, vault);

for (const pair of pairs) {
  const jupResult = await getJupTokenLedgerAndSwapInstructions({
    keeper: wallet.publicKey,
    vaultMintIn: new PublicKey(pair.inMint),
    vaultMintOut: new PublicKey(pair.outMint),
    vaultAmountIn: pair.inAmount,
    vaultAmountOut: pair.outAmount,
    swapMode: "ioc",
    apiKey: "<JUP_API_KEY>",
    maxJupAccounts: 64,
  });

  const tx: TxPayloadBatchSequence = await sdk.flashSwapTx({
    keeper: wallet.publicKey.toBase58(),
    vault: "<VAULT_PUBKEY>",
    rebalance_intent: "<REBALANCE_INTENT_PUBKEY>",
    mint_in: pair.inMint,       // token the vault receives
    mint_out: pair.outMint,     // token the vault gives
    amount_in: pair.inAmount,   // raw amount deposited to vault
    amount_out: pair.outAmount, // raw amount withdrawn from vault
    mode: 2,                    // IOC (immediate-or-cancel)
    jup_token_ledger_ix: jupResult.tokenLedgerInstruction,
    jup_swap_ix: jupResult.swapInstruction,
    jup_address_lookup_table_addresses: jupResult.addressLookupTableAddresses,
  });

  await sdk.signAndSendTxPayloadBatchSequence({
    txPayloadBatchSequence: tx,
    wallet,
  });
}
```

### Flash Swap Parameters

| Parameter                            | Description                                                  |
| ------------------------------------ | ------------------------------------------------------------ |
| `keeper`                             | Keeper's public key                                          |
| `vault`                              | Vault account public key                                     |
| `rebalance_intent`                   | Rebalance intent public key (for rebalance swaps)            |
| `intent`                             | Intent public key (for direct swap intents)                  |
| `mint_in`                            | Token the vault receives (keeper deposits this)              |
| `mint_out`                           | Token the vault gives (keeper receives this)                 |
| `amount_in`                          | Amount of `mint_in` to deposit                               |
| `amount_out`                         | Amount of `mint_out` to withdraw                             |
| `mode`                               | 0 = exact\_in, 1 = exact\_out, 2 = IOC (immediate-or-cancel) |
| `jup_token_ledger_ix`                | Optional: Jupiter token ledger instruction                   |
| `jup_swap_ix`                        | Optional: Jupiter swap instruction                           |
| `jup_address_lookup_table_addresses` | Optional: Jupiter ALT addresses                              |

### Swap Pairs

`getSwapPairs(rebalanceIntent, vault)` computes all valid swap pairs from the current auction state:

```typescript theme={null}
{
  inMint: string;
  outMint: string;
  inAmount: number;
  outAmount: number;
  value: number;
}
```

Pairs with `value < 0.005` are typically skipped as not worth the transaction cost.

## Minting Vault Tokens

After auction windows close for a deposit rebalance:

```typescript theme={null}
const tx: TxPayloadBatchSequence = await sdk.mintTx({
  keeper: wallet.publicKey.toBase58(),  // signer — any wallet can call this
  rebalance_intent: "<REBALANCE_INTENT_PUBKEY>",
});
```

This mints vault tokens to the depositor proportional to the value of their contribution minus fees.

## Redeeming Tokens

After auction windows close for a withdrawal rebalance:

```typescript theme={null}
const tx: TxPayloadBatchSequence = await sdk.redeemTokensTx({
  keeper: wallet.publicKey.toBase58(),  // signer — any wallet can call this
  rebalance_intent: "<REBALANCE_INTENT_PUBKEY>",
});
```

This sends the underlying tokens to the withdrawer.

<Warning>
  The withdrawer must have existing Associated Token Accounts (ATAs) for all tokens being redeemed. If an ATA doesn't exist for a token and the transaction is submitted by a keeper (not the withdrawer themselves), that token will be **skipped** during redemption. Create ATAs for all expected tokens before the redemption step.
</Warning>

## Claiming Bounty

After minting or redeeming is complete:

```typescript theme={null}
const tx: TxPayloadBatchSequence = await sdk.claimBountyTx({
  keeper: wallet.publicKey.toBase58(),  // signer — any wallet can call this
  rebalance_intent: "<REBALANCE_INTENT_PUBKEY>",
});
```

This distributes earned bounties to all keepers who completed tasks during the rebalance, awards a task bounty to the caller, returns unused bounty to the depositor, and closes the rebalance intent account.

## Price Updates During Rebalance

Keepers must update oracle prices before the auction can begin:

```typescript theme={null}
const tx = await sdk.updateTokenPricesTx({
  keeper: wallet.publicKey.toBase58(),
  vault: "<VAULT_PUBKEY>",
  rebalance_intent: "<REBALANCE_INTENT_PUBKEY>",
});
```

This handles all the complexity of Pyth VAA creation, verification, and feed updates automatically. The batch layout:

1. Create and initialize Pyth VAAs
2. Write and verify VAAs
3. Update individual price feeds
4. Update token prices in the vault + close VAA accounts

### Standalone Pyth Price Update

To update Pyth prices without a rebalance context:

```typescript theme={null}
const tx: TxPayloadBatchSequence = await sdk.updatePythPriceFeedsTx({
  keeper: wallet.publicKey.toBase58(),
  accounts: ["<PYTH_PRICE_ACCOUNT_1>", "<PYTH_PRICE_ACCOUNT_2>"],
});
```

## Cancelling a Rebalance

```typescript theme={null}
const tx = await sdk.cancelRebalanceIntentTx({
  keeper: wallet.publicKey.toBase58(),
  rebalance_intent: "<REBALANCE_INTENT_PUBKEY>",
});
```

## Fetching Rebalance Intents

```typescript theme={null}
const ri = await sdk.fetchRebalanceIntent("<PUBKEY>");

const riMap = await sdk.fetchMultipleRebalanceIntents(["<PUBKEY_1>", "<PUBKEY_2>"]);

const all = await sdk.fetchAllRebalanceIntents();
const byOwner = await sdk.fetchOwnerRebalanceIntents("<OWNER_PUBKEY>");
const byVault = await sdk.fetchVaultRebalanceIntents("<VAULT_PUBKEY>");
```

## Checking If Rebalance Is Needed

```typescript theme={null}
import { isRebalanceRequired } from "@symmetry-hq/sdk";

const needed = await isRebalanceRequired(vault, connection);
// Returns true when all conditions are met: automation enabled, no active rebalance,
// bounty available, within schedule window, cooldown elapsed, and threshold exceeded.
```

## Concurrent Operations

Deposits and withdrawals can proceed while a vault rebalance is in progress. When a deposit mints tokens or a withdrawal redeems tokens during an active vault rebalance, the vault rebalance intent's token amounts and target amounts are automatically updated to reflect the changed vault composition. This prevents stale data in the rebalance auction.

However, vault rebalances cannot run concurrently with each other (only one vault rebalance can exist at a time), and they require `active_managements == 0` (no pending configuration changes).

## Rebalance Intent Data Structure

```typescript theme={null}
interface UIRebalanceIntent {
  rebalance_type: "deposit" | "withdraw" | "vault" | "vault_custom";
  deposit_data: DepositData | null;
  price_updates_data: PriceUpdatesData | null;
  auction_data: AuctionData | null;
  mint_data: MintData | null;
  redeem_data: RedeemData | null;
  claim_bounty_data: ClaimBountyData | null;
  formatted_data: FormattedRebalanceIntent;
  chain_data: RebalanceIntent;
}
```
