Transaction

BFF90BAA75D881…DB7E8D45717E

Block 77,266 · index 0 · indexed

Summary

Hash
BFF90BAA75D8819C7A6BFB0A0380E929BE67686DD25C692A508CDB7E8D45717E
Block
77,266
Size
88267 bytes
Gas used
109,343,391 / 131,212,022
Fee
131213 ugnot
Status
success

Messages

#1AddPackagegno.land/r/gnoswap/position/v124 arguments

Arguments · 24

  1. #1position
  2. #2README.md
  3. #3# 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. ## 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 a new position through the domain proxy 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 swap fees positionId, collected0, collected1, poolPath, rawAmount0, rawAmount1 := CollectFee( cross(cur), tokenId, ) // Reposition to a new range (requires a clear 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
  4. #4assert.gno
  5. #5package position import ( "errors" "time" u256 "gno.land/p/gnoswap/uint256/v1" ufmt "gno.land/p/nt/ufmt/v0" prbac "gno.land/p/gnoswap/rbac/v1" "gno.land/r/gnoswap/access/v1" ) // assertIsNotExpired panics if the deadline is expired. func assertIsNotExpired(deadline int64) { now := time.Now().Unix() if now > deadline { panic(makeErrorWithDetails( errExpired, ufmt.Sprintf("transaction too old, now(%d) > deadline(%d)", now, deadline), )) } } // assertValidNumberString panics if the input string does not represent a valid integer. func assertValidNumberString(input string) { if len(input) == 0 { panic(newErrorWithDetail(errInvalidInput, "input is empty")) } bytes := []byte(input) for i, b := range bytes { if i == 0 && b == '-' { continue // Allow if the first character is a negative sign (-) } if b < '0' || b > '9' { panic(newErrorWithDetail( errInvalidInput, ufmt.Sprintf("input string : %s", input))) } } } // assertValidLiquidityAmount panics if the liquidity amount is zero. func assertValidLiquidityAmount(liquidity string) { if u256.MustFromDecimal(liquidity).IsZero() { panic(newErrorWithDetail( errZeroLiquidity, ufmt.Sprintf("liquidity amount must be greater than 0, got %s", liquidity), )) } } // assertExistsPosition panics if the position does not exist. func assertExistsPosition(p *positionV1, positionId uint64) { if !p.exists(positionId) { panic(newErrorWithDetail( errPositionDoesNotExist, ufmt.Sprintf("position with position ID(%d) doesn't exist", positionId), )) } } // assertIsOwnerForToken panics if caller is not the owner of the position. func assertIsOwnerForToken(p *positionV1, positionId uint64, caller address) { assertExistsPosition(p, positionId) if !p.isOwner(positionId, caller) { panic(newErrorWithDetail( errNoPermission, ufmt.Sprintf("caller(%s) is not owner of positionId(%d)", caller, positionId), )) } } // assertIsOwnerOrOperatorForToken panics if caller is not the owner or operator of the position. func assertIsOwnerOrOperatorForToken(p *positionV1, positionId uint64, caller address) { assertExistsPosition(p, positionId) if !p.isOwnerOrOperator(positionId, caller) { panic(newErrorWithDetail( errNoPermission, ufmt.Sprintf("caller(%s) is not owner or approved operator of positionId(%d)", caller, positionId), )) } } // assertEqualsAddress panics if addresses are invalid or not equal. func assertEqualsAddress(prevAddr, otherAddr address) { access.AssertIsValidAddress(prevAddr) access.AssertIsValidAddress(otherAddr) if prevAddr != otherAddr { panic(newErrorWithDetail( errInvalidAddress, ufmt.Sprintf("(%s, %s)", prevAddr, otherAddr), )) } } // assertValidOperatorAddress validates operator address. // Empty address is allowed (for operator removal), but non-empty addresses must be valid. func assertValidOperatorAddress(operator address) { if operator != address("") && !operator.IsValid() { panic(newErrorWithDetail(errInvalidAddress, ufmt.Sprintf("operator(%s)", operator))) } } // assertIsNotMintToStaker panics if the mintTo address is staker. func assertIsNotMintToStaker(mintTo address) { stakerAddr := access.MustGetAddress(prbac.ROLE_STAKER.String()) if mintTo == stakerAddr { panic(errors.New(errCannotMintToStaker)) } }
  6. #6burn.gno
  7. #7package position import ( "errors" "gno.land/p/gnoswap/consts/v1" "gno.land/p/gnoswap/gnsmath/v1" u256 "gno.land/p/gnoswap/uint256/v1" "gno.land/p/gnoswap/utils/v1" ufmt "gno.land/p/nt/ufmt/v0" pl "gno.land/r/gnoswap/pool" "gno.land/r/gnoswap/position" ) // decreaseLiquidity reduces position liquidity and collects fees. // Returns positionId, liquidity, fee0, fee1, amount0, amount1, poolPath. func (p *positionV1) decreaseLiquidity(_ int, rlm realm, params DecreaseLiquidityParams) (uint64, string, string, string, string, string, string, error) { caller := params.caller // before decrease liquidity, collect fee first _, fee0Str, fee1Str, _, _, _ := p.collectFee(0, rlm, params.positionId, params.caller) position := p.mustGetPosition(params.positionId) positionLiquidity := u256.MustFromDecimal(position.Liquidity()) if positionLiquidity.IsZero() { return params.positionId, "", fee0Str, fee1Str, "", "", position.PoolKey(), makeErrorWithDetails( errZeroLiquidity, ufmt.Sprintf("position(position ID:%d) has 0 liquidity", params.positionId), ) } liquidityToRemove := u256.MustFromDecimal(params.liquidity) if liquidityToRemove.Gt(positionLiquidity) { return params.positionId, liquidityToRemove.ToString(), fee0Str, fee1Str, "", "", position.PoolKey(), makeErrorWithDetails( errInvalidLiquidity, ufmt.Sprintf("Liquidity requested(%s) is greater than liquidity held(%s)", liquidityToRemove.ToString(), positionLiquidity.ToString()), ) } pToken0, pToken1, pFee := splitOf(position.PoolKey()) burn0, burn1 := pl.Burn(cross(rlm), pToken0, pToken1, pFee, position.TickLower(), position.TickUpper(), liquidityToRemove.ToString(), caller) burnedAmount0 := utils.SafeParseInt64(burn0) burnedAmount1 := utils.SafeParseInt64(burn1) if burnedAmount0 < 0 || burnedAmount1 < 0 { panic(errors.New(errUnderflow)) } positionKey := computePositionKey(position.TickLower(), position.TickUpper()) feeGrowthInside0LastX128Str, feeGrowthInside1LastX128Str, err := pl.GetPositionFeeGrowthInsideLastX128(position.PoolKey(), positionKey) if err != nil { return 0, "", "", "", "", "", position.PoolKey(), err } // Add only burned amounts to tokensOwed since fees were already collected and processed in collectFee tokensOwed0 := gnsmath.SafeAddInt64(position.TokensOwed0(), burnedAmount0) tokensOwed1 := gnsmath.SafeAddInt64(position.TokensOwed1(), burnedAmount1) newLiquidity, underflow := u256.Zero().SubOverflow(positionLiquidity, liquidityToRemove) if underflow { panic(newErrorWithDetail(errUnderflow, "positionLiquidity - liquidityToRemove underflow")) } position.SetTokensOwed0(tokensOwed0) position.SetTokensOwed1(tokensOwed1) position.SetFeeGrowthInside0LastX128(feeGrowthInside0LastX128Str) position.SetFeeGrowthInside1LastX128(feeGrowthInside1LastX128Str) position.SetLiquidity(newLiquidity.ToString()) p.mustUpdatePosition(0, rlm, params.positionId, *position) collect0, collect1 := pl.Collect( cross(rlm), pToken0, pToken1, pFee, caller, position.TickLower(), position.TickUpper(), burn0, burn1, ) collectAmount0 := u256.MustFromDecimal(collect0) collectAmount1 := u256.MustFromDecimal(collect1) // Slippage check on actually collected amounts to ensure user receives minimum expected tokens if isSlippageExceeded(collectAmount0, collectAmount1, params.amount0Min, params.amount1Min) { return params.positionId, liquidityToRemove.ToString(), fee0Str, fee1Str, collect0, collect1, position.PoolKey(), makeErrorWithDetails( errSlippage, ufmt.Sprintf("collectAmount0(%s) >= amount0Min(%s) && collectAmount1(%s) >= amount1Min(%s)", collectAmount0.ToString(), params.amount0Min.ToString(), collectAmount1.ToString(), params.amount1Min.ToString(), ), ) } // Check for underflow when subtracting collected amounts from tokens owed collectAmount0Int64 := gnsmath.SafeConvertToInt64(collectAmount0) collectAmount1Int64 := gnsmath.SafeConvertToInt64(collectAmount1) if position.TokensOwed0() < collectAmount0Int64 { panic(ufmt.Sprintf("[POSITION] burn.gno | collect() | tokensOwed0(%d) < collectAmount0(%d)", position.TokensOwed0(), collectAmount0Int64)) } position.SetTokensOwed0(gnsmath.SafeSubInt64(position.TokensOwed0(), collectAmount0Int64)) if position.TokensOwed1() < collectAmount1Int64 { panic(ufmt.Sprintf("[POSITION] burn.gno | collect() | tokensOwed1(%d) < collectAmount1(%d)", position.TokensOwed1(), collectAmount1Int64)) } position.SetTokensOwed1(gnsmath.SafeSubInt64(position.TokensOwed1(), collectAmount1Int64)) if position.IsClear() { position.SetBurned(true) // just update flag (we don't want to burn actual position) } p.mustUpdatePosition(0, rlm, params.positionId, *position) return params.positionId, liquidityToRemove.ToString(), fee0Str, fee1Str, collect0, collect1, position.PoolKey(), nil } // calculateFees calculates the fees for the current position. func (p *positionV1) calculateFees(position *position.Position, currentFeeGrowth FeeGrowthInside) (int64, int64) { posLiquidity := u256.MustFromDecimal(position.Liquidity()) fee0 := calculateTokensOwed( currentFeeGrowth.feeGrowthInside0LastX128, u256.MustFromDecimal(position.FeeGrowthInside0LastX128()), posLiquidity, ) fee1 := calculateTokensOwed( currentFeeGrowth.feeGrowthInside1LastX128, u256.MustFromDecimal(position.FeeGrowthInside1LastX128()), posLiquidity, ) return gnsmath.SafeAddInt64(position.TokensOwed0(), gnsmath.SafeConvertToInt64(fee0)), gnsmath.SafeAddInt64(position.TokensOwed1(), gnsmath.SafeConvertToInt64(fee1)) } func calculateTokensOwed( feeGrowthInsideLastX128 *u256.Uint, positionFeeGrowthInsideLastX128 *u256.Uint, positionLiquidity *u256.Uint, ) *u256.Uint { diff := u256.Zero().Sub(feeGrowthInsideLastX128, positionFeeGrowthInsideLastX128) return u256.MulDiv(diff, positionLiquidity, consts.Q128()) }
  8. #8doc.gno
  9. #9// package v1 manages liquidity positions as NFTs in GnoSwap pools. // // Each position is represented as a GRC721 NFT with concentrated liquidity // within a specific tick range. Unstaked NFTs follow GRC721 owner/approval/ // operator transfer rules; staked NFTs are locked to staker-mediated transfers. // The package handles minting, liquidity adjustments, fee collection, and // repositioning. package position
  10. #10errors.gno
  11. #11package position import ( ufmt "gno.land/p/nt/ufmt/v0" ) const ( errNoPermission = "[GNOSWAP-POSITION-001] caller has no permission" errSlippage = "[GNOSWAP-POSITION-002] slippage failed" errInvalidTokenPath = "[GNOSWAP-POSITION-003] invalid token address" errInvalidInput = "[GNOSWAP-POSITION-004] invalid input data" errDataNotFound = "[GNOSWAP-POSITION-005] requested data not found" errExpired = "[GNOSWAP-POSITION-006] transaction expired" errInvalidLiquidity = "[GNOSWAP-POSITION-007] invalid liquidity" errNotClear = "[GNOSWAP-POSITION-008] position is not clear" errZeroLiquidity = "[GNOSWAP-POSITION-009] zero liquidity" errPositionExist = "[GNOSWAP-POSITION-010] position with same positionId already exists" errInvalidAddress = "[GNOSWAP-POSITION-011] invalid address" errPositionDoesNotExist = "[GNOSWAP-POSITION-012] position does not exist" errUnderflow = "[GNOSWAP-POSITION-013] underflow" errOverflow = "[GNOSWAP-POSITION-014] overflow" errCannotMintToStaker = "[GNOSWAP-POSITION-015] cannot mint to staker" errSpoofedRealm = "[GNOSWAP-POSITION-016] rlm does not match the current crossing frame" ) // newErrorWithDetail appends additional context or details to an existing error message. func newErrorWithDetail(message string, detail string) string { return ufmt.Errorf("%s || %s", message, detail).Error() } // makeErrorWithDetails creates an error with additional context. func makeErrorWithDetails(message string, details string) error { return ufmt.Errorf("%s || %s", message, details) }
  12. #12getter.gno
  13. #13package position import ( "errors" "gno.land/p/gnoswap/consts/v1" u256 "gno.land/p/gnoswap/uint256/v1" rotree "gno.land/p/nt/bptree/rotree/v0" ufmt "gno.land/p/nt/ufmt/v0" pl "gno.land/r/gnoswap/pool" "gno.land/r/gnoswap/position" ) // getPosition returns a position for a given position ID. // Returns an error if the position doesn't exist. func (p *positionV1) getPosition(positionId uint64) (position.Position, error) { pos, exists := p.store.GetPosition(positionId) if !exists { return pos, errors.New(newErrorWithDetail( errPositionDoesNotExist, ufmt.Sprintf("position with position ID(%d) doesn't exist", positionId), )) } return pos, nil } // mustGetPosition returns a position for a given position ID. // panics if position doesn't exist func (p *positionV1) mustGetPosition(positionId uint64) *position.Position { position, err := p.getPosition(positionId) if err != nil { panic(err.Error()) } return &position } // GetPositions returns a read-only view of every position, keyed by the decimal // string form of the position ID. // // Returns: // - positions: read-only tree exposing the position store without mutation methods func (p *positionV1) GetPositions() *rotree.ReadOnlyTree { // makeEntrySafeFn is a function that makes an entry safe to read. // But Position is immutable, so we can just return the entry as is. return rotree.Wrap(p.store.GetPositions(), nil) } // ExistPosition reports whether a position exists for a given position ID. // // Parameters: // - positionId: numeric ID to test in the position store // // Returns: // - exists: true when the position store contains positionId func (p *positionV1) ExistPosition(positionId uint64) bool { return p.store.HasPosition(positionId) } // IsBurned reports the empty-position marker stored for a position. // // Parameters: // - positionId: numeric ID whose burned marker is requested // // Returns: // - burned: true when the position is marked empty; the NFT record itself may still exist // - err: nil on lookup success; errPositionDoesNotExist when the position is absent func (p *positionV1) IsBurned(positionId uint64) (bool, error) { position, err := p.getPosition(positionId) if err != nil { return false, err } return position.Burned(), nil } // IsInRange reports whether the pool's current tick is inside a position's range. // // Parameters: // - positionId: numeric ID whose tick range and pool are inspected // // Returns: // - inRange: true when tickLower <= current pool tick < tickUpper // - err: nil on success; an error when the position or its pool tick cannot be read func (p *positionV1) IsInRange(positionId uint64) (bool, error) { position, err := p.getPosition(positionId) if err != nil { return false, err } poolPath := position.PoolKey() poolCurrentTick, err := pl.GetSlot0Tick(poolPath) if err != nil { return false, err } return position.TickLower() <= poolCurrentTick && poolCurrentTick < position.TickUpper(), nil } // GetPositionOperator returns the address approved to operate on a position. // // Parameters: // - positionId: numeric ID whose operator is requested // // Returns: // - operator: approved operator address, or the empty address when none is set // - err: nil on lookup success; errPositionDoesNotExist when the position is absent func (p *positionV1) GetPositionOperator(positionId uint64) (address, error) { position, err := p.getPosition(positionId) if err != nil { return address(""), err } return position.Operator(), nil } // GetPositionPoolKey returns the pool identifier stored on a position. // // Parameters: // - positionId: numeric ID whose pool key is requested // // Returns: // - poolKey: pool key encoding the position's token pair and fee tier // - err: nil on lookup success; errPositionDoesNotExist when the position is absent func (p *positionV1) GetPositionPoolKey(positionId uint64) (string, error) { position, err := p.getPosition(positionId) if err != nil { return "", err } return position.PoolKey(), nil } // GetPositionTickLower returns the lower tick boundary stored on a position. // // Parameters: // - positionId: numeric ID whose lower tick is requested // // Returns: // - tickLower: lower tick boundary, inclusive in range checks // - err: nil on lookup success; errPositionDoesNotExist when the position is absent func (p *positionV1) GetPositionTickLower(positionId uint64) (int32, error) { position, err := p.getPosition(positionId) if err != nil { return 0, err } return position.TickLower(), nil } // GetPositionTickUpper returns the upper tick boundary stored on a position. // // Parameters: // - positionId: numeric ID whose upper tick is requested // // Returns: // - tickUpper: upper tick boundary, exclusive in range checks // - err: nil on lookup success; errPositionDoesNotExist when the position is absent func (p *positionV1) GetPositionTickUpper(positionId uint64) (int32, error) { position, err := p.getPosition(positionId) if err != nil { return 0, err } return position.TickUpper(), nil } // GetPositionLiquidity returns the decimal-encoded liquidity stored on a position. // // Parameters: // - positionId: numeric ID whose liquidity is requested // // Returns: // - liquidity: decimal string representing the position's current liquidity // - err: nil on lookup success; errPositionDoesNotExist when the position is absent func (p *positionV1) GetPositionLiquidity(positionId uint64) (string, error) { position, err := p.getPosition(positionId) if err != nil { return "", err } return position.Liquidity(), nil } // GetPositionTokenBalances returns balances derived from current pool price, // position range, and liquidity; token balances are not stored snapshots. // // Parameters: // - positionId: numeric ID whose current token balances are calculated // // Returns: // - token0Balance: current token0 amount represented by the position's liquidity and range // - token1Balance: current token1 amount represented by the position's liquidity and range // - err: nil when the position exists; errPositionDoesNotExist when it is absent (pool price lookup failures panic during calculation) func (p *positionV1) GetPositionTokenBalances(positionId uint64) (int64, int64, error) { position, err := p.getPosition(positionId) if err != nil { return 0, 0, err } balance0, balance1 := calculatePositionBalances(&position) return balance0, balance1, nil } // GetPositionFeeGrowthInside0LastX128 returns the token0 fee-growth checkpoint // stored on a position. // // Parameters: // - positionId: numeric ID whose token0 fee-growth checkpoint is requested // // Returns: // - feeGrowthInside0LastX128: decimal-encoded Q128 token0 fee growth inside the position's range at its last update // - err: nil on lookup success; errPositionDoesNotExist when the position is absent func (p *positionV1) GetPositionFeeGrowthInside0LastX128(positionId uint64) (string, error) { position, err := p.getPosition(positionId) if err != nil { return "", err } return position.FeeGrowthInside0LastX128(), nil } // GetPositionFeeGrowthInside1LastX128 returns the token1 fee-growth checkpoint // stored on a position. // // Parameters: // - positionId: numeric ID whose token1 fee-growth checkpoint is requested // // Returns: // - feeGrowthInside1LastX128: decimal-encoded Q128 token1 fee growth inside the position's range at its last update // - err: nil on lookup success; errPositionDoesNotExist when the position is absent func (p *positionV1) GetPositionFeeGrowthInside1LastX128(positionId uint64) (string, error) { position, err := p.getPosition(positionId) if err != nil { return "", err } return position.FeeGrowthInside1LastX128(), nil } // GetPositionFeeGrowthInsideLastX128 returns both fee-growth checkpoints stored // on a position, in token0 then token1 order. // // Parameters: // - positionId: numeric ID whose fee-growth checkpoints are requested // // Returns: // - feeGrowthInside0LastX128: decimal-encoded Q128 token0 fee growth checkpoint // - feeGrowthInside1LastX128: decimal-encoded Q128 token1 fee growth checkpoint // - err: nil on lookup success; errPositionDoesNotExist when the position is absent func (p *positionV1) GetPositionFeeGrowthInsideLastX128(positionId uint64) (string, string, error) { position, err := p.getPosition(positionId) if err != nil { return "", "", err } return position.FeeGrowthInside0LastX128(), position.FeeGrowthInside1LastX128(), nil } // GetPositionTicks returns both tick boundaries stored on a position. // // Parameters: // - positionId: numeric ID whose tick boundaries are requested // // Returns: // - tickLower: lower tick boundary, inclusive in range checks // - tickUpper: upper tick boundary, exclusive in range checks // - err: nil on lookup success; errPositionDoesNotExist when the position is absent func (p *positionV1) GetPositionTicks(positionId uint64) (int32, int32, error) { position, err := p.getPosition(positionId) if err != nil { return 0, 0, err } return position.TickLower(), position.TickUpper(), nil } // GetPositionTokensOwed0 returns the stored token0 amount owed to a position. // // Parameters: // - positionId: numeric ID whose token0 owed amount is requested // // Returns: // - tokensOwed0: accrued token0 amount awaiting collection, in token units // - err: nil on lookup success; errPositionDoesNotExist when the position is absent func (p *positionV1) GetPositionTokensOwed0(positionId uint64) (int64, error) { position, err := p.getPosition(positionId) if err != nil { return 0, err } return position.TokensOwed0(), nil } // GetPositionTokensOwed1 returns the stored token1 amount owed to a position. // // Parameters: // - positionId: numeric ID whose token1 owed amount is requested // // Returns: // - tokensOwed1: accrued token1 amount awaiting collection, in token units // - err: nil on lookup success; errPositionDoesNotExist when the position is absent func (p *positionV1) GetPositionTokensOwed1(positionId uint64) (int64, error) { position, err := p.getPosition(positionId) if err != nil { return 0, err } return position.TokensOwed1(), nil } // GetPositionTokensOwed returns both stored token amounts owed to a position, // in token0 then token1 order. // // Parameters: // - positionId: numeric ID whose owed amounts are requested // // Returns: // - tokensOwed0: accrued token0 amount awaiting collection, in token units // - tokensOwed1: accrued token1 amount awaiting collection, in token units // - err: nil on lookup success; errPositionDoesNotExist when the position is absent func (p *positionV1) GetPositionTokensOwed(positionId uint64) (int64, int64, error) { position, err := p.getPosition(positionId) if err != nil { return 0, 0, err } return position.TokensOwed0(), position.TokensOwed1(), nil } // GetPositionOwner returns the address that owns the position NFT. // // Parameters: // - positionId: numeric ID whose NFT ownership is requested // // Returns: // - owner: current owner address reported by the NFT accessor // - err: nil when the NFT accessor finds the token; otherwise its ownership lookup error func (p *positionV1) GetPositionOwner(positionId uint64) (address, error) { owner, err := p.nftAccessor.OwnerOf(positionIdFrom(positionId)) if err != nil { return address(""), err } return owner, nil } // GetUnclaimedFee calculates unclaimed token fees from current in-range // fee growth, the position's checkpoints, and its liquidity. // // Parameters: // - positionId: numeric ID whose unclaimed token fees are calculated // // Returns: // - unclaimedFee0: token0 fees accrued since the stored checkpoint, scaled by position liquidity // - unclaimedFee1: token1 fees accrued since the stored checkpoint, scaled by position liquidity // - err: nil on success; nil fee pointers and the underlying lookup error when position or pool data is unavailable func (p *positionV1) GetUnclaimedFee(positionId uint64) (*u256.Uint, *u256.Uint, error) { // ref: https://blog.uniswap.org/uniswap-v3-math-primer-2#calculating-uncollected-fees position, err := p.getPosition(positionId) if err != nil { return nil, nil, err } liquidity := u256.MustFromDecimal(position.Liquidity()) tickLower := position.TickLower() tickUpper := position.TickUpper() poolKey := position.PoolKey() currentTick, err := pl.GetSlot0Tick(poolKey) if err != nil { return nil, nil, err } feeGrowthGlobal0X128Str, feeGrowthGlobal1X128Str, err := pl.GetFeeGrowthGlobalX128(poolKey) if err != nil { return nil, nil, err } feeGrowthGlobal0X128 := u256.MustFromDecimal(feeGrowthGlobal0X128Str) feeGrowthGlobal1X128 := u256.MustFromDecimal(feeGrowthGlobal1X128Str) tickUpperFeeGrowthOutside0X128Str, tickUpperFeeGrowthOutside1X128Str, err := pl.GetTickFeeGrowthOutsideX128(poolKey, tickUpper) if err != nil { return nil, nil, err } tickUpperFeeGrowthOutside0X128 := u256.MustFromDecimal(tickUpperFeeGrowthOutside0X128Str) tickUpperFeeGrowthOutside1X128 := u256.MustFromDecimal(tickUpperFeeGrowthOutside1X128Str) tickLowerFeeGrowthOutside0X128Str, tickLowerFeeGrowthOutside1X128Str, err := pl.GetTickFeeGrowthOutsideX128(poolKey, tickLower) if err != nil { return nil, nil, err } tickLowerFeeGrowthOutside0X128 := u256.MustFromDecimal(tickLowerFeeGrowthOutside0X128Str) tickLowerFeeGrowthOutside1X128 := u256.MustFromDecimal(tickLowerFeeGrowthOutside1X128Str) feeGrowthInside0LastX128 := u256.MustFromDecimal(position.FeeGrowthInside0LastX128()) feeGrowthInside1LastX128 := u256.MustFromDecimal(position.FeeGrowthInside1LastX128()) var tickLowerFeeGrowthBelow0, tickLowerFeeGrowthBelow1, tickUpperFeeGrowthAbove0, tickUpperFeeGrowthAbove1 *u256.Uint if currentTick >= tickUpper { tickUpperFeeGrowthAbove0 = u256.Zero().Sub(feeGrowthGlobal0X128, tickUpperFeeGrowthOutside0X128) tickUpperFeeGrowthAbove1 = u256.Zero().Sub(feeGrowthGlobal1X128, tickUpperFeeGrowthOutside1X128) } else { tickUpperFeeGrowthAbove0 = tickUpperFeeGrowthOutside0X128 tickUpperFeeGrowthAbove1 = tickUpperFeeGrowthOutside1X128 } if currentTick >= tickLower { tickLowerFeeGrowthBelow0 = tickLowerFeeGrowthOutside0X128 tickLowerFeeGrowthBelow1 = tickLowerFeeGrowthOutside1X128 } else { tickLowerFeeGrowthBelow0 = u256.Zero().Sub(feeGrowthGlobal0X128, tickLowerFeeGrowthOutside0X128) tickLowerFeeGrowthBelow1 = u256.Zero().Sub(feeGrowthGlobal1X128, tickLowerFeeGrowthOutside1X128) } feeGrowthInside0X128 := u256.Zero().Sub(feeGrowthGlobal0X128, tickLowerFeeGrowthBelow0) feeGrowthInside0X128 = u256.Zero().Sub(feeGrowthInside0X128, tickUpperFeeGrowthAbove0) feeGrowthInside1X128 := u256.Zero().Sub(feeGrowthGlobal1X128, tickLowerFeeGrowthBelow1) feeGrowthInside1X128 = u256.Zero().Sub(feeGrowthInside1X128, tickUpperFeeGrowthAbove1) diffGrowthInside0X128 := u256.Zero().Sub(feeGrowthInside0X128, feeGrowthInside0LastX128) unclaimedFee0 := u256.MulDiv(diffGrowthInside0X128, liquidity, consts.Q128()) diffGrowthInside1X128 := u256.Zero().Sub(feeGrowthInside1X128, feeGrowthInside1LastX128) unclaimedFee1 := u256.MulDiv(diffGrowthInside1X128, liquidity, consts.Q128()) return unclaimedFee0, unclaimedFee1, nil }
  14. #14gnomod.toml
  15. #15module = "gno.land/r/gnoswap/position/v1" gno = "0.9"
  16. #16init.gno
  17. #17package position import ( "gno.land/r/gnoswap/access/v1" "gno.land/r/gnoswap/position" ) func init(cur realm) { registerPositionV1(cur) } func registerPositionV1(cur realm) { position.RegisterInitializer( cross(cur), func(_ int, rlm realm, positionStore position.IPositionStore) position.IPosition { access.AssertIsRlmCurrent(0, rlm) err := initStoreData(0, rlm, positionStore) if err != nil { panic(err) } return NewPositionV1(positionStore, newGNFTAccessor()) }, ) } func initStoreData(_ int, rlm realm, positionStore position.IPositionStore) error { if !positionStore.HasPositionNextIDStoreKey() { err := positionStore.SetPositionNextID(0, rlm, uint64(1)) if err != nil { return err } } if !positionStore.HasPositionsStoreKey() { err := positionStore.SetPositions(0, rlm, position.NewPositionsTree()) if err != nil { return err } } return nil }
  18. #18instance.gno
  19. #19package position import ( "errors" "gno.land/p/nt/grc721/v0" "gno.land/r/gnoswap/access/v1" "gno.land/r/gnoswap/gnft" "gno.land/r/gnoswap/position" ) type positionV1 struct { store position.IPositionStore nftAccessor NFTAccessor } // NewPositionV1 constructs a position implementation backed by the supplied // position store and NFT accessor. // // Parameters: // - positionStore: storage interface used to read and update position records // - accessor: NFT interface used for position token minting, burning, approval, and ownership // // Returns: // - position: IPosition implementation connected to positionStore and accessor func NewPositionV1(positionStore position.IPositionStore, accessor NFTAccessor) position.IPosition { return &positionV1{ store: positionStore, nftAccessor: accessor, } } type NFTAccessor interface { // Approve forwards an operator approval for a position NFT. // // Parameters: // - _: leading realm-call discriminator for the forwarded NFT operation; callers pass 0 // - rlm: propagated realm context validated by the concrete accessor before crossing into the NFT realm // - approved: address to approve for the specified position NFT; the empty address revokes approval // - tid: position NFT token ID whose operator approval is changed // // Returns: // - err: nil when approval succeeds; otherwise the NFT accessor or realm-validation error Approve(_ int, rlm realm, approved address, tid grc721.TokenID) error // Mint creates a position NFT for an address and token ID. // // Parameters: // - _: leading realm-call discriminator for the forwarded NFT operation; callers pass 0 // - rlm: propagated realm context used to cross into the NFT realm // - to: address that receives the newly minted position NFT // - tid: requested position NFT token ID // // Returns: // - mintedTid: token ID returned by the NFT realm after minting Mint(_ int, rlm realm, to address, tid grc721.TokenID) grc721.TokenID // Burn removes a position NFT from the NFT ledger. // // Parameters: // - _: leading realm-call discriminator for the forwarded NFT operation; callers pass 0 // - rlm: propagated realm context used to cross into the NFT realm // - tid: position NFT token ID to burn Burn(_ int, rlm realm, tid grc721.TokenID) // TotalSupply returns the number of NFTs currently recorded by the ledger. // // Returns: // - totalSupply: current number of position NFTs in the ledger TotalSupply() int64 // Exists reports whether the NFT ledger contains a token ID. // // Parameters: // - tid: position NFT token ID to test // // Returns: // - exists: true when the NFT ledger contains tid Exists(tid grc721.TokenID) bool // OwnerOf returns the current owner of a position NFT. // // Parameters: // - tid: position NFT token ID whose owner is requested // // Returns: // - owner: address recorded as the token owner // - err: nil when the token exists; otherwise the NFT ledger's ownership lookup error OwnerOf(tid grc721.TokenID) (address, error) } type gnftAccessor struct{} // Approve forwards an operator approval to the GNFT realm. // // Parameters: // - _: leading realm-call discriminator for the forwarded NFT operation; callers pass 0 // - rlm: propagated realm context that must be current before crossing into GNFT // - approved: address to approve for the specified position NFT; the empty address revokes approval // - tid: position NFT token ID whose operator approval is changed // // Returns: // - err: nil when GNFT accepts the approval; errSpoofedRealm when rlm is not current, otherwise the underlying GNFT error func (n *gnftAccessor) Approve(_ int, rlm realm, approved address, tid grc721.TokenID) error { if !rlm.IsCurrent() { return errors.New(errSpoofedRealm) } return gnft.Approve(cross(rlm), approved, tid) } // Mint forwards position NFT creation to the GNFT realm. // // Parameters: // - _: leading realm-call discriminator for the forwarded NFT operation; callers pass 0 // - rlm: propagated realm context asserted current before crossing into GNFT // - to: address that receives the newly minted position NFT // - tid: requested position NFT token ID // // Returns: // - mintedTid: token ID returned by GNFT after minting func (n *gnftAccessor) Mint(_ int, rlm realm, to address, tid grc721.TokenID) grc721.TokenID { access.AssertIsRlmCurrent(0, rlm) return gnft.Mint(cross(rlm), to, tid) } // Burn forwards position NFT destruction to the GNFT realm. // // Parameters: // - _: leading realm-call discriminator for the forwarded NFT operation; callers pass 0 // - rlm: propagated realm context asserted current before crossing into GNFT // - tid: position NFT token ID to burn func (n *gnftAccessor) Burn(_ int, rlm realm, tid grc721.TokenID) { access.AssertIsRlmCurrent(0, rlm) gnft.Burn(cross(rlm), tid) } // TotalSupply returns the GNFT ledger's current token count. // // Returns: // - totalSupply: number of NFTs currently recorded by GNFT func (n *gnftAccessor) TotalSupply() int64 { return gnft.TotalSupply() } // Exists reports whether GNFT contains a position NFT token ID. // // Parameters: // - tid: position NFT token ID to test // // Returns: // - exists: true when GNFT contains tid func (n *gnftAccessor) Exists(tid grc721.TokenID) bool { return gnft.Exists(tid) } // OwnerOf returns the owner recorded by GNFT for a position NFT. // // Parameters: // - tid: position NFT token ID whose owner is requested // // Returns: // - owner: address recorded as the token owner // - err: nil when GNFT finds the token; otherwise GNFT's ownership lookup error func (n *gnftAccessor) OwnerOf(tid grc721.TokenID) (address, error) { return gnft.OwnerOf(tid) } func newGNFTAccessor() NFTAccessor { return &gnftAccessor{} }
  20. #20liquidity_management.gno
  21. #21package position import ( "gno.land/p/gnoswap/gnsmath/v1" u256 "gno.land/p/gnoswap/uint256/v1" ufmt "gno.land/p/nt/ufmt/v0" pl "gno.land/r/gnoswap/pool" ) type AddLiquidityParams struct { poolKey string // poolPath of the pool which has the position tickLower int32 // lower end of the tick range for the position tickUpper int32 // upper end of the tick range for the position amount0Desired *u256.Uint // desired amount of token0 to be minted amount1Desired *u256.Uint // desired amount of token1 to be minted amount0Min *u256.Uint // minimum amount of token0 to be minted amount1Min *u256.Uint // minimum amount of token1 to be minted caller address // address to call the function } // addLiquidity calculates liquidity amounts and mints position tokens to a pool. func (p *positionV1) addLiquidity(_ int, rlm realm, params AddLiquidityParams) (*u256.Uint, *u256.Uint, *u256.Uint) { sqrtPriceX96String, err := pl.GetSlot0SqrtPriceX96(params.poolKey) if err != nil { panic(err) } sqrtPriceX96 := u256.MustFromDecimal(sqrtPriceX96String) sqrtRatioAX96 := gnsmath.TickMathGetSqrtRatioAtTick(params.tickLower) sqrtRatioBX96 := gnsmath.TickMathGetSqrtRatioAtTick(params.tickUpper) liquidity := gnsmath.GetLiquidityForAmounts( sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, params.amount0Desired, params.amount1Desired, ) token0, token1, fee := splitOf(params.poolKey) amount0Str, amount1Str := pl.Mint( cross(rlm), token0, token1, fee, params.tickLower, params.tickUpper, liquidity.ToString(), params.caller, ) amount0 := u256.MustFromDecimal(amount0Str) amount1 := u256.MustFromDecimal(amount1Str) amount0Cond := amount0.Gte(params.amount0Min) amount1Cond := amount1.Gte(params.amount1Min) if !(amount0Cond && amount1Cond) { panic(newErrorWithDetail( errSlippage, ufmt.Sprintf( "Price Slippage Check(amount0(%s) >= amount0Min(%s), amount1(%s) >= amount1Min(%s))", amount0Str, params.amount0Min.ToString(), amount1Str, params.amount1Min.ToString()), )) } return liquidity, amount0, amount1 }
  22. #22manager.gno
  23. #23package position import ( "gno.land/r/gnoswap/position" ) // GetNextId is the next position ID to be minted func (p *positionV1) getNextId() uint64 { return p.store.GetPositionNextID() } // incrementNextId increments the next position ID to be minted func (p *positionV1) incrementNextId(_ int, rlm realm) error { return p.store.SetPositionNextID(0, rlm, p.getNextId()+1) } // setPosition sets a position for a given position ID. // Returns false if position is newly created, true if position already existed and was updated. func (p *positionV1) setPosition(_ int, rlm realm, id uint64, pos position.Position) error { return p.store.SetPosition(0, rlm, id, pos) } // mustUpdatePosition updates a position for a given position ID. func (p *positionV1) mustUpdatePosition(_ int, rlm realm, id uint64, pos position.Position) { err := p.setPosition(0, rlm, id, pos) if err != nil { panic(err) } }
  24. #24mint.gno

Result log

msg:0,success:true,log:,events:[]