Skip to content

Encode_V3_Position_Manager_Call

Elnaril edited this page Nov 11, 2026 · 1 revision

How to encode a V3 position manager call ?

The V3_POSITION_MANAGER_CALL command calls one of the 3 V3 position manager functions the UR allows: decreaseLiquidity, collect and burn. Only one function can be called per command (the UR forwards the input as a single call and rejects anything else, including multicall), so several calls must be chained in the same transaction.

⚠ The UR must be approved for the position (NFT) first, with the V3_POSITION_MANAGER_PERMIT command.

Codec creation

No rpc access is required to encode these calls:

from uniswap_universal_router_decoder import RouterCodec
codec = RouterCodec()

decreaseLiquidity

v3_decrease_liquidity() removes liquidity from a V3 position:

encoded_data = codec.encode.chain().v3_decrease_liquidity(
        token_id,  # the id of the V3 position (NFT)
        liquidity,  # the amount of liquidity to remove
        amount_0_min,  # the minimum amount of token0 the decrease must free
        amount_1_min,  # the minimum amount of token1 the decrease must free
        deadline,  # when this call is not valid anymore
    ).build(codec.get_default_deadline())  # unix timestamp after which the trx is not valid any more

The freed tokens are not sent out: they are recorded as tokensOwed0/tokensOwed1 on the position, together with all the fees earned since the last update (fees are not prorated, even if only part of the liquidity is removed). Use v3_collect() to withdraw them.

collect

v3_collect() sends the tokens owed to a V3 position:

encoded_data = codec.encode.chain().v3_collect(
        token_id,  # the id of the V3 position (NFT)
        recipient_address,  # the address receiving the collected tokens
        amount_0_max,  # the maximum amount of token0 to collect
        amount_1_max,  # the maximum amount of token1 to collect
    ).build(codec.get_default_deadline())  # unix timestamp after which the trx is not valid any more

It sends min(amountMax, tokensOwed) to the recipient. As tokensOwed holds both the principal freed by v3_decrease_liquidity() and the fees, v3_collect() alone harvests the fees without touching the liquidity.

burn

v3_burn() deletes a V3 position (NFT):

encoded_data = codec.encode.chain().v3_burn(
        token_id,  # the id of the V3 position (NFT)
    ).build(codec.get_default_deadline())  # unix timestamp after which the trx is not valid any more

The position must be empty (liquidity == 0 and tokensOwed0 == tokensOwed1 == 0), hence the usual decreaseLiquidity -> collect -> burn order.

Chaining the calls

As only one function can be called per command, several calls are chained in the same transaction. Here is the usual decreaseLiquidity -> collect -> burn sequence:

deadline = codec.get_default_deadline()
encoded_data = (
    codec
        .encode
        .chain()
        .v3_decrease_liquidity(token_id, liquidity, 0, 0, deadline)
        .v3_collect(token_id, recipient_address, 2 ** 128 - 1, 2 ** 128 - 1)  # collect everything owed
        .v3_burn(token_id)
        .build(deadline)
)

The calls can also be split across several transactions if needed.

Ending words

Note that mint and increaseLiquidity are not reachable through the UR: only the 3 functions above are allowed, the others must be called directly on the V3 position manager.

Only the input data is built here. See How to build a full transaction to the Universal Router ? to build the whole transaction.

Clone this wiki locally