# Position
NFT-based liquidity position management for concentrated liquidity.
## Overview
Each liquidity position is a unique GRC721 NFT. Stored state includes the pool
key, price range, liquidity, fee-growth checkpoints, tokens owed, burned marker,
and operator. Current token balances are derived from the current pool price,
range, and liquidity; they are not permanently stored balances.
The pool accounting key encodes only the lower/upper tick pair and is scoped by
pool. NFTs with the same range in one pool share the pool-level accounting entry.
## Gnoweb
The root `Render("")` delegates to the active implementation and shows realm identity, halt flags, stored position count, and the next position ID. Stored records include burned positions, so the count does not represent active liquidity positions.
Supported routes:
- `""`: the root summary. It reads only aggregate position-store metadata.
- `id/<id>`: one position record, selected by a single keyed lookup. The `<id>` must be an unsigned decimal `uint64`; malformed, overflowing, missing, and extra-segment paths return `404`.
For example, `/r/gnoswap/position:id/1` shows position 1. Detail pages show
the ID, burn status, NFT owner when available, token realm links, fee tier, tick
range, liquidity, and stored fee accounting.
The composite pool key is inline code; each token reference links to its defining
realm rather than a `.SYMBOL` URL. Rendering does not enumerate positions or
recompute claimable fees and current token balances. Burned records remain
addressable even when their NFT owner is unavailable.
## Configuration
- **Withdrawal Fee**: 1% by default on fee-bearing swap-fee collection
- **Max Position Size**: No separate position-level cap; pool tick limits apply
- **Transfers**: Unstaked NFTs follow GRC721 owner/approval/operator rules;
staked NFTs are locked to staker-mediated transfers
## Core Functions
### `Mint`
Creates new position NFT with initial liquidity.
- Validates tick range alignment
- Calculates optimal token ratio
- Returns actual amounts used
### `IncreaseLiquidity`
Adds liquidity to an existing position.
- Maintains the existing price range
- Uses the current-price token ratio
- Can clear a burned marker when the position is used again
### `DecreaseLiquidity`
Removes liquidity while keeping the NFT.
- One atomic public operation: internally collects swap fees, burns liquidity,
then collects principal through the pool's fee-free `Collect` path
- Returns fee amounts net of the withdrawal fee and collected principal
- Amount-minimum checks apply to the principal actually collected
### `CollectFee`
Claims accumulated swap fees without removing liquidity.
- No liquidity removal required
- Returns net collected amounts plus the raw pre-withdrawal-fee amounts
- The configured withdrawal fee applies only to this fee-bearing path
### `Reposition`
Updates an existing position's price range.
- Requires the position to be clear first (zero liquidity and tokens owed)
- Reuses the same position ID and NFT
- Adds new liquidity to the updated range and clears the burned marker
## Technical Details
### Tick Alignment
Ticks must align with pool's tick spacing:
```
0.01% fee: every 1 tick
0.05% fee: every 10 ticks
0.3% fee: every 60 ticks
1% fee: every 200 ticks
```
### Optimal Range Width
**Stable Pairs (USDC/USDT)**:
- Narrow: ±0.05% (max efficiency)
- Medium: ±0.1% (balanced)
- Wide: ±0.5% (safety)
**Correlated Pairs (WETH/stETH)**:
- Narrow: ±0.5%
- Medium: ±1%
- Wide: ±2%
**Volatile Pairs (WETH/USDC)**:
- Narrow: ±5%
- Medium: ±10%
- Wide: ±25%
### Capital Efficiency
Concentration factor vs infinite range:
```
Range ±0.1% → 2000x efficient
Range ±1% → 200x efficient
Range ±10% → 20x efficient
Range ±50% → 4x efficient
```
### Token Calculations
For liquidity `L` and square-root prices `sqrtLower`, `sqrtCurrent`, and
`sqrtUpper`:
**Below range (`current < lower`, token0 only)**:
```
amount0 = L * (sqrtUpper - sqrtLower) / (sqrtUpper * sqrtLower)
amount1 = 0
```
**In range (`lower <= current < upper`, both tokens)**:
```
amount0 = L * (sqrtUpper - sqrtCurrent) / (sqrtUpper * sqrtCurrent)
amount1 = L * (sqrtCurrent - sqrtLower)
```
**Above range (`current >= upper`, token1 only)**:
```
amount0 = 0
amount1 = L * (sqrtUpper - sqrtLower)
```
## Approval and Transfer Requirements
`Mint`, `IncreaseLiquidity`, and `Reposition` pull token0 and token1 from the
caller inside the **pool** realm, so the approved spender is the pool realm
address, not the position realm.
- Approve the pool realm for both token contracts before calling a
liquidity-adding function.
- Approving the position realm alone is not sufficient; the position realm never
holds or pulls the pair tokens itself.
- Approve at least `amount0Desired` / `amount1Desired`. Any desired amount the
pool does not consume stays with the caller.
- `DecreaseLiquidity` and `CollectFee` pay out to the caller and require no
approval.
```go
// Approve the pool realm for both pair tokens before minting
poolAddress := access.MustGetAddress(prabc.ROLE_POOL.String())
weth.Approve(cross(cur), poolAddress, 1000000)
usdc.Approve(cross(cur), poolAddress, 2000000000)
```
## Usage
These snippets call the public domain proxy from a realm function with a current `cur` token.
Import the proxy package and qualify its function names in integrating code.
```go
// Mint new position
tokenId, liquidity, amount0, amount1 := Mint(
cross(cur),
"gno.land/r/gnoland/wugnot.wugnot", // token0
"gno.land/r/gnoswap/gns.GNS", // token1
3000, // fee
-887220, // tickLower
887220, // tickUpper
"1000000", // amount0Desired
"2000000000", // amount1Desired
"950000", // amount0Min
"1900000000", // amount1Min
deadline,
recipient, // mintTo
"", // referrer
)
// Add liquidity
positionId, liquidity, amount0, amount1, poolPath := IncreaseLiquidity(
cross(cur),
tokenId,
"500000",
"1000000000",
"475000",
"950000000",
deadline,
)
// Collect fees
positionId, collected0, collected1, poolPath, rawAmount0, rawAmount1 := CollectFee(
cross(cur),
tokenId,
)
// Reposition to new range (requires cleared position)
positionId, liquidity, tickLower, tickUpper, amount0, amount1 := Reposition(
cross(cur),
tokenId,
-443610, // new tickLower
443610, // new tickUpper
"1000000", // amount0Desired
"2000000000", // amount1Desired
"950000", // amount0Min
"1900000000", // amount1Min
deadline,
)
```
## Lifecycle
A full decrease that leaves zero liquidity and zero tokens owed sets the
`burned` marker but does not destroy the NFT. `IncreaseLiquidity` and
`Reposition` clear the marker when the position is used again; the marker does
not by itself block an increase.
## Security
- Tick range validation prevents invalid positions
- Slippage protection applies to liquidity-changing operations; fee collection
has no amount-minimum parameter
- Deadlines prevent stale liquidity-changing transactions
- Unstaked NFTs follow standard GRC721 transfer authorization; staked NFTs
can move only through staker-mediated flows
- Liquidity changes and repositioning require the owner; fee collection also
permits the position's approved operator where applicable
CollectFee(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, positionId uint64) (uint64, string, string, string, string, string)
DecreaseLiquidity(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, positionId uint64, liquidityStr string, amount0MinStr string, amount1MinStr string, deadline int64) (uint64, string, string, string, string, string, string)
GetImplementationPackagePath() string
GetPositionFeeGrowthInside0LastX128(positionId uint64) (string, interface {Error func() string})
GetPositionFeeGrowthInside1LastX128(positionId uint64) (string, interface {Error func() string})
GetPositionFeeGrowthInsideLastX128(positionId uint64) (string, string, interface {Error func() string})
GetPositionLiquidity(positionId uint64) (string, interface {Error func() string})
GetPositionOperator(positionId uint64) (string, interface {Error func() string})
GetPositionOwner(positionId uint64) (string, interface {Error func() string})
GetPositionPoolKey(positionId uint64) (string, interface {Error func() string})
GetPositions() *gno.land/p/nt/bptree/rotree/v0.ReadOnlyTree
GetPositionTickLower(positionId uint64) (int32, interface {Error func() string})
GetPositionTicks(positionId uint64) (int32, int32, interface {Error func() string})
GetPositionTickUpper(positionId uint64) (int32, interface {Error func() string})
GetPositionToken0Balance(positionId uint64) (int64, interface {Error func() string})
GetPositionToken1Balance(positionId uint64) (int64, interface {Error func() string})
GetPositionTokenBalances(positionId uint64) (int64, int64, interface {Error func() string})
GetPositionTokensOwed(positionId uint64) (int64, int64, interface {Error func() string})
GetPositionTokensOwed0(positionId uint64) (int64, interface {Error func() string})
GetPositionTokensOwed1(positionId uint64) (int64, interface {Error func() string})
GetUnclaimedFee(positionId uint64) (string, string, interface {Error func() string})
IncreaseLiquidity(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, positionId uint64, amount0DesiredStr string, amount1DesiredStr string, amount0MinStr string, amount1MinStr string, deadline int64) (uint64, string, string, string, string)
IsBurned(positionId uint64) (bool, interface {Error func() string})
IsInRange(positionId uint64) (bool, interface {Error func() string})
Mint(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, token0 string, token1 string, fee uint32, tickLower int32, tickUpper int32, amount0Desired string, amount1Desired string, amount0Min string, amount1Min string, deadline int64, mintTo string, referrer string) (uint64, string, string, string)
NewPosition(poolKey string, tickLower int32, tickUpper int32, liquidity string, feeGrowthInside0LastX128 string, feeGrowthInside1LastX128 string, tokensOwed0 int64, tokensOwed1 int64, burned bool, operator string) *gno.land/r/gnoswap/position.Position
NewPositionStore(kvStore interface {AddAuthorizedCaller func(int, .uverse.realm, .uverse.address, gno.land/p/gnoswap/store/v1.Permission) .uverse.error; Delete func(int, .uverse.realm, string) .uverse.error; Get func(string) (interface {}, .uverse.error); GetAddress func(string) (.uverse.address, .uverse.error); GetAllKeys func() ([]string, .uverse.error); GetAuthorizedCallers func() (map[.uverse.address]gno.land/p/gnoswap/store/v1.Permission, .uverse.error); GetBPTree func(string) (*gno.land/p/nt/bptree/v0.BPTree, .uverse.error); GetBool func(string) (bool, .uverse.error); GetDomainAddress func() .uverse.address; GetInt64 func(string) (int64, .uverse.error); GetString func(string) (string, .uverse.error); GetUint64 func(string) (uint64, .uverse.error); Has func(string) bool; IsWriteAuthorized func(.uverse.address) bool; RemoveAuthorizedCaller func(int, .uverse.realm, .uverse.address) .uverse.error; Set func(int, .uverse.realm, string, interface {}) .uverse.error; UpdateAuthorizedCaller func(int, .uverse.realm, .uverse.address, gno.land/p/gnoswap/store/v1.Permission) .uverse.error}) interface {GetPosition func(uint64) (gno.land/r/gnoswap/position.Position, bool); GetPositionNextID func() uint64; GetPositions func() *gno.land/p/nt/bptree/v0.BPTree; HasPosition func(uint64) bool; HasPositionNextIDStoreKey func() bool; HasPositionsStoreKey func() bool; RemovePosition func(int, .uverse.realm, uint64) .uverse.error; SetPosition func(int, .uverse.realm, uint64, gno.land/r/gnoswap/position.Position) .uverse.error; SetPositionNextID func(int, .uverse.realm, uint64) .uverse.error; SetPositions func(int, .uverse.realm, *gno.land/p/nt/bptree/v0.BPTree) .uverse.error}
NewPositionsTree() *gno.land/p/nt/bptree/v0.BPTree
RegisterInitializer(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, initializer func(int, .uverse.realm, gno.land/r/gnoswap/position.IPositionStore) gno.land/r/gnoswap/position.IPosition)
Render(path string) string
Reposition(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, positionId uint64, tickLower int32, tickUpper int32, amount0DesiredStr string, amount1DesiredStr string, amount0MinStr string, amount1MinStr string, deadline int64) (uint64, string, int32, int32, string, string)
SetPositionOperator(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, positionId uint64, operator string)
UpgradeImpl(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, packagePath string)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.