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 theshield_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.
- 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 theshield_swap.aleo core contract, with the shield_swap_lp_router.aleo router handling wrapped-asset collection and minting.
- 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.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.
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 or 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 arerecovered0, 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:
Size the replacement from token inventory
Use canonical pool token order and native base units throughout. If token0 hasd0 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:
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 for the fixed-point form.
Your recovered balance includes more than principal:
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:
B0, B1. Inside the new range, the real-number sizing relationship is:
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.
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.
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.poolmust match the old pool, andassets.token0.token_idandassets.token1.token_idmust 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.aleoentrypoint. The core rebalance rejects a direct call. Eachunderlying_idmust 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:
nonceand 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.
Then act on the transaction result:
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 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.
- Typescript
- Python
- Rust
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.
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.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 for the acceptance checks and the confirmation helper 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 nodeadline 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. 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 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:
[s_low, s_high], calculate:
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. Rust takes the liquidity and precomputed minima directly.
- Typescript
- Python
- Rust
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 setamount0_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.
- Typescript
- Python
- Rust
Mint the new range with bounded deposits
Start with spendable budgetsB0 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:
L_preview is not an execution guarantee.
- Check
pool,token0_id, andtoken1_idin canonical order. Bothtick_lowerandtick_uppermust align to spacing and satisfy-400000 <= tick_lower < tick_upper <= 400000. The liquidity calculated at execution must fit each boundary’smax_liquidity_per_tickcapacity as well as the pool minimum. - Resolve
tick_lower_hintandtick_upper_hintfrom the live initialized-tick list, with the lower insertion applied before the upper. A new boundary needs a valid predecessor withhint.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_desiredandamount1_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
recipientandwithdrawaladdresses; neither may be zero or the core contract’s own address. - Choose a
noncewhose committed inputs produce an unused position ID. A successful decrease or collection does not guarantee that these mint checks will pass later.
- Typescript
- Python
- Rust
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.
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 covers the app walkthrough and SDK steps.
- Choosing a liquidity range covers tick alignment and range selection.
- Collecting and burning covers payout and NFT cleanup.
- Tracking position fills separates inventory changes from fee income.