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

# Advanced rebalancing

> Compare atomic and staged rebalancing strategies.

## Overview

Shield Swap supports two core strategies: **atomic rebalancing**, which makes withdrawal and replacement succeed or fail together, and **staged rebalancing**, which separates the transactions and lets you set bounds on withdrawal and deposit amounts. Atomic rebalancing suits situations where keeping the old position until its replacement succeeds matters most. Staged rebalancing gives you more tolerance for changing amounts in active pools, provided you can manage the exposure between transactions or use reserves to fund the replacement.

### Atomic rebalance: one transaction

An atomic rebalance combines the withdrawal and replacement in a single call to the `shield_swap_rebalance_router.aleo` router contract. It recovers the old position's principal and fees and uses them to open the new position in the same pool. If the replacement cannot be completed, the old position stays intact. The current implementation requires the recovered amounts and replacement deposits to match your plan exactly.

![One transaction: the rebalance router calls the core contract to withdraw, settle and close, then mint. Acceptance replaces the old position; rejection leaves it intact. Exact recovery and deposit amounts must match the plan.](https://mintcdn.com/shield-swap-documentation/3PZYuGjR2S0lmUXj/images/rebalance-atomic.svg?fit=max\&auto=format\&n=3PZYuGjR2S0lmUXj\&q=85\&s=b22146bdc33660ce98cb249fedc01d08)

**Use when:**

* You want to keep the old position active until its replacement succeeds.
* You can refresh the plan and retry when price changes or accrued fees invalidate its amounts.

### Staged rebalance: separate transactions

A staged rebalance separates the process into decrease, collect, and mint transactions. You confirm the withdrawal and collection before using the resulting balances to plan the replacement, or fund the new position from reserves. Withdrawal and minting accept minimum token amounts, so you can allow the token mix to change within your bounds. Each transaction completes independently, though: a failed mint does not undo a completed withdrawal. These operations use the `shield_swap.aleo` core contract, with the `shield_swap_lp_router.aleo` router handling wrapped-asset collection and minting.

![Three staged paths: decrease, collect, then mint using the proceeds; decrease, then mint from reserves and collect independently; or mint from reserves before decreasing and collecting the old position. Reusing capital leaves a gap; minting first creates temporary overlap. Decrease calls core; wrapped collect and mint use the LP router. A later failure does not undo earlier accepted steps.](https://mintcdn.com/shield-swap-documentation/3PZYuGjR2S0lmUXj/images/rebalance-staged.svg?fit=max\&auto=format\&n=3PZYuGjR2S0lmUXj\&q=85\&s=8604c31bfbb1c8231a19e6715256ec9d)

**Use when:**

* You need amount tolerance because pool activity makes exact amounts difficult to match.
* High-volume trading is pushing your position toward an unwanted one-sided balance. Decrease first so withdrawing the old liquidity does not depend on minting its replacement.
* You can accept a gap between positions, or use reserves to fund overlapping positions.

## Comparing the Two Strategies

Choose based on what must succeed together, the amount changes you can accept, and the capital available between transactions.

| Tradeoff | Atomic rebalance | Staged rebalance |
| - | - | - |
| Execution guarantee | Removing the old position and opening its replacement succeed or fail together. | Each transaction completes independently; earlier accepted steps remain completed if a later step fails. |
| Amount tolerance | Recovery and deposits for the target liquidity must match the plan exactly. | Decrease and mint use per-token minimums; collection requests must fit the owed balances. |
| Capital requirements | Reuses the old position's capital within the same transaction, with optional additional funding. | Reuses capital after collection, or uses reserves to open the replacement sooner. |
| Pool activity | Changes in price or accrued fees can invalidate exact amounts and require a fresh plan. | Token amounts can vary within the decrease and mint bounds; inclusion and successful replacement still are not guaranteed. |

### Comparing Exposure by Strategy

Before choosing an execution path, decide what should happen if the new range cannot be opened immediately. Keeping the old position, holding the withdrawn assets, and temporarily running two positions are different portfolio decisions.

| Your constraint | Execution path | Exposure |
| - | - | - |
| Keep the old position unless its replacement succeeds. | Atomic rebalance. | The old position continues operating after rejection. |
| Remove the old range now; reuse its capital for the new range. | Full decrease, collect, then mint. | You hold an owed claim, then spendable assets, with no liquidity earning fees during the gap. |
| Remove the old range now; fund the replacement from reserves. | Full decrease, then mint with separate records; collect the old position independently. | The old claim remains owed while reserves fund the new position. |
| Establish the new range before retiring the old one. | Mint with reserves, then decrease and collect the old position. | Both positions remain deployed until the decrease succeeds. Budget for their combined exposure. |

The last three are variations of the same staged strategy. Spare capital changes the ordering; it does not make the transactions atomic.

A range change does not exchange token0 for token1. If the replacement needs more token1 than you recover, supply token1, reduce the new position's size, or complete a separate swap and claim. A larger token0 balance cannot cover a token1 shortfall.

For a recurring strategy, compare the expected benefit of the new range with execution fees, proving costs, any inventory conversion, and the time your capital spends inactive or doubly deployed. A narrower range can support more liquidity per unit of capital near the current price, but can also require more frequent intervention. Recentring does not erase the inventory change or loss relative to holding that has already occurred.

Continue with [Strategy 1: Atomic Rebalance](#strategy-1-atomic-rebalance) or [Strategy 2: Staged Rebalance](#strategy-2-staged-rebalance) for the sizing, amount checks, and code for that path.

## Strategy 1: Atomic Rebalance

The router removes all old liquidity, settles and collects everything owed, removes the old position, and mints a successor in the same pool. It preserves the owner and withdrawal address, creates a new position ID and NFT, and sends surplus to the withdrawal address. All of those changes succeed or fail together.

### What an atomic rebalance commits to

The most sensitive inputs are `recovered0`, `recovered1`, `mint.amount0_desired`, and `mint.amount1_desired`. They must match the amounts calculated at execution, including rounding. Even a one-base-unit difference can reject the entire replacement.

For each token independently, the contract enforces:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
recovered + funded = amount_desired + refund
amount_min = amount_desired
amount_required_for_liquidity_target = amount_desired
funded = 0 OR refund = 0
```

It also requires the old position to end with zero liquidity and zero owed balances. Understating recovery leaves a balance behind and prevents closure; overstating it exceeds what can be collected.

### Size the replacement from token inventory

Use canonical pool token order and native base units throughout. If token0 has `d0` decimals and token1 has `d1`, the human price of token0 in token1 is the raw price multiplied by `10^(d0 - d1)`.

Let `s` be the unscaled square-root price, `a < b` the old range's square-root bounds, and `L` its liquidity. Clamp the current price to the range with `c = min(max(s, a), b)`. The real-number principal relationships are:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
x(L, s, a, b) = L × (1/c - 1/b)     token0
y(L, s, a, b) = L × (c - a)         token1
```

Token0 decreases as price rises; token1 increases. Outside the range, the inventory is one-sided and constant until price re-enters. The contract uses Q128.128 square-root prices (`S = s × 2^128`), bounded integers, and explicit rounding. Withdrawals round down and deposits round up. Use the SDK's integer helpers for transaction amounts; these real-number equations explain the economics. See [Liquidity formulas](https://shield.fi/docs/math/liquidity-formulas.md) for the fixed-point form.

Your recovered balance includes more than principal:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
R0 = withdrawn_principal0 + stored_owed0 + pending_fees0
R1 = withdrawn_principal1 + stored_owed1 + pending_fees1

pending_fees_i = floor(L × Δfee_growth_inside_i / 2^128)
```

Inside fee growth depends on the global accumulators, both old boundary ticks, and the current tick. Its difference from the position checkpoint uses the protocol's modular arithmetic. Reading only `tokens_owed` misses fees earned since the last checkpoint. Stored owed balances can also contain unused earlier deposits or previously withdrawn principal, so they are not necessarily all fee income.

For new bounds `a'`, `b'` and a target `L'`, calculate the rounded-up deposits `D0`, `D1`. The atomic funding and refunds are:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
funded_i = max(D_i - R_i, 0)
refund_i = max(R_i - D_i, 0)
R_i + funded_i = D_i + refund_i
```

Alternatively, start from per-token budgets `B0`, `B1`. Inside the new range, the real-number sizing relationship is:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
L0 = B0 / (1/s - 1/b')
L1 = B1 / (s - a')
L' = min(L0, L1)
```

Use the one-sided formula outside the range. For an atomic budget plan, `B_i` is recovery plus the maximum additional funding you allow. The planner floors the liquidity solve and checks that rounded-up deposits fit both budgets. Passing zero additional funding for both tokens means that recovery alone must fund the replacement.

### Check the atomic request fields

These are the contract's field names; SDK options may use camelCase. The requirements apply at execution. Passing them in your planning snapshot does not reserve that state.

| Parameter | Requirement at execution |
| - | - |
| `old_liquidity` | Must be positive and equal the old position's entire remaining liquidity. Too little leaves liquidity behind and prevents closure; too much exceeds the position's balance. Swaps alone do not change this value. |
| `recovered0`, `recovered1` | Must equal all withdrawn principal, stored owed balances, and newly accrued fees, separately for each token. Understating either leaves an owed balance; overstating either exceeds what can be collected. Both reject. |
| `liquidity_target` | Must be positive, meet the pool's minimum position liquidity, and fit the remaining capacity of both new boundary ticks after removing the old liquidity. Each boundary's resulting gross liquidity must be at most `max_liquidity_per_tick`. |
| `mint.amount0_desired`, `mint.amount1_desired` | Must exactly equal the deposits required for `liquidity_target` at the execution price and new bounds, rounded up by the contract. Both too little and too much reject. These are exact deposits, not maximum budgets. |
| `mint.amount0_min`, `mint.amount1_min` | Must equal their corresponding desired amounts. Lower minimums do not introduce tolerance. |
| `funded0`, `funded1`, `refund0`, `refund1` | Must satisfy the per-token accounting equations above. Funding and refund cannot both be positive for the same token. The router entrypoint and supplied funding records must match the funded sides. |
| `mint.tick_lower`, `mint.tick_upper` | Must satisfy `-400000 <= tick_lower < tick_upper <= 400000`, with both ticks aligned to the pool's tick spacing. |
| `mint.tick_lower_hint`, `mint.tick_upper_hint` | If a boundary needs insertion, its hint must name a live preceding initialized tick or the lower sentinel in the same pool. At insertion, `hint.tick < new_tick < hint.next` must hold. Plan against the list after the old withdrawal, then insert the new lower tick before resolving the upper hint. |
| `deadline` | Must be at least the execution block height. It is an absolute block height, not a timestamp or a duration. |

**Recompute the amount fields together.** A change in recovery or required deposits also changes funding and refunds. Refreshing only `recovered0/1`, or only the desired deposits, can break the accounting equality. If you size from a budget, refresh the target too. A newly required funding side can also change the router entrypoint and funding records.

**Your own withdrawal can invalidate a hint.** If removing your liquidity leaves an old boundary with zero gross liquidity, that tick leaves the initialized list. It cannot serve as a predecessor unless it is reinserted before the dependent insertion. If other positions still use it, it remains. Recompute hints and boundary capacity after simulating the full decrease; another LP's mint or withdrawal can change them again before execution.

### When atomic rebalances fail

An atomic rebalance fails if any of these conditions holds at execution:

* **Recovery differs from the plan.** Price movement changes withdrawn principal, or owed balances and accrued fees change `recovered0/1`. Even one additional base unit of fees can prevent closure, including when price returns to its earlier value.
* **Deposit or funding amounts do not match.** Desired deposits differ from the rounded amounts required for the target liquidity, minimums differ from desired amounts, or funding and refunds do not balance per token.
* **Position or tick requirements fail.** The request does not remove all old liquidity, the target violates minimum or tick-capacity limits, the new bounds are invalid, or insertion hints are stale. Your own withdrawal can invalidate a hint.
* **Records or identities are invalid.** A required record is spent or insufficient, the owner, pool, tokens, or asset route do not match, or the successor position ID already exists.
* **The deadline passes.** Execution occurs after the supplied block height.

A swap does not automatically cause rejection: price can move if all checked integer amounts remain unchanged and the other checks still pass.

### Check records and routing

Matching the amounts is necessary, but these checks can independently reject the atomic transaction:

* **Position and identity:** use the latest unspent `nft`, with the signer equal to its owner. `mint.pool` must match the old pool, and `assets.token0.token_id` and `assets.token1.token_id` must match the NFT and pool in canonical order. The immutable withdrawal address must be valid; zero and the core contract's own address are rejected.
* **Asset route and funding:** call the matching `shield_swap_rebalance_router.aleo` entrypoint. The core rebalance rejects a direct call. Each `underlying_id` must match the registered plain or wrapped identity. Supply spendable records for every funded side and sufficient fee inputs. A correct amount with the wrong token record or route still fails.
* **Successor identity:** `nonce` and the other committed inputs must produce a new position ID distinct from the old ID and absent from existing positions. Preserve the submitted transaction's ID and resolve its outcome before creating another attempt.

### Execute an atomic attempt

Choose the old position and a spacing-aligned replacement range. Prepare records, transaction fees, program dependencies, and proving keys before reading live state.

| Step | What to do |
| - | - |
| **1. Read state** | Read the pool, price, position, boundary ticks, and fee growth from a consistent state. Repeat if state changes during reads; parallel requests alone do not guarantee consistency. |
| **2. Plan amounts** | Choose a liquidity target or funding budget. Calculate recovery, replacement deposits, funding, and refunds together for each token. |
| **3. Check ticks** | Simulate the full withdrawal, then check boundary capacity and insertion hints. Resolve the new lower boundary before the upper; removed ticks cannot serve as hints until reinserted. |
| **4. Submit** | Authorize, prove, and broadcast promptly. Allow enough blocks for proving and inclusion before the deadline. |

**Then act on the transaction result:**

| Result | Next action |
| - | - |
| **Accepted** | Save the replacement position's ID and NFT. The old position is closed. |
| **Rejected** | The old position stays intact. Fix the failure cause, refresh affected amounts and ticks, and recheck the request before rebuilding. |
| **Pending or unknown** | Resolve the submitted transaction's status before making another attempt. |

The deadline only limits execution to `block_height <= deadline`. It does not relax amount checks or guarantee execution ahead of swaps, even with a priority fee.

#### Code examples

Use the latest releases of the **Typescript SDK** (`@provablehq/shield-swap-sdk` and `@provablehq/veil-aleo-sdk`) or **Python `shield-swap-sdk`**. Follow [Setup](https://shield.fi/docs/trading/configure.md) to configure an authenticated **mainnet** local-account `client` or `dex`. Verify that the target deployment has the rebalance router.

The examples define functions that build and submit transactions when called. Submission can incur fees. Choose the position ID from your owned positions, range ticks from your strategy, and the deadline offset from measured proving and inclusion time. Amounts and liquidity use integers, never floating point.

> [tab] Typescript
>
> Resolve dynamic token dependencies before planning. Both funding budgets are zero, so the new size uses only recovery. This example relies on local-account record selection; a wallet signer must supply the position record and any funded token records.
>
> ```typescript theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> async function rebalanceOnce(
>   positionTokenId: string,
>   tickLower: number,
>   tickUpper: number,
>   deadlineOffsetBlocks: number,
> ) {
>   const position = (await client.getOwnedPositions()).find(
>     (p) => p.positionTokenId === positionTokenId,
>   )
>   if (!position?.state || position.state.liquidity === 0n) {
>     throw new Error("position unavailable or empty")
>   }
>   const [token0, token1] = await Promise.all([
>     client.tokenData(position.token0Id),
>     client.tokenData(position.token1Id),
>   ])
>   if (!token0.ammTokenProgram || !token1.ammTokenProgram) {
>     throw new Error("AMM token programs unavailable")
>   }
>   const imports = await client.resolveDexImports({
>     tokenPrograms: [token0.ammTokenProgram, token1.ammTokenProgram],
>   })
>   const plan = await client.planRebalance({
>     poolKey: position.poolKey,
>     positionTokenId,
>     tickLower,
>     tickUpper,
>     maxFunding0: 0n,
>     maxFunding1: 0n,
>   })
>   const result = await client.rebalancePosition({
>     ...plan,
>     imports,
>     deadlineOffsetBlocks,
>   })
>   return result // transactionId, optional positionTokenId, submitted plan
> }
> ```
>
> `planRebalance` also accepts pre-read `pool`, `slot`, `position`, `lowerTick`, and `upperTick` values. Feed those from one consistent snapshot when your service supplies the state. Its default reads are not a reservation of that state. Pass `liquidityTarget` instead of the two funding budgets to request an exact size, and check its funding requirements before submission.

> [tab] Python
>
> The client reads the planning state and builds the route and dependencies. Its convenience planner performs separate reads; a service requiring a coherent snapshot can use the pure planner in `aleo_shield_swap.rebalance` with its own state source.
>
> ```python theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> def rebalance_once(position_token_id: str, tick_lower: int,
>                    tick_upper: int, deadline_offset_blocks: int):
>     position = dex.get_owned_position(position_token_id)
>     if position is None or position.state is None or position.state.liquidity == 0:
>         raise ValueError("position unavailable or empty")
>     plan = dex.plan_rebalance(
>         pool_key=position.pool_key,
>         position_token_id=position_token_id,
>         tick_lower=tick_lower,
>         tick_upper=tick_upper,
>         max_funding0=0,
>         max_funding1=0,
>     )
>     return dex.rebalance_position(
>         plan=plan,
>         position_record=position.record,
>         deadline_offset_blocks=deadline_offset_blocks,
>     ).delegate(wait=True)
> ```
>
> The result contains `transaction_id` and `position_token_id`. Set `liquidity_target` instead of both funding budgets when you require an exact size. Preload dependencies and prepare funding before the final plan when running a latency-sensitive service.

> [tab] Rust
>
> SnarkVM executes the same program ABI; it does not provide an LP rebalance planner. You supply exact integer amounts, current records, route selection, and insertion hints yourself. Use the latest release of SnarkVM. For mainnet, instantiate these functions with `MainnetV0` and a mainnet query source; do not mix records or program editions across networks.
>
> Start with a configured `VM` containing the deployed programs and imports in dependency order, including dynamic token and wrapper callees. Supply a `QueryTrait` implementation for the target ledger, a signing key, a cryptographically secure RNG, and a spendable fee record. The priority fee is in microcredits and is additional to the calculated execution fee. Load proving material before taking the final LP snapshot.
>
> All four Rust examples share the `Submitter` below. Add the latest dependencies to your Rust project:
>
> ```bash theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> cargo add snarkvm --no-default-features --features console,ledger,synthesizer,utilities
> cargo add reqwest --features blocking,json
> cargo add serde_json
> ```
>
> Set `broadcast_url` to your node's network-scoped endpoint, `https://<node>/<network>/transaction/broadcast`, using the same network as the VM and query source. Each call saves the signed transaction, including its ID, to `checkpoint` before making the HTTP request. Use a path in an existing directory; the helper refuses to overwrite an existing file. Keep that checkpoint until the outcome is resolved, including when the request times out.
>
> The returned ID means the node acknowledged the broadcast. **It does not confirm acceptance.** Reconcile the transaction and scan accepted outputs before the next dependent operation. Proof generation alone does not establish that finalization will accept current pool state. These are blocking functions; run them outside an async executor's worker thread.
>
> For an atomic rebalance with two plain assets and no additional funding, call the rebalance **router**, not the core function, which checks its caller.
>
> ```rust theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> use reqwest::blocking::Client;
> use snarkvm::{
>     ledger::{query::QueryTrait, store::ConsensusStorage},
>     prelude::*,
> };
> use std::{
>     error::Error, fs::OpenOptions, path::Path,
>     str::FromStr, time::Duration,
> };
>
> type SubmitResult = Result<String, Box<dyn Error>>;
>
> pub struct Submitter<'a, N: Network, C: ConsensusStorage<N>> {
>     pub vm: &'a VM<N, C>,
>     pub key: &'a PrivateKey<N>,
>     pub query: &'a dyn QueryTrait<N>,
>     pub broadcast_url: &'a str,
>     pub priority_fee_microcredits: u64,
> }
>
> impl<N: Network, C: ConsensusStorage<N>> Submitter<'_, N, C> {
>     pub fn build_and_broadcast<R: Rng + CryptoRng>(
>         &self,
>         entrypoint: (&str, &str),
>         encoded_inputs: &[String],
>         fee_record: Record<N, Plaintext<N>>,
>         checkpoint: &Path,
>         rng: &mut R,
>     ) -> SubmitResult {
>         let http = Client::builder()
>             .timeout(Duration::from_secs(60))
>             .retry(reqwest::retry::never())
>             .redirect(reqwest::redirect::Policy::none())
>             .build()?;
>         let inputs = encoded_inputs.iter()
>             .map(|input| Value::<N>::from_str(input))
>             .collect::<Result<Vec<_>, _>>()?;
>         let transaction = self.vm.execute(
>             self.key, entrypoint, inputs.into_iter(), Some(fee_record),
>             self.priority_fee_microcredits, Some(self.query), rng,
>         )?;
>         let id = transaction.id().to_string();
>
>         // Save before POST. An HTTP error leaves submission status unresolved.
>         let mut file = OpenOptions::new().write(true).create_new(true)
>             .open(checkpoint)?;
>         serde_json::to_writer(&mut file, &transaction)?;
>         file.sync_all()?;
>         eprintln!("Broadcasting {id}; saved at {}", checkpoint.display());
>
>         let returned_id: String = http.post(self.broadcast_url)
>             .json(&transaction)
>             .send()?
>             .error_for_status()?
>             .json()?;
>         if returned_id != id {
>             return Err(format!("broadcast ID mismatch; reconcile {id}").into());
>         }
>         Ok(id)
>     }
> }
>
> pub fn rebalance_plain<N: Network, C: ConsensusStorage<N>, R: Rng + CryptoRng>(
>     submitter: &Submitter<N, C>,
>     encoded_inputs: [&str; 6],
>     fee_record: Record<N, Plaintext<N>>,
>     checkpoint: &Path,
>     rng: &mut R,
> ) -> SubmitResult {
>     let inputs = encoded_inputs.map(str::to_owned);
>     submitter.build_and_broadcast(
>         ("shield_swap_rebalance_router.aleo", "rebalance_plain_plain_none"),
>         &inputs, fee_record, checkpoint, rng,
>     )
> }
> ```
>
> Supply Aleo value literals in this exact order, not JSON or serialized SDK objects:
>
> | Index | ABI value | What to supply |
> | - | - | - |
> | 0 | `PositionNFT` record | Current unspent old-position record. |
> | 1 | `field` nonce | A fresh nonce yielding an unused successor position ID. |
> | 2 | `RebalanceRequest` struct | `old_liquidity`, `recovered0/1`, `funded0/1`, `refund0/1`, `liquidity_target`, nested `mint`, and `deadline`. Both funded values must be `0u128` on this route. |
> | 3 | `RebalanceAssets` struct | `token0` and `token1`, each containing `token_id` and `underlying_id`. The two IDs match within each plain side. |
> | 4 | `[MerkleProof; 2]` | Owner proof bundle. |
> | 5 | `[MerkleProof; 2]` | Withdrawal proof bundle. |
>
> The nested `mint` fields are `pool`, `tick_lower`, `tick_upper`, `amount0_desired`, `amount1_desired`, `amount0_min`, `amount1_min`, `tick_lower_hint`, and `tick_upper_hint`. Amounts and liquidity are `u128`, ticks are `i32`, the pool is a `field`, and the deadline is a `u32` block height. Each Merkle proof contains `siblings: [field; 16]` and `leaf_index: u32`. Match the literals to your deployed router's ABI; the [entrypoint reference](https://shield.fi/docs/reference/entrypoint-signatures.md#rebalance-router) lists the supported routes.
>
> Funded or wrapped rebalances use different router functions and additional inputs. Select the matching [entrypoint](https://shield.fi/docs/reference/entrypoint-signatures.md#rebalance-router); changing only the function name is insufficient. A `none` funding route still needs transaction fees and the token program dependencies used to pay refunds.

The Typescript SDK and Python SDK default to a 20-block deadline offset; their functions above require you to choose it explicitly. Rust supplies the absolute deadline in `RebalanceRequest`. Save submission identifiers and distinguish accepted, rejected, pending, and unknown outcomes. A client timeout does not cancel a transaction. Resolve an uncertain attempt before spending its records in a replacement attempt or switching to staged execution. Use [Settlement verification](https://shield.fi/docs/trading/verify-settlement.md) for the acceptance checks and [the confirmation helper](https://shield.fi/docs/liquidity/minting-a-position.md#wait-between-sdk-operations) for position discovery after acceptance.

## Strategy 2: Staged Rebalance

Staging removes the requirement to predict exact recovery and an exact replacement in the same transaction. Decrease and mint each have their own amount bounds; collection uses the owed balances after the decrease. Confirm each transaction before spending its output records in the next step. Staging does not guarantee inclusion, a particular final liquidity, or a successful replacement.

Standalone decrease and mint have no `deadline` parameter in this ABI. A client timeout does not expire or cancel a submitted transaction. Store each transaction ID and resolve uncertain outcomes before retrying or advancing to the next step.

Use the latest releases of the **Typescript SDK** or **Python `shield-swap-sdk`** with a mainnet local account; follow the configuration instructions in [Code examples](#code-examples). Rust uses the latest **SnarkVM** release and the shared `Submitter` defined there. You can start with the withdrawal below, or use reserves to [mint the new range](#mint-the-new-range-with-bounded-deposits) before collecting the old capital.

### Set withdrawal bounds from your acceptable inventory

A decrease takes a liquidity quantity and two minimum principal amounts. Those minima apply separately to each token. Fees do not make up for principal below a minimum, and excess token1 does not satisfy a token0 minimum.

`liquidity_to_remove` must not exceed the position's confirmed liquidity. For a full exit, use the entire remaining amount; for a partial withdrawal, the remainder must meet the pool minimum. Use the latest unspent NFT.

To calculate the bounds, let `L_remove` be the liquidity to remove and `a < b` the old range's unscaled square-root price bounds. At a square-root price `s`, clamp the price to the range before calculating principal:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
c = min(max(s, a), b)
x(L_remove, s, a, b) = L_remove × (1/c - 1/b)     token0
y(L_remove, s, a, b) = L_remove × (c - a)         token1
```

Use canonical token0/token1 order and native base units. These are real-number relationships; the contract uses Q128.128 square-root prices and integer arithmetic. Use the SDK's integer helpers with withdrawal rounding, as the examples below do. See [Liquidity formulas](https://shield.fi/docs/math/liquidity-formulas.md) for the fixed-point form.

For your acceptable square-root price interval `[s_low, s_high]`, calculate:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
amount0_min = floor(x(L_remove, s_high, a, b))
amount1_min = floor(y(L_remove, s_low,  a, b))
```

Token0 is smallest at the high price; token1 is smallest at the low price. Use withdrawal rounding. The actual checks bound inventory, so integer rounding and constant amounts outside the position's range mean they are not a strict execution-price interval.

Reducing each quoted amount by 1% is not the same as allowing a 1% price move. Setting both minima to zero accepts any principal mix, including a one-sided result. It still removes the full requested liquidity and credits all calculated principal; lowering a minimum does not donate the difference.

The Typescript and Python functions below remove all liquidity using two policy ticks as price endpoints. These are price bounds, not the new position's range. `acceptableTickLow` and `acceptableTickHigh` (Python: `acceptable_tick_low/high`) must lie within the supported tick domain and be ordered; they need not align to pool tick spacing. They use the **mainnet** `client` or `dex` from [Setup](https://shield.fi/docs/trading/configure.md). Rust takes the liquidity and precomputed minima directly.

> [tab] Typescript
>
> ```typescript theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> import { amountsForLiquidity, getSqrtPriceAtTickX128 } from "@provablehq/shield-swap-sdk"
>
> async function decreaseForRebalance(
>   positionTokenId: string,
>   acceptableTickLow: number,
>   acceptableTickHigh: number,
> ) {
>   if (acceptableTickLow > acceptableTickHigh) throw new Error("inverted price bounds")
>   const p = await client.getPosition({ positionTokenId })
>   if (!p || p.liquidity === 0n) throw new Error("position unavailable or empty")
>   const amountsAt = (tick: number) => amountsForLiquidity({
>     sqrtPriceX128: getSqrtPriceAtTickX128(tick),
>     sqrtLowerX128: getSqrtPriceAtTickX128(p.tick_lower),
>     sqrtUpperX128: getSqrtPriceAtTickX128(p.tick_upper),
>     liquidity: p.liquidity,
>     roundUp: false,
>   })
>   return client.decreaseLiquidity({
>     poolKey: p.pool,
>     positionTokenId,
>     liquidityToRemove: p.liquidity,
>     amount0Min: amountsAt(acceptableTickHigh).amount0,
>     amount1Min: amountsAt(acceptableTickLow).amount1,
>   })
> }
> ```

> [tab] Python
>
> ```python theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> from aleo_shield_swap.position_math import amounts_for_liquidity
> from aleo_shield_swap.tick_math import get_sqrt_price_at_tick_x128
>
> def decrease_for_rebalance(position_token_id: str,
>                            acceptable_tick_low: int, acceptable_tick_high: int):
>     if acceptable_tick_low > acceptable_tick_high:
>         raise ValueError("inverted price bounds")
>     p = dex.get_owned_position(position_token_id)
>     if p is None or p.state is None or p.state.liquidity == 0:
>         raise ValueError("position unavailable or empty")
>     lower = get_sqrt_price_at_tick_x128(p.tick_lower)
>     upper = get_sqrt_price_at_tick_x128(p.tick_upper)
>     amount0_min, _ = amounts_for_liquidity(
>         get_sqrt_price_at_tick_x128(acceptable_tick_high), lower, upper,
>         p.state.liquidity, round_up=False,
>     )
>     _, amount1_min = amounts_for_liquidity(
>         get_sqrt_price_at_tick_x128(acceptable_tick_low), lower, upper,
>         p.state.liquidity, round_up=False,
>     )
>     return dex.decrease_liquidity(
>         pool_key=p.pool_key,
>         position_record=p.record,
>         liquidity_to_remove=p.state.liquidity,
>         amount0_min=amount0_min,
>         amount1_min=amount1_min,
>     ).delegate(wait=True)
> ```

> [tab] Rust
>
> Add this function to the same module as `Submitter` in the [atomic example](#code-examples). It builds, checkpoints, and broadcasts the decrease, returning the broadcast transaction ID. Confirm acceptance before collecting.
>
> ```rust theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> pub fn decrease_for_rebalance<N: Network, C: ConsensusStorage<N>, R: Rng + CryptoRng>(
>     submitter: &Submitter<N, C>,
>     nft: &str,
>     liquidity_to_remove: u128,
>     amount0_min: u128,
>     amount1_min: u128,
>     fee_record: Record<N, Plaintext<N>>,
>     checkpoint: &Path,
>     rng: &mut R,
> ) -> SubmitResult {
>     let inputs = [
>         nft.to_owned(),
>         format!("{liquidity_to_remove}u128"),
>         format!("{amount0_min}u128"),
>         format!("{amount1_min}u128"),
>     ];
>
>     submitter.build_and_broadcast(
>         ("shield_swap.aleo", "decrease_liquidity"),
>         &inputs, fee_record, checkpoint, rng,
>     )
> }
> ```
>
> `nft` is the exact latest unspent `PositionNFT` plaintext, including its record metadata. For a full decrease, pass the position's entire confirmed liquidity as `liquidity_to_remove`; a smaller value leaves active liquidity. Calculate token0 and token1 principal minima in base units from the withdrawal bounds above using integer math and withdrawal rounding. Decrease requires no token transfer. After acceptance, use its replacement NFT for collect.

After a full decrease is accepted, the old position has zero liquidity and its principal plus fees are stored as owed balances. It keeps the same position ID but returns a replacement NFT record. Save that record before the next operation. Subsequent swaps cannot change those owed quantities or earn further fees for this position, although their market value still changes. You can leave the claim there and collect later; no residual liquidity is needed to preserve it.

A partial decrease behaves differently: the remaining liquidity continues to change inventory and earn fees when in range. For a nonzero decrease, the remainder must be zero or meet the pool's minimum position liquidity.

### Collect the old capital, or deploy reserves

If you need the old capital for the next mint, collect after the full decrease is accepted. Re-read the now-stable owed balances and set `amount0_requested` and `amount1_requested` to those amounts. Each request must be at most its corresponding collectible balance. An oversized request rejects instead of being capped; a smaller request succeeds if the other checks pass and leaves the remainder owed. Unlike atomic recovery, staged collection does not require emptying the old position.

Use the NFT returned by the accepted decrease, and match `token0_id` and `token1_id` to the pool's canonical assets.

> [tab] Typescript
>
> ```typescript theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> async function collectClosedPosition(
>   positionTokenId: string,
>   imports: Record<string, string>,
> ) {
>   const p = await client.getPosition({ positionTokenId })
>   if (!p || p.liquidity !== 0n) throw new Error("full decrease not confirmed")
>   if (p.tokens_owed0 === 0n && p.tokens_owed1 === 0n) return null
>   return client.collect({
>     poolKey: p.pool,
>     positionTokenId,
>     amount0Requested: p.tokens_owed0,
>     amount1Requested: p.tokens_owed1,
>     imports,
>   })
> }
> ```

> [tab] Python
>
> ```python theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> def collect_closed_position(position_token_id: str):
>     p = dex.get_owned_position(position_token_id)
>     if p is None or p.state is None or p.state.liquidity != 0:
>         raise ValueError("full decrease not confirmed")
>     if p.state.tokens_owed0 == 0 and p.state.tokens_owed1 == 0:
>         return None
>     return dex.collect(
>         pool_key=p.pool_key,
>         position_record=p.record,
>         amount0_requested=p.state.tokens_owed0,
>         amount1_requested=p.state.tokens_owed1,
>     ).delegate(wait=True)
> ```

> [tab] Rust
>
> Add this function to the module containing the [shared `Submitter`](#code-examples). Read the confirmed position state and pass its liquidity, exact owed amounts, and latest unspent NFT. The function submits nothing when both owed amounts are zero.
>
> `token_ids` are the pool's canonical AMM token IDs, in token0/token1 order. For each side, set `receiver_proofs` to `None` for a plain asset or `Some(bundle)` for a wrapped asset, using the verified wrapper bindings. Include it for every wrapped side even if its requested amount is zero. These choices select all four supported collection routes.
>
> ```rust theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> pub fn collect_closed_position<N: Network, C: ConsensusStorage<N>, R: Rng + CryptoRng>(
>     submitter: &Submitter<N, C>,
>     nft: &str,
>     liquidity: u128,
>     owed: [u128; 2],
>     token_ids: [&str; 2],
>     amm_proofs: [&str; 2], // owner, withdrawal
>     receiver_proofs: [Option<&str>; 2], // underlying token0, token1
>     fee_record: Record<N, Plaintext<N>>,
>     checkpoint: &Path,
>     rng: &mut R,
> ) -> Result<Option<String>, Box<dyn Error>> {
>     if liquidity != 0 {
>         return Err("full decrease not confirmed".into());
>     }
>     if owed == [0, 0] {
>         return Ok(None);
>     }
>     let entrypoint = match receiver_proofs {
>         [None, None] => ("shield_swap.aleo", "collect"),
>         [Some(_), None] => ("shield_swap_lp_router.aleo", "collect_to_wrapped_arc20"),
>         [None, Some(_)] => ("shield_swap_lp_router.aleo", "collect_to_arc20_wrapped"),
>         [Some(_), Some(_)] => ("shield_swap_lp_router.aleo", "collect_to_wrapped_wrapped"),
>     };
>     let mut inputs = vec![
>         nft.to_owned(), format!("{}u128", owed[0]), format!("{}u128", owed[1]),
>         token_ids[0].to_owned(), token_ids[1].to_owned(),
>         amm_proofs[0].to_owned(), amm_proofs[1].to_owned(),
>     ];
>     // Wrapped receiver bundles follow both AMM bundles, in token order.
>     inputs.extend(receiver_proofs.into_iter().flatten().map(str::to_owned));
>     let id = submitter.build_and_broadcast(
>         entrypoint, &inputs, fee_record, checkpoint, rng,
>     )?;
>     Ok(Some(id))
> }
> ```
>
> Proofs, token IDs, and the NFT are Aleo value literals. Load the AMM, token programs, and any wrapper and LP-router dependencies into the VM before proving. Confirm acceptance and scan the payout records before using them for mint; the broadcast response alone is insufficient.

Collection pays the immutable withdrawal address. The NFT owner, withdrawal address, and account funding the next mint can differ. Confirm that the minting account can spend the payout records; ordinary mint cannot spend another position's owed balances directly. Wrapped collection routes pay the underlying asset and can require the withdrawal address to equal the signer because of the unwrap operation.

If you have separate spendable reserves, minting does not depend on collecting the old claim. Decrease first to stop the old exposure, or mint first to establish the replacement before removing it. Use distinct token and fee records for independent transactions. Operations consuming the same NFT must remain sequential.

### Mint the new range with bounded deposits

Start with spendable budgets `B0` and `B1`, from collection or reserves, in native base units and canonical pool token order. Let `s` be the current unscaled square-root price and `a_new < b_new` the replacement range's bounds. When `a_new < s < b_new`, preview the liquidity each budget supports:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
L0 = B0 / (1/s - 1/b_new)
L1 = B1 / (s - a_new)
L_preview = min(L0, L1)
```

The smaller result limits the position. Outside the range, only the required token side determines liquidity. This is a real-number preview; use integer helpers and protocol rounding for a quote. Choose the desired budgets and minimum allocations you are willing to submit from that preview.

Ordinary mint transfers both desired budgets, derives liquidity again at the execution price, and checks that each allocated amount lies between its minimum and desired amount. It has no caller-supplied liquidity target or explicit minimum-liquidity parameter, so `L_preview` is not an execution guarantee.

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
amount_min_i <= amount_allocated_i <= amount_desired_i
new_tokens_owed_i = amount_desired_i - amount_allocated_i
```

Unused desired deposits become owed balances on the new position and require collection. Only input-record value above the desired deposit returns immediately as record change. Oversized budgets can therefore leave capital outside active liquidity while it awaits collection.

Choose minima for the allocation you accept across your price interval. If price can leave the range, a positive minimum on the side that becomes unused rejects the mint. Zero minima still require positive liquidity meeting the pool minimum and nonzero allocated amounts on the sides required at execution. They do not guarantee the liquidity size you saw in a preview.

**Before submitting the replacement mint:**

* Check `pool`, `token0_id`, and `token1_id` in canonical order. Both `tick_lower` and `tick_upper` must align to spacing and satisfy `-400000 <= tick_lower < tick_upper <= 400000`. The liquidity calculated at execution must fit each boundary's `max_liquidity_per_tick` capacity as well as the pool minimum.
* Resolve `tick_lower_hint` and `tick_upper_hint` from the live initialized-tick list, with the lower insertion applied before the upper. A new boundary needs a valid predecessor with `hint.tick < new_tick < hint.next`. If decrease ran first, its removed boundary ticks are no longer valid predecessors. If mint runs first, the old liquidity still occupies boundary capacity. Amount tolerance does not protect against stale hints or insufficient capacity.
* Supply spendable funding records covering `amount0_desired` and `amount1_desired`, plus fee inputs. Check registered wrapper identities and use the LP router for wrapped assets. A balance still owed on the old position cannot fund mint directly. Keep distinct funding and fee records for independent reserve-funded transactions.
* Use valid `recipient` and `withdrawal` addresses; neither may be zero or the core contract's own address.
* Choose a `nonce` whose committed inputs produce an unused position ID. A successful decrease or collection does not guarantee that these mint checks will pass later.

> [tab] Typescript
>
> ```typescript theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> async function mintReplacement(
>   poolKey: string, tickLower: number, tickUpper: number,
>   amount0Desired: bigint, amount1Desired: bigint,
>   amount0Min: bigint, amount1Min: bigint,
>   recipient: string, withdrawal: string,
>   imports: Record<string, string>,
> ) {
>   return client.mint({
>     poolKey, tickLower, tickUpper,
>     amount0Desired, amount1Desired, amount0Min, amount1Min,
>     recipient, withdrawal, imports,
>   })
> }
> ```

> [tab] Python
>
> ```python theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> def mint_replacement(pool_key: str, tick_lower: int, tick_upper: int,
>                      amount0_desired: int, amount1_desired: int,
>                      amount0_min: int, amount1_min: int,
>                      recipient: str, withdrawal: str):
>     return dex.mint(
>         pool_key=pool_key, tick_lower=tick_lower, tick_upper=tick_upper,
>         amount0_desired=amount0_desired, amount1_desired=amount1_desired,
>         amount0_min=amount0_min, amount1_min=amount1_min,
>         recipient=recipient, withdrawal=withdrawal,
>     ).delegate(wait=True)
> ```

> [tab] Rust
>
> Add this function to the module containing the [shared `Submitter`](#code-examples). It supports plain/plain, wrapped/plain, plain/wrapped, and wrapped/wrapped pools. For a wrapped side, supply the underlying asset's funding record and `Some(sender_proof)` for its signer; use `None` for a plain side. Determine these choices from verified wrapper bindings.
>
> ```rust theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> pub fn mint_replacement<N: Network, C: ConsensusStorage<N>, R: Rng + CryptoRng>(
>     submitter: &Submitter<N, C>,
>     nonce: &str,
>     funding_records: [&str; 2],
>     sender_proofs: [Option<&str>; 2], // underlying token0, token1
>     recipient: &str,
>     withdrawal: &str,
>     request: &str,
>     token_ids: [&str; 2],
>     amm_proofs: [&str; 3], // signer, recipient, withdrawal
>     fee_record: Record<N, Plaintext<N>>,
>     checkpoint: &Path,
>     rng: &mut R,
> ) -> SubmitResult {
>     let entrypoint = match sender_proofs {
>         [None, None] => ("shield_swap.aleo", "mint"),
>         [Some(_), None] => ("shield_swap_lp_router.aleo", "mint_from_wrapped_arc20"),
>         [None, Some(_)] => ("shield_swap_lp_router.aleo", "mint_from_arc20_wrapped"),
>         [Some(_), Some(_)] => ("shield_swap_lp_router.aleo", "mint_from_wrapped_wrapped"),
>     };
>     let mut inputs = vec![nonce.to_owned()];
>     for (record, proof) in funding_records.into_iter().zip(sender_proofs) {
>         inputs.push(record.to_owned());
>         // A wrapped side's sender bundle immediately follows its record.
>         if let Some(bundle) = proof {
>             inputs.push(bundle.to_owned());
>         }
>     }
>     inputs.extend([
>         recipient.to_owned(), withdrawal.to_owned(), request.to_owned(),
>         token_ids[0].to_owned(), token_ids[1].to_owned(),
>     ]);
>     inputs.extend(amm_proofs.map(str::to_owned));
>     submitter.build_and_broadcast(
>         entrypoint, &inputs, fee_record, checkpoint, rng,
>     )
> }
> ```
>
> Supply Aleo literals: a fresh `field` nonce, complete record plaintexts, addresses, the pool's two canonical AMM token IDs as `field` values, and proof arrays. `request` is a `MintPositionRequest` struct with `pool`, `tick_lower`, `tick_upper`, `amount0_desired`, `amount1_desired`, `amount0_min`, `amount1_min`, `tick_lower_hint`, and `tick_upper_hint`. Use `field` for the pool, `i32` for ticks and hints, and `u128` for all amounts. Resolve insertion hints from current tick state, including the new lower tick before the upper tick. See [entrypoint signatures](https://shield.fi/docs/reference/entrypoint-signatures.md#core-pool-and-position-lifecycle) for the core ABI.
>
> Load the route and its dynamic dependencies into the VM first. The function returns a broadcast transaction ID; after acceptance, obtain the new position ID and NFT from the accepted mint outputs. Reuse the old owner and withdrawal explicitly when preserving those roles. The reserve-funded and collected-capital strategies use this same function with different funding records.

Save the returned transaction and successor position IDs. After acceptance, read the new position's actual liquidity and owed balances, and save its NFT. If mint rejects, the earlier decrease and collection remain completed. Reassess the available token mix before retrying. Burn the old NFT only once its liquidity and both owed balances are zero; burning moves no funds and can wait until redeployment is complete.

## Execution and Recovery

Use an atomic attempt budget based on measured cost and latency. If matching a live quote becomes unreliable, choose staged execution according to the exposure and working capital you can carry. Retrying indefinitely does not remove the dependency on a quiet execution window.

| Last accepted step | Capital state | Next action |
| - | - | - |
| Atomic attempt rejected | Old position remains; rejected execution may still cost a fee. | Diagnose the rejection, refresh all affected inputs, and decide whether to retry or stage. |
| Full decrease | Principal and fees are owed on a zero-liquidity position. | Collect or mint with separate reserves. |
| Collect | Payout records belong to the withdrawal address. | Confirm spendability and refresh replacement budgets. |
| Mint with reserves, before old decrease | Both positions exist. | Manage combined exposure until the old decrease succeeds. |
| Replacement mint | New liquidity and any unused deposits are on the new position. | Verify actual allocation, preserve its NFT, and finish old-position cleanup. |
| Submission outcome unknown | Record consumption and state change are unresolved. | Reconcile that transaction before replacing it or advancing. |

For every operation, store the transaction identifier and accepted output records. Resolve uncertain submissions before replacing them, and resume from the last accepted step using the checks in the corresponding strategy section.

## Related operations

* [Rebalance positions](https://shield.fi/docs/liquidity/rebalance-position.md) covers the app walkthrough and SDK steps.
* [Choosing a liquidity range](https://shield.fi/docs/liquidity/choosing-a-range.md) covers tick alignment and range selection.
* [Collecting and burning](https://shield.fi/docs/liquidity/collecting-and-burning.md) covers payout and NFT cleanup.
* [Tracking position fills](https://shield.fi/docs/liquidity/position-fills.md) separates inventory changes from fee income.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.