Skip to main content

Reshield

A reshield turns a shielded position in one asset into a shielded position in another. On-chain it is a withdraw-and-swap whose aggregator route ends not at a recipient but at a contractCalls step that deposits the swapped output back into the Entrypoint. The result is a new note with no on-chain link to the one you spent, and the funds never sit in a wallet in between.

Reshielding is honest about one thing the shorthand hides: the funds do leave the pool. They exit, trade, and return within one transaction. The privacy claim is about what an observer can link, not about the funds staying shielded throughout.

Same-chain reshield

  1. Estimate. estimateReshieldAmount queries the relayer fee and the aggregator, reads the output token's vettingFeeBPS from the Entrypoint, and returns the note value the deposit will carry plus totalDepositCost (the deposit value and its vetting fee).
  2. Prove the deposit. Build a standard deposit proof for the output token with prepareDeposit({ tokenId: outputToken, value: estimate.noteValue }). This is the note you will hold afterwards.
  3. Quote. prepareReshieldQuote fetches the aggregator's contractCalls quote (swap, then a call to Entrypoint.deposit with your deposit proof and note data), and commits the routing with the relayer. Because the deposit calldata is inside the aggregator calldata, and that is inside the proof-bound routing, the deposit is as fixed as the swap.
  4. Relay. relayReshield proves the withdrawal and submits. RelaySwaps executes the swap; the aggregator's post-swap call deposits; the pool inserts your new commitment.
const estimate = await session.estimateReshieldAmount({
relayerInfo,
amount: spendAmount, // Hex, in the input token
tokenId: usdcAddress,
outputToken: usdtAddress,
});

const depositResult = await session.prepareDeposit({
tokenId: usdtAddress,
value: estimate.noteValue,
});

const quote = await session.prepareReshieldQuote({
relayerInfo,
amount: spendAmount,
tokenId: usdcAddress,
outputToken: usdtAddress,
depositResult,
totalDepositCost: estimate.totalDepositCost,
});

const receipt = await session.relayReshield({
inputCommitments: [noteToSpend.commitment],
amount: spendAmount,
tokenId: usdcAddress,
outputToken: usdtAddress,
swapCalldata: quote.swapCalldata,
selectedQuote: quote.selectedQuote,
swapTarget: quote.swapTarget,
approvalAddress: quote.approvalAddress,
dustRecipient: quote.dustRecipient,
depositNote: quote.depositNote, // persisted as PENDING once mined
});

When outputToken is a yield share, the embedded call is PPRouter.depositExactShares (or depositExactSharesNative for a native-mode router) instead of Entrypoint.deposit: the swap buys the wrapper's underlying, and the aggregator approves the router. Configure the session's yield deployments so the SDK can resolve the router.

Cross-chain reshield

The same three calls with a destination: estimateReshieldCrosschainAmount, prepareReshieldCrosschainQuote, and relayReshieldCrosschain, each taking toChainId. The withdrawal happens on the source chain, the aggregator bridges and swaps, and the deposit lands in the destination chain's Entrypoint. Two things change:

  • The deposit proof is destination-scoped. Build depositResult against the destination chain's pool and Entrypoint (a second session, or the destination interactors), because that is where the commitment will be inserted. Share deposits on the destination reserve a larger rate headroom (CROSS_CHAIN_DEPOSIT_RATE_HEADROOM_PPM), since a bridge leg takes minutes rather than seconds.
  • Dust can be rescued to a stealth address. A bridge delivers slightly more or less than quoted. Opt in with dustRescue: { recipientIdentifier } and the leftover on the destination is deposited for a one-time stealth address derived from your meta-address, with the discovery material packed into the deposit's own noteData rather than a public announcement. The identifier rides in the routing's dustRescue field, so it is proof-bound even though RelaySwaps never reads it.

Cross-chain is not a single moment. The source transaction confirms, then the bridge leg completes on its own schedule; the SDK's swap status provider tracks it.

Constraints

  • Two providers. Reshield needs withSwapAndDepositQuoteProvider(...) for the contractCalls quote in addition to withSwapQuoteProvider(...). LiFiSwapQuoteProvider implements both.
  • The vetting fee is read on-chain. It is whatever Entrypoint.assets(outputToken).vettingFeeBPS says at quote time and is not caller-supplied, because Entrypoint.deposit accepts nothing else.
  • No recipient, no floor. The routing carries a zero recipient, so the on-chain output check does not run; a failed or short delivery makes the embedded Entrypoint.deposit revert, which fails the whole transaction. The contract also rejects any nativeGas or minOutputAmount on such a routing, so those fields cannot be set by mistake.
  • The new note is PENDING. Like any deposit it awaits ASP attestation before it is spendable; the ASP sees a fresh deposit from RelaySwaps' aggregator leg, not from you.
  • Cross-chain needs both chains. Source-chain relayer with swaps enabled, destination-chain Entrypoint interactor, and matching yield targets on the destination if you are landing as a share.

Behind the scenes

SDK calls estimateReshieldAmount, prepareReshieldQuote, relayReshield; or the ...Crosschain trio. Plus prepareDeposit for the output note.
Contract method RelaySwaps.execute (or .executeUnwrapped when spending a yield share), whose aggregator call ends in Entrypoint.deposit or PPRouter.depositExactShares on the destination.
Relayer routes POST /v1/quote/evm/{chainId}/swap and POST /v1/relay/evm/{chainId}/relay-swap, as for a plain withdraw-and-swap.
Circuits A transact_NxM proof for the spend and a deposit proof for the new note, the second embedded in the aggregator calldata the first commits to.

What's next