What a transfer does
A transfer moves these values from the sender to the recipient:- V2 (constant-product): a chosen amount of
lp_sharesfromlp_shares[from]tolp_shares[to]. Sender keeps any residual. - V3 (concentrated): the entire position at one tick range. The
v3_positionsrow at(pool_id, sender, tickLower, tickUpper)is deleted and reinserted under the recipient’s key.
What a transfer does not do
Owner-keyed accruals stay with the original owner and are never transferred:- Host fees collected as a host (
v3_host_fees) - Integrator fees collected as an integrator (
v3_integrator_fees) - Free balance (
v3_user_pool_balances) - V2 deposit principal accounting (
liquidity.principal)
Lock-follow semantics
The lock travels with the shares. The exact behavior depends on what the recipient already holds in the pool.V3: lock follows whole position
The sender’slp_locks row is rewritten under the recipient’s key with the same lockUntilTimestamp. The recipient cannot withdraw the position until the original expiry (or never, if the lock was indefinite).
V2: lock-follow gated by recipient_pre_shares
V2 stores one lp_locks row per (pool_id, owner) pair, applied to the entire lp_shares[owner] value. A naive lock-follow would let a sender impose their lock terms on the recipient’s pre-existing stake. The server prevents this:
| Recipient state at time of transfer | Lock applied to recipient |
|---|---|
lp_shares[recipient] == 0 (fresh in this pool) | New lp_locks row at the sender’s expiry |
lp_shares[recipient] > 0 (already a position) | No write. Recipient’s existing lock state is preserved verbatim |
lockPosition themselves before or after the transfer.
Failure modes
| Condition | Outcome |
|---|---|
| Sender has no active lock | Rejected. No state changes. |
| Sender’s lock has expired | Rejected. Treat the same as no lock. |
newOwnerPublicKey equals the sender | Rejected client-side before signing. |
newOwnerPublicKey is malformed or all zeros | Rejected client-side. |
V2: lpTokensToTransfer is not a positive integer string | Rejected client-side. |
V2: requested amount exceeds lp_shares[sender] | Rejected after share lookup; no commit. |
V3: tickLower/tickUpper does not match a position the sender owns | Rejected on the position read. |
| V3: recipient already owns a position at the same tick range | Rejected. The recipient must move that position first. |
| Both V2 and V3 fields supplied in one call | Rejected client-side. Pick one shape. |
Scoping: V2 vs V3
| Pool type | Granularity | Required fields |
|---|---|---|
| Constant-product (V2) | Partial amount of LP shares | lpTokensToTransfer (positive integer string), newOwnerPublicKey |
| Concentrated (V3) | Whole position at one tick range | tickLower, tickUpper, newOwnerPublicKey |
decreaseLiquidity followed by sending the unlocked output through Spark.