diff --git a/_deploy/r/gnoswap/consts/consts.gno b/_deploy/r/gnoswap/consts/consts.gno index d2ebff665..91d3523df 100644 --- a/_deploy/r/gnoswap/consts/consts.gno +++ b/_deploy/r/gnoswap/consts/consts.gno @@ -18,6 +18,7 @@ const ( // WRAP & UNWRAP const ( GNOT string = "gnot" + UGNOT string = "ugnot" WRAPPED_WUGNOT string = "gno.land/r/demo/wugnot" // defined in https://github.com/gnolang/gno/blob/81a88a2976ba9f2f9127ebbe7fb7d1e1f7fa4bd4/examples/gno.land/r/demo/wugnot/wugnot.gno#L19 diff --git a/pool/_helper_test.gno b/pool/_helper_test.gno index 34826edaa..6df8ca8c3 100644 --- a/pool/_helper_test.gno +++ b/pool/_helper_test.gno @@ -33,13 +33,17 @@ const ( oblPath string = "gno.land/r/onbloc/obl" quxPath string = "gno.land/r/onbloc/qux" - fee100 uint32 = 100 - fee500 uint32 = 500 - fee3000 uint32 = 3000 - maxApprove uint64 = 18446744073709551615 - max_timeout int64 = 9999999999 - + fee100 uint32 = 100 + fee500 uint32 = 500 + fee3000 uint32 = 3000 + fee10000 uint32 = 10000 + maxApprove uint64 = 18446744073709551615 + max_timeout int64 = 9999999999 maxSqrtPriceLimitX96 string = "1461446703485210103287273052203988822378723970341" + + TIER_1 uint64 = 1 + TIER_2 uint64 = 2 + TIER_3 uint64 = 3 ) const ( @@ -52,6 +56,7 @@ var ( admin = pusers.AddressOrName(consts.ADMIN) alice = pusers.AddressOrName(testutils.TestAddress("alice")) pool = pusers.AddressOrName(consts.POOL_ADDR) + bob = pusers.AddressOrName(testutils.TestAddress("bob")) protocolFee = pusers.AddressOrName(consts.PROTOCOL_FEE_ADDR) router = pusers.AddressOrName(consts.ROUTER_ADDR) adminRealm = std.NewUserRealm(users.Resolve(admin)) @@ -187,6 +192,22 @@ func TokenApprove(t *testing.T, tokenPath string, owner, spender pusers.AddressO } } +func CreatePool(t *testing.T, + token0 string, + token1 string, + fee uint32, + sqrtPriceX96 string, + caller std.Address) { + t.Helper() + + std.TestSetRealm(std.NewUserRealm(caller)) + poolPath := pl.GetPoolPath(token0, token1, fee) + if !pl.DoesPoolPathExist(poolPath) { + pl.CreatePool(token0, token1, fee, sqrtPriceX96) + sr.SetPoolTierByAdmin(poolPath, TIER_1) + } +} + func MintPosition(t *testing.T, token0 string, token1 string, @@ -373,6 +394,54 @@ func MintPositionAll(t *testing.T, caller std.Address) { } +func MakeMintPositionWithoutFee(t *testing.T) (uint64, string, string, string) { + t.Helper() + + // make actual data to test resetting not only position's state but also pool's state + std.TestSetRealm(adminRealm) + + // set pool create fee to 0 for testing + pl.SetPoolCreationFeeByAdmin(0) + CreatePool(t, barPath, fooPath, fee500, common.TickMathGetSqrtRatioAtTick(0).ToString(), users.Resolve(admin)) + + TokenApprove(t, barPath, admin, pool, consts.UINT64_MAX) + TokenApprove(t, fooPath, admin, pool, consts.UINT64_MAX) + + // mint position + return Mint( + barPath, + fooPath, + fee500, + -887270, + 887270, + "50000", + "50000", + "0", + "0", + max_timeout, + users.Resolve(admin), + users.Resolve(admin), + ) +} + +func LPTokenApprove(t *testing.T, owner, operator pusers.AddressOrName, tokenId uint64) { + t.Helper() + std.TestSetRealm(std.NewUserRealm(users.Resolve(owner))) + gnft.Approve(operator, tokenIdFrom(tokenId)) +} + +func LPTokenStake(t *testing.T, owner pusers.AddressOrName, tokenId uint64) { + t.Helper() + std.TestSetRealm(std.NewUserRealm(users.Resolve(owner))) + sr.StakeToken(tokenId) +} + +func LPTokenUnStake(t *testing.T, owner pusers.AddressOrName, tokenId uint64, unwrap bool) { + t.Helper() + std.TestSetRealm(std.NewUserRealm(users.Resolve(owner))) + sr.UnstakeToken(tokenId, unwrap) +} + func wugnotApprove(t *testing.T, owner, spender pusers.AddressOrName, amount uint64) { t.Helper() std.TestSetRealm(std.NewUserRealm(users.Resolve(owner))) @@ -569,7 +638,17 @@ func burnUsdc(addr pusers.AddressOrName) { usdc.Burn(addr, usdc.BalanceOf(addr)) } -func TestBeforeResetObject(t *testing.T) { +// burnAllNFT burns all NFTs +func burnAllNFT(t *testing.T) { + t.Helper() + + std.TestSetRealm(std.NewCodeRealm(consts.POSITION_PATH)) + for i := uint64(1); i <= gnft.TotalSupply(); i++ { + gnft.Burn(tokenIdFrom(i)) + } +} + +func TestBeforeResetPoolObject(t *testing.T) { // make some data pools = avl.NewTree() pools.Set("gno.land/r/gnoswap/v1/gns:gno.land/r/onbloc/usdc", &Pool{ @@ -604,3 +683,8 @@ func TestBurnTokens(t *testing.T) { uassert.Equal(t, foo.BalanceOf(a2u(addr01)), uint64(0)) // 100_000_000 -> 0 uassert.Equal(t, bar.BalanceOf(a2u(addr01)), uint64(0)) // 100_000_000 -> 0 } + +func TestBurnAllNFT(t *testing.T) { + burnAllNFT(t) + uassert.Equal(t, gnft.TotalSupply(), uint64(0), "gnft total supply should be 0") +} diff --git a/pool/position.gno b/pool/position.gno index 572b556dd..ac57a061d 100644 --- a/pool/position.gno +++ b/pool/position.gno @@ -77,7 +77,7 @@ func positionUpdate( if position.liquidity.IsZero() { panic(addDetailToError( errZeroLiquidity, - "position.gno__positionUpdate() || both liquidityDelta and current position's liquidity are zero", + "both liquidityDelta and current position's liquidity are zero", )) } diff --git a/pool/position_test.gno b/pool/position_test.gno index b96821af7..03ab0987b 100644 --- a/pool/position_test.gno +++ b/pool/position_test.gno @@ -75,7 +75,7 @@ func TestPositionUpdateWithKey(t *testing.T) { panicMsg string expectedLiquidity string }{ - {i256.MustFromDecimal("0"), u256.Zero(), u256.Zero(), true, `[GNOSWAP-POOL-010] zero liquidity || position.gno__positionUpdate() || both liquidityDelta and current position's liquidity are zero`, ""}, + {i256.MustFromDecimal("0"), u256.Zero(), u256.Zero(), true, `[GNOSWAP-POOL-010] zero liquidity || both liquidityDelta and current position's liquidity are zero`, ""}, {i256.MustFromDecimal("100000"), u256.Zero(), u256.Zero(), false, "", "100000"}, } @@ -109,7 +109,7 @@ func TestPositionUpdate(t *testing.T) { feeGrowthInside0X128: u256.Zero(), feeGrowthInside1X128: u256.Zero(), shouldPanic: true, - panicMsg: `[GNOSWAP-POOL-010] zero liquidity || position.gno__positionUpdate() || both liquidityDelta and current position's liquidity are zero`, + panicMsg: `[GNOSWAP-POOL-010] zero liquidity || both liquidityDelta and current position's liquidity are zero`, }, { initialLiquidity: u256.Zero(), diff --git a/pool/tests/__TEST_pool_spec_#6_test.gnoA b/pool/tests/__TEST_pool_spec_#6_test.gnoA index 1e36d3c2f..adf746066 100644 --- a/pool/tests/__TEST_pool_spec_#6_test.gnoA +++ b/pool/tests/__TEST_pool_spec_#6_test.gnoA @@ -81,7 +81,7 @@ func TestPokeIsNotAllowedOnUninitilaizedPosition(t *testing.T) { std.TestSetRealm(posRealm) uassert.PanicsWithMessage( t, - `[GNOSWAP-POOL-010] zero liquidity || position.gno__positionUpdate() || both liquidityDelta and (self)liquidity are zero`, + `[GNOSWAP-POOL-010] zero liquidity || both liquidityDelta and (self)liquidity are zero`, func() { Burn( barPath, diff --git a/position/_GET_no_receiver.gno b/position/_GET_no_receiver.gno index ffd26f733..b49143d81 100644 --- a/position/_GET_no_receiver.gno +++ b/position/_GET_no_receiver.gno @@ -12,51 +12,62 @@ import ( // type Position func PositionGetPosition(tokenId uint64) Position { - return positions[tokenId] + position, _ := GetPosition(tokenId) + return position } func PositionGetPositionNonce(tokenId uint64) *u256.Uint { - return positions[tokenId].nonce + position := MustGetPosition(tokenId) + return position.nonce } func PositionGetPositionOperator(tokenId uint64) std.Address { - return positions[tokenId].operator + position := MustGetPosition(tokenId) + return position.operator } func PositionGetPositionPoolKey(tokenId uint64) string { - return positions[tokenId].poolKey + position := MustGetPosition(tokenId) + return position.poolKey } func PositionGetPositionTickLower(tokenId uint64) int32 { - return positions[tokenId].tickLower + position := MustGetPosition(tokenId) + return position.tickLower } func PositionGetPositionTickUpper(tokenId uint64) int32 { - return positions[tokenId].tickUpper + position := MustGetPosition(tokenId) + return position.tickUpper } func PositionGetPositionLiquidity(tokenId uint64) *u256.Uint { - return positions[tokenId].liquidity + position := MustGetPosition(tokenId) + return position.liquidity } func PositionGetPositionFeeGrowthInside0LastX128(tokenId uint64) *u256.Uint { - return positions[tokenId].feeGrowthInside0LastX128 + position := MustGetPosition(tokenId) + return position.feeGrowthInside0LastX128 } func PositionGetPositionFeeGrowthInside1LastX128(tokenId uint64) *u256.Uint { - return positions[tokenId].feeGrowthInside1LastX128 + position := MustGetPosition(tokenId) + return position.feeGrowthInside1LastX128 } func PositionGetPositionTokensOwed0(tokenId uint64) *u256.Uint { - return positions[tokenId].tokensOwed0 + position := MustGetPosition(tokenId) + return position.tokensOwed0 } func PositionGetPositionTokensOwed1(tokenId uint64) *u256.Uint { - return positions[tokenId].tokensOwed1 + position := MustGetPosition(tokenId) + return position.tokensOwed1 } func PositionIsInRange(tokenId uint64) bool { - position := positions[tokenId] + position := MustGetPosition(tokenId) poolPath := position.poolKey poolCurrentTick := pl.PoolGetSlot0Tick(poolPath) diff --git a/position/_GET_no_receiver_string.gno b/position/_GET_no_receiver_string.gno index 6048a62ad..52a9c3a33 100644 --- a/position/_GET_no_receiver_string.gno +++ b/position/_GET_no_receiver_string.gno @@ -2,25 +2,25 @@ package position // type Position func PositionGetPositionNonceStr(tokenId uint64) string { - return positions[tokenId].nonce.ToString() + return MustGetPosition(tokenId).nonce.ToString() } func PositionGetPositionLiquidityStr(tokenId uint64) string { - return positions[tokenId].liquidity.ToString() + return MustGetPosition(tokenId).liquidity.ToString() } func PositionGetPositionFeeGrowthInside0LastX128Str(tokenId uint64) string { - return positions[tokenId].feeGrowthInside0LastX128.ToString() + return MustGetPosition(tokenId).feeGrowthInside0LastX128.ToString() } func PositionGetPositionFeeGrowthInside1LastX128Str(tokenId uint64) string { - return positions[tokenId].feeGrowthInside1LastX128.ToString() + return MustGetPosition(tokenId).feeGrowthInside1LastX128.ToString() } func PositionGetPositionTokensOwed0Str(tokenId uint64) string { - return positions[tokenId].tokensOwed0.ToString() + return MustGetPosition(tokenId).tokensOwed0.ToString() } func PositionGetPositionTokensOwed1Str(tokenId uint64) string { - return positions[tokenId].tokensOwed1.ToString() + return MustGetPosition(tokenId).tokensOwed1.ToString() } diff --git a/position/_RPC_api.gno b/position/_RPC_api.gno index 5b1cd3d0d..5afcdd684 100644 --- a/position/_RPC_api.gno +++ b/position/_RPC_api.gno @@ -5,7 +5,6 @@ import ( "time" "gno.land/p/demo/json" - "gno.land/p/demo/ufmt" i256 "gno.land/p/gnoswap/int256" "gno.land/r/gnoswap/v1/common" @@ -52,8 +51,9 @@ type ResponseApiGetPositions struct { func ApiGetPositions() string { rpcPositions := []RpcPosition{} - for lpTokenId, _ := range positions { - rpcPosition := rpcMakePosition(lpTokenId) + + for tokenId := uint64(1); tokenId < nextId; tokenId++ { + rpcPosition := rpcMakePosition(tokenId) rpcPositions = append(rpcPositions, rpcPosition) } @@ -74,7 +74,7 @@ func ApiGetPositions() string { // RESPONSE (ARRAY) NODE responses := json.ArrayNode("", []*json.Node{}) for _, position := range r.Response { - _positionNode := json.ObjectNode("", map[string]*json.Node{ + positionNode := json.ObjectNode("", map[string]*json.Node{ "lpTokenId": json.NumberNode("lpTokenId", float64(position.LpTokenId)), "burned": json.BoolNode("burned", position.Burned), "owner": json.StringNode("owner", gnft.OwnerOf(tokenIdFrom(position.LpTokenId)).String()), @@ -92,7 +92,7 @@ func ApiGetPositions() string { "fee0Unclaimed": json.StringNode("fee0Unclaimed", position.FeeUnclaimed0), "fee1Unclaimed": json.StringNode("fee1Unclaimed", position.FeeUnclaimed1), }) - responses.AppendArray(_positionNode) + responses.AppendArray(positionNode) } node := json.ObjectNode("", map[string]*json.Node{ @@ -111,12 +111,14 @@ func ApiGetPositions() string { func ApiGetPosition(lpTokenId uint64) string { rpcPositions := []RpcPosition{} - _, ok := positions[lpTokenId] - if ok { - rpcPosition := rpcMakePosition(lpTokenId) - rpcPositions = append(rpcPositions, rpcPosition) + position, exist := GetPosition(lpTokenId) + if !exist { + return "" } + rpcPosition := rpcMakePosition(lpTokenId) + rpcPositions = append(rpcPositions, rpcPosition) + r := ResponseApiGetPositions{ Stat: ResponseQueryBase{ Height: std.GetHeight(), @@ -126,7 +128,7 @@ func ApiGetPosition(lpTokenId uint64) string { } // STAT NODE - _stat := json.ObjectNode("", map[string]*json.Node{ + stat := json.ObjectNode("", map[string]*json.Node{ "height": json.NumberNode("height", float64(std.GetHeight())), "timestamp": json.NumberNode("timestamp", float64(time.Now().Unix())), }) @@ -134,7 +136,7 @@ func ApiGetPosition(lpTokenId uint64) string { // RESPONSE (ARRAY) NODE responses := json.ArrayNode("", []*json.Node{}) for _, position := range r.Response { - _positionNode := json.ObjectNode("", map[string]*json.Node{ + positionNode := json.ObjectNode("", map[string]*json.Node{ "lpTokenId": json.NumberNode("lpTokenId", float64(position.LpTokenId)), "burned": json.BoolNode("burned", position.Burned), "owner": json.StringNode("owner", gnft.OwnerOf(tokenIdFrom(position.LpTokenId)).String()), @@ -152,11 +154,11 @@ func ApiGetPosition(lpTokenId uint64) string { "fee0Unclaimed": json.StringNode("fee0Unclaimed", position.FeeUnclaimed0), "fee1Unclaimed": json.StringNode("fee1Unclaimed", position.FeeUnclaimed1), }) - responses.AppendArray(_positionNode) + responses.AppendArray(positionNode) } node := json.ObjectNode("", map[string]*json.Node{ - "stat": _stat, + "stat": stat, "response": responses, }) @@ -170,8 +172,9 @@ func ApiGetPosition(lpTokenId uint64) string { func ApiGetPositionsByPoolPath(poolPath string) string { rpcPositions := []RpcPosition{} - for lpTokenId, position := range positions { + for lpTokenId := uint64(1); lpTokenId < nextId; lpTokenId++ { + position := MustGetPosition(lpTokenId) if position.poolKey != poolPath { continue } @@ -189,7 +192,7 @@ func ApiGetPositionsByPoolPath(poolPath string) string { } // STAT NODE - _stat := json.ObjectNode("", map[string]*json.Node{ + stat := json.ObjectNode("", map[string]*json.Node{ "height": json.NumberNode("height", float64(std.GetHeight())), "timestamp": json.NumberNode("timestamp", float64(time.Now().Unix())), }) @@ -197,7 +200,7 @@ func ApiGetPositionsByPoolPath(poolPath string) string { // RESPONSE (ARRAY) NODE responses := json.ArrayNode("", []*json.Node{}) for _, position := range r.Response { - _positionNode := json.ObjectNode("", map[string]*json.Node{ + positionNode := json.ObjectNode("", map[string]*json.Node{ "lpTokenId": json.NumberNode("lpTokenId", float64(position.LpTokenId)), "burned": json.BoolNode("burned", position.Burned), "owner": json.StringNode("owner", gnft.OwnerOf(tokenIdFrom(position.LpTokenId)).String()), @@ -215,11 +218,11 @@ func ApiGetPositionsByPoolPath(poolPath string) string { "fee0Unclaimed": json.StringNode("fee0Unclaimed", position.FeeUnclaimed0), "fee1Unclaimed": json.StringNode("fee1Unclaimed", position.FeeUnclaimed1), }) - responses.AppendArray(_positionNode) + responses.AppendArray(positionNode) } node := json.ObjectNode("", map[string]*json.Node{ - "stat": _stat, + "stat": stat, "response": responses, }) @@ -233,8 +236,8 @@ func ApiGetPositionsByPoolPath(poolPath string) string { func ApiGetPositionsByAddress(address std.Address) string { rpcPositions := []RpcPosition{} - for lpTokenId, position := range positions { - + for lpTokenId := uint64(1); lpTokenId < nextId; lpTokenId++ { + position := MustGetPosition(lpTokenId) if !(position.operator == address || gnft.OwnerOf(tokenIdFrom(lpTokenId)) == address) { continue } @@ -252,7 +255,7 @@ func ApiGetPositionsByAddress(address std.Address) string { } // STAT NODE - _stat := json.ObjectNode("", map[string]*json.Node{ + stat := json.ObjectNode("", map[string]*json.Node{ "height": json.NumberNode("height", float64(std.GetHeight())), "timestamp": json.NumberNode("timestamp", float64(time.Now().Unix())), }) @@ -260,7 +263,7 @@ func ApiGetPositionsByAddress(address std.Address) string { // RESPONSE (ARRAY) NODE responses := json.ArrayNode("", []*json.Node{}) for _, position := range r.Response { - _positionNode := json.ObjectNode("", map[string]*json.Node{ + positionNode := json.ObjectNode("", map[string]*json.Node{ "lpTokenId": json.NumberNode("lpTokenId", float64(position.LpTokenId)), "burned": json.BoolNode("burned", position.Burned), "owner": json.StringNode("owner", gnft.OwnerOf(tokenIdFrom(position.LpTokenId)).String()), @@ -278,11 +281,11 @@ func ApiGetPositionsByAddress(address std.Address) string { "fee0Unclaimed": json.StringNode("fee0Unclaimed", position.FeeUnclaimed0), "fee1Unclaimed": json.StringNode("fee1Unclaimed", position.FeeUnclaimed1), }) - responses.AppendArray(_positionNode) + responses.AppendArray(positionNode) } node := json.ObjectNode("", map[string]*json.Node{ - "stat": _stat, + "stat": stat, "response": responses, }) @@ -296,7 +299,7 @@ func ApiGetPositionsByAddress(address std.Address) string { func ApiGetPositionsUnclaimedFee() string { rpcUnclaimedFee := []RpcUnclaimedFee{} - for lpTokenId, _ := range positions { + for lpTokenId := uint64(1); lpTokenId < nextId; lpTokenId++ { unclaimedFee0, unclaimedFee1 := unclaimedFee(lpTokenId) rpcUnclaimedFee = append(rpcUnclaimedFee, RpcUnclaimedFee{ LpTokenId: lpTokenId, @@ -306,7 +309,7 @@ func ApiGetPositionsUnclaimedFee() string { } // STAT NODE - _stat := json.ObjectNode("", map[string]*json.Node{ + stat := json.ObjectNode("", map[string]*json.Node{ "height": json.NumberNode("height", float64(std.GetHeight())), "timestamp": json.NumberNode("timestamp", float64(time.Now().Unix())), }) @@ -314,16 +317,16 @@ func ApiGetPositionsUnclaimedFee() string { // RESPONSE (ARRAY) NODE responses := json.ArrayNode("", []*json.Node{}) for _, unclaimedFee := range rpcUnclaimedFee { - _unclaimedFeeNode := json.ObjectNode("", map[string]*json.Node{ + unclaimedFeeNode := json.ObjectNode("", map[string]*json.Node{ "lpTokenId": json.NumberNode("lpTokenId", float64(unclaimedFee.LpTokenId)), "fee0": json.StringNode("fee0", unclaimedFee.Fee0), "fee1": json.StringNode("fee1", unclaimedFee.Fee1), }) - responses.AppendArray(_unclaimedFeeNode) + responses.AppendArray(unclaimedFeeNode) } node := json.ObjectNode("", map[string]*json.Node{ - "stat": _stat, + "stat": stat, "response": responses, }) @@ -346,7 +349,7 @@ func ApiGetPositionUnclaimedFeeByLpTokenId(lpTokenId uint64) string { }) // STAT NODE - _stat := json.ObjectNode("", map[string]*json.Node{ + stat := json.ObjectNode("", map[string]*json.Node{ "height": json.NumberNode("height", float64(std.GetHeight())), "timestamp": json.NumberNode("timestamp", float64(time.Now().Unix())), }) @@ -354,16 +357,16 @@ func ApiGetPositionUnclaimedFeeByLpTokenId(lpTokenId uint64) string { // RESPONSE (ARRAY) NODE responses := json.ArrayNode("", []*json.Node{}) for _, unclaimedFee := range rpcUnclaimedFee { - _unclaimedFeeNode := json.ObjectNode("", map[string]*json.Node{ + unclaimedFeeNode := json.ObjectNode("", map[string]*json.Node{ "lpTokenId": json.NumberNode("lpTokenId", float64(unclaimedFee.LpTokenId)), "fee0": json.StringNode("fee0", unclaimedFee.Fee0), "fee1": json.StringNode("fee1", unclaimedFee.Fee1), }) - responses.AppendArray(_unclaimedFeeNode) + responses.AppendArray(unclaimedFeeNode) } node := json.ObjectNode("", map[string]*json.Node{ - "stat": _stat, + "stat": stat, "response": responses, }) @@ -376,13 +379,7 @@ func ApiGetPositionUnclaimedFeeByLpTokenId(lpTokenId uint64) string { } func rpcMakePosition(lpTokenId uint64) RpcPosition { - position, exist := positions[lpTokenId] - if !exist { - panic(addDetailToError( - errDataNotFound, - ufmt.Sprintf("_RPC_api.gno__rpcMakePosition() || lpTokenId(%d) not found", lpTokenId), - )) - } + position := MustGetPosition(lpTokenId) burned := isBurned(lpTokenId) @@ -427,40 +424,30 @@ func rpcMakePosition(lpTokenId uint64) RpcPosition { func unclaimedFee(tokenId uint64) (*i256.Int, *i256.Int) { // ref: https://blog.uniswap.org/uniswap-v3-math-primer-2#calculating-uncollected-fees - _liquidity := positions[tokenId].liquidity // u256 - liquidity := i256.FromUint256(_liquidity) // i256 + position := MustGetPosition(tokenId) + + liquidityU256 := position.liquidity + liquidity := i256.FromUint256(liquidityU256) - tickLower := positions[tokenId].tickLower - tickUpper := positions[tokenId].tickUpper + tickLower := position.tickLower + tickUpper := position.tickUpper - poolKey := positions[tokenId].poolKey + poolKey := position.poolKey pool := pl.GetPoolFromPoolPath(poolKey) currentTick := pool.Slot0Tick() - _feeGrowthGlobal0X128 := pool.FeeGrowthGlobal0X128() // u256 - feeGrowthGlobal0X128 := i256.FromUint256(_feeGrowthGlobal0X128) // i256 - - _feeGrowthGlobal1X128 := pool.FeeGrowthGlobal1X128() // u256 - feeGrowthGlobal1X128 := i256.FromUint256(_feeGrowthGlobal1X128) // i256 - - _tickUpperFeeGrowthOutside0X128 := pool.GetTickFeeGrowthOutside0X128(tickUpper) // u256 - tickUpperFeeGrowthOutside0X128 := i256.FromUint256(_tickUpperFeeGrowthOutside0X128) // i256 - - _tickUpperFeeGrowthOutside1X128 := pool.GetTickFeeGrowthOutside1X128(tickUpper) // u256 - tickUpperFeeGrowthOutside1X128 := i256.FromUint256(_tickUpperFeeGrowthOutside1X128) // i256 - - _tickLowerFeeGrowthOutside0X128 := pool.GetTickFeeGrowthOutside0X128(tickLower) // u256 - tickLowerFeeGrowthOutside0X128 := i256.FromUint256(_tickLowerFeeGrowthOutside0X128) // i256 + feeGrowthGlobal0X128 := i256.FromUint256(pool.FeeGrowthGlobal0X128()) + feeGrowthGlobal1X128 := i256.FromUint256(pool.FeeGrowthGlobal1X128()) - _tickLowerFeeGrowthOutside1X128 := pool.GetTickFeeGrowthOutside1X128(tickLower) // u256 - tickLowerFeeGrowthOutside1X128 := i256.FromUint256(_tickLowerFeeGrowthOutside1X128) // i256 + tickUpperFeeGrowthOutside0X128 := i256.FromUint256(pool.GetTickFeeGrowthOutside0X128(tickUpper)) + tickUpperFeeGrowthOutside1X128 := i256.FromUint256(pool.GetTickFeeGrowthOutside1X128(tickUpper)) - _feeGrowthInside0LastX128 := positions[tokenId].feeGrowthInside0LastX128 // u256 - feeGrowthInside0LastX128 := i256.FromUint256(_feeGrowthInside0LastX128) // i256 + tickLowerFeeGrowthOutside0X128 := i256.FromUint256(pool.GetTickFeeGrowthOutside0X128(tickLower)) + tickLowerFeeGrowthOutside1X128 := i256.FromUint256(pool.GetTickFeeGrowthOutside1X128(tickLower)) - _feeGrowthInside1LastX128 := positions[tokenId].feeGrowthInside1LastX128 // u256 - feeGrowthInside1LastX128 := i256.FromUint256(_feeGrowthInside1LastX128) // i256 + feeGrowthInside0LastX128 := i256.FromUint256(position.feeGrowthInside0LastX128) + feeGrowthInside1LastX128 := i256.FromUint256(position.feeGrowthInside1LastX128) var tickLowerFeeGrowthBelow0, tickLowerFeeGrowthBelow1, tickUpperFeeGrowthAbove0, tickUpperFeeGrowthAbove1 *i256.Int @@ -510,5 +497,6 @@ func subIn256(x, y *i256.Int) *i256.Int { } func isBurned(tokenId uint64) bool { - return positions[tokenId].burned + position := MustGetPosition(tokenId) + return position.burned } diff --git a/position/_RPC_dry.gno b/position/_RPC_dry.gno index 670ba0384..56f361cbf 100644 --- a/position/_RPC_dry.gno +++ b/position/_RPC_dry.gno @@ -23,16 +23,16 @@ func DryMint( tickCurrent int32, tickLower int32, tickUpper int32, - _amount0Desired string, - _amount1Desired string, + amount0DesiredStr string, + amount1DesiredStr string, ) (string, string) { // FROM: position__liquidity_management.gno sqrtRatioX96 := common.TickMathGetSqrtRatioAtTick(tickCurrent) sqrtLowerX96 := common.TickMathGetSqrtRatioAtTick(tickLower) sqrtUpperX96 := common.TickMathGetSqrtRatioAtTick(tickUpper) - amount0Desired := u256.MustFromDecimal(_amount0Desired) - amount1Desired := u256.MustFromDecimal(_amount1Desired) + amount0Desired := u256.MustFromDecimal(amount0DesiredStr) + amount1Desired := u256.MustFromDecimal(amount1DesiredStr) liquidity := common.GetLiquidityForAmounts( sqrtRatioX96, diff --git a/position/_helper_test.gno b/position/_helper_test.gno index f998f2ab9..41c4c458f 100644 --- a/position/_helper_test.gno +++ b/position/_helper_test.gno @@ -4,22 +4,27 @@ import ( "std" "testing" + "gno.land/p/demo/avl" "gno.land/p/demo/testutils" "gno.land/p/demo/uassert" pusers "gno.land/p/demo/users" "gno.land/r/demo/users" - "gno.land/r/demo/wugnot" "gno.land/r/gnoswap/v1/common" "gno.land/r/gnoswap/v1/consts" "gno.land/r/gnoswap/v1/gnft" - "gno.land/r/gnoswap/v1/gns" + pl "gno.land/r/gnoswap/v1/pool" - sr "gno.land/r/gnoswap/v1/staker" + + "gno.land/r/demo/wugnot" + "gno.land/r/gnoswap/v1/gns" "gno.land/r/onbloc/bar" "gno.land/r/onbloc/baz" "gno.land/r/onbloc/foo" "gno.land/r/onbloc/obl" "gno.land/r/onbloc/qux" + "gno.land/r/onbloc/usdc" + + sr "gno.land/r/gnoswap/v1/staker" ) const ( @@ -33,11 +38,13 @@ const ( oblPath string = "gno.land/r/onbloc/obl" quxPath string = "gno.land/r/onbloc/qux" - fee100 uint32 = 100 - fee500 uint32 = 500 - fee3000 uint32 = 3000 - maxApprove uint64 = 18446744073709551615 - max_timeout int64 = 9999999999 + fee100 uint32 = 100 + fee500 uint32 = 500 + fee3000 uint32 = 3000 + fee10000 uint32 = 10000 + maxApprove uint64 = 18446744073709551615 + max_timeout int64 = 9999999999 + maxSqrtPriceLimitX96 string = "1461446703485210103287273052203988822378723970341" TIER_1 uint64 = 1 TIER_2 uint64 = 2 @@ -155,24 +162,13 @@ func (QuxToken) Approve() func(spender pusers.AddressOrName, amount uint64) { return qux.Approve } -func init() { - std.TestSetRealm(std.NewUserRealm(consts.TOKEN_REGISTER)) - - pl.RegisterGRC20Interface(wugnotPath, WugnotToken{}) - pl.RegisterGRC20Interface(gnsPath, GNSToken{}) - pl.RegisterGRC20Interface(barPath, BarToken{}) - pl.RegisterGRC20Interface(bazPath, BazToken{}) - pl.RegisterGRC20Interface(fooPath, FooToken{}) - pl.RegisterGRC20Interface(oblPath, OBLToken{}) - pl.RegisterGRC20Interface(quxPath, QuxToken{}) -} - var ( admin = pusers.AddressOrName(consts.ADMIN) alice = pusers.AddressOrName(testutils.TestAddress("alice")) bob = pusers.AddressOrName(testutils.TestAddress("bob")) pool = pusers.AddressOrName(consts.POOL_ADDR) protocolFee = pusers.AddressOrName(consts.PROTOCOL_FEE_ADDR) + router = pusers.AddressOrName(consts.ROUTER_ADDR) adminRealm = std.NewUserRealm(users.Resolve(admin)) posRealm = std.NewCodeRealm(consts.POSITION_PATH) @@ -188,7 +184,10 @@ func InitialisePoolTest(t *testing.T) { std.TestSetOrigCaller(users.Resolve(admin)) TokenApprove(t, gnsPath, admin, pool, maxApprove) - CreatePool(t, wugnotPath, gnsPath, fee3000, "79228162514264337593543950336", users.Resolve(admin)) + poolPath := pl.GetPoolPath(wugnotPath, gnsPath, fee3000) + if !pl.DoesPoolPathExist(poolPath) { + pl.CreatePool(wugnotPath, gnsPath, fee3000, "79228162514264337593543950336") + } //2. create position std.TestSetOrigCaller(users.Resolve(alice)) @@ -351,6 +350,160 @@ func MintPosition(t *testing.T, caller) } +func MintPositionAll(t *testing.T, caller std.Address) { + t.Helper() + std.TestSetRealm(std.NewUserRealm(caller)) + TokenApprove(t, gnsPath, pusers.AddressOrName(caller), pool, maxApprove) + TokenApprove(t, gnsPath, pusers.AddressOrName(caller), router, maxApprove) + TokenApprove(t, wugnotPath, pusers.AddressOrName(caller), pool, maxApprove) + TokenApprove(t, wugnotPath, pusers.AddressOrName(caller), router, maxApprove) + + params := []struct { + tickLower int32 + tickUpper int32 + liquidity uint64 + zeroToOne bool + }{ + { + tickLower: -300, + tickUpper: -240, + liquidity: 10, + zeroToOne: true, + }, + { + tickLower: -240, + tickUpper: -180, + liquidity: 10, + zeroToOne: true, + }, + { + tickLower: -180, + tickUpper: -120, + liquidity: 10, + zeroToOne: true, + }, + { + tickLower: -120, + tickUpper: -60, + liquidity: 10, + zeroToOne: true, + }, + { + tickLower: -60, + tickUpper: 0, + liquidity: 10, + zeroToOne: true, + }, + { + tickLower: 0, + tickUpper: 60, + liquidity: 10, + zeroToOne: false, + }, + { + tickLower: 60, + tickUpper: 120, + liquidity: 10, + zeroToOne: false, + }, + { + tickLower: 120, + tickUpper: 180, + liquidity: 10, + zeroToOne: false, + }, + { + tickLower: 180, + tickUpper: 240, + liquidity: 10, + zeroToOne: false, + }, + { + tickLower: 240, + tickUpper: 300, + liquidity: 10, + zeroToOne: false, + }, + { + tickLower: -360, + tickUpper: -300, + liquidity: 10, + zeroToOne: true, + }, + { + tickLower: -420, + tickUpper: -360, + liquidity: 10, + zeroToOne: true, + }, + { + tickLower: -480, + tickUpper: -420, + liquidity: 10, + zeroToOne: true, + }, + { + tickLower: -540, + tickUpper: -480, + liquidity: 10, + zeroToOne: true, + }, + { + tickLower: -600, + tickUpper: -540, + liquidity: 10, + zeroToOne: true, + }, + { + tickLower: 300, + tickUpper: 360, + liquidity: 10, + zeroToOne: false, + }, + { + tickLower: 360, + tickUpper: 420, + liquidity: 10, + zeroToOne: false, + }, + { + tickLower: 420, + tickUpper: 480, + liquidity: 10, + zeroToOne: false, + }, + { + tickLower: 480, + tickUpper: 540, + liquidity: 10, + zeroToOne: false, + }, + { + tickLower: 540, + tickUpper: 600, + liquidity: 10, + zeroToOne: false, + }, + } + + for _, p := range params { + MintPosition(t, + wugnotPath, + gnsPath, + fee3000, + p.tickLower, + p.tickUpper, + "100", + "100", + "0", + "0", + max_timeout, + caller, + caller) + } + +} + func MakeMintPositionWithoutFee(t *testing.T) (uint64, string, string, string) { t.Helper() @@ -544,10 +697,55 @@ func ugnotDeposit(t *testing.T, addr std.Address, amount uint64) { // resetObject resets the object state(clear or make it default values) func resetObject(t *testing.T) { - positions = make(map[uint64]Position) + positions = avl.NewTree() nextId = 1 } +func burnTokens(t *testing.T) { + t.Helper() + + // burn tokens + for _, addr := range addrUsedInTest { + uAddr := a2u(addr) + burnFoo(uAddr) + burnBar(uAddr) + burnBaz(uAddr) + burnQux(uAddr) + burnObl(uAddr) + burnUsdc(uAddr) + } +} + +func burnFoo(addr pusers.AddressOrName) { + std.TestSetRealm(std.NewUserRealm(users.Resolve(admin))) + foo.Burn(addr, foo.BalanceOf(addr)) +} + +func burnBar(addr pusers.AddressOrName) { + std.TestSetRealm(std.NewUserRealm(users.Resolve(admin))) + bar.Burn(addr, bar.BalanceOf(addr)) +} + +func burnBaz(addr pusers.AddressOrName) { + std.TestSetRealm(std.NewUserRealm(users.Resolve(admin))) + baz.Burn(addr, baz.BalanceOf(addr)) +} + +func burnQux(addr pusers.AddressOrName) { + std.TestSetRealm(std.NewUserRealm(users.Resolve(admin))) + qux.Burn(addr, qux.BalanceOf(addr)) +} + +func burnObl(addr pusers.AddressOrName) { + std.TestSetRealm(std.NewUserRealm(users.Resolve(admin))) + obl.Burn(addr, obl.BalanceOf(addr)) +} + +func burnUsdc(addr pusers.AddressOrName) { + std.TestSetRealm(std.NewUserRealm(users.Resolve(admin))) + usdc.Burn(addr, usdc.BalanceOf(addr)) +} + // burnAllNFT burns all NFTs func burnAllNFT(t *testing.T) { t.Helper() @@ -558,7 +756,7 @@ func burnAllNFT(t *testing.T) { } } -func TestBeforeResetObject(t *testing.T) { +func TestBeforeResetPositionObject(t *testing.T) { // make actual data to test resetting not only position's state but also pool's state std.TestSetRealm(adminRealm) @@ -567,7 +765,7 @@ func TestBeforeResetObject(t *testing.T) { uassert.Equal(t, liquidity, "50000", "liquidity should be 50000") uassert.Equal(t, amount0, "50000", "amount0 should be 50000") uassert.Equal(t, amount1, "50000", "amount1 should be 50000") - uassert.Equal(t, len(positions), 1, "positions should have 1 position") + uassert.Equal(t, positions.Size(), 1, "positions should have 1 position") uassert.Equal(t, nextId, uint64(2), "nextId should be 2") uassert.Equal(t, gnft.TotalSupply(), uint64(1), "gnft total supply should be 1") uassert.Equal(t, pl.PoolGetLiquidity("gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500"), "50000", "pool liquidity should be 50000") @@ -576,10 +774,17 @@ func TestBeforeResetObject(t *testing.T) { func TestResetObject(t *testing.T) { resetObject(t) - uassert.Equal(t, len(positions), 0, "positions should be empty") + uassert.Equal(t, positions.Size(), 0, "positions should be empty") uassert.Equal(t, nextId, uint64(1), "nextId should be 1") } +func TestBurnTokens(t *testing.T) { + burnTokens(t) + + uassert.Equal(t, foo.BalanceOf(a2u(addr01)), uint64(0)) // 100_000_000 -> 0 + uassert.Equal(t, bar.BalanceOf(a2u(addr01)), uint64(0)) // 100_000_000 -> 0 +} + func TestBurnAllNFT(t *testing.T) { burnAllNFT(t) uassert.Equal(t, gnft.TotalSupply(), uint64(0), "gnft total supply should be 0") diff --git a/position/errors.gno b/position/errors.gno index 60ef43ceb..e169a7a17 100644 --- a/position/errors.gno +++ b/position/errors.gno @@ -7,31 +7,32 @@ import ( ) var ( - errNoPermission = errors.New("[GNOSWAP-POSITION-001] caller has no permission") - errSlippage = errors.New("[GNOSWAP-POSITION-002] slippage failed") - errWrapUnwrap = errors.New("[GNOSWAP-POSITION-003] wrap, unwrap failed") - errOutOfRange = errors.New("[GNOSWAP-POSITION-004] out of range for numeric value") - errInvalidInput = errors.New("[GNOSWAP-POSITION-005] invalid input data") - errDataNotFound = errors.New("[GNOSWAP-POSITION-006] requested data not found") - errExpired = errors.New("[GNOSWAP-POSITION-007] transaction expired") - errWugnotMinimum = errors.New("[GNOSWAP-POSITION-008] can not wrap less than minimum amount") - errNotClear = errors.New("[GNOSWAP-POSITION-009] position is not clear") - errZeroLiquidity = errors.New("[GNOSWAP-POSITION-010] zero liquidity") - errInvalidAddress = errors.New("[GNOSWAP-POSITION-011] invalid address") + errNoPermission = errors.New("[GNOSWAP-POSITION-001] caller has no permission") + errSlippage = errors.New("[GNOSWAP-POSITION-002] slippage failed") + errWrapUnwrap = errors.New("[GNOSWAP-POSITION-003] wrap, unwrap failed") + errOutOfRange = errors.New("[GNOSWAP-POSITION-004] out of range for numeric value") + errInvalidInput = errors.New("[GNOSWAP-POSITION-005] invalid input data") + errDataNotFound = errors.New("[GNOSWAP-POSITION-006] requested data not found") + errExpired = errors.New("[GNOSWAP-POSITION-007] transaction expired") + errWugnotMinimum = errors.New("[GNOSWAP-POSITION-008] can not wrap less than minimum amount") + errNotClear = errors.New("[GNOSWAP-POSITION-009] position is not clear") + errZeroLiquidity = errors.New("[GNOSWAP-POSITION-010] zero liquidity") + errPositionExist = errors.New("[GNOSWAP-POSITION-011] position with same tokenId already exists") + errInvalidAddress = errors.New("[GNOSWAP-POSITION-012] invalid address") + errPositionDoesNotExist = errors.New("[GNOSWAP-POSITION-013] position does not exist") + errZeroUGNOT = errors.New("[GNOSWAP-POSITION-014] No UGNOTs were sent") + errInsufficientUGNOT = errors.New("[GNOSWAP-POSITION-015] Insufficient UGNOT provided") + errInvalidTokenPath = errors.New("[GNOSWAP-POSITION-016] invalid token address") ) -// TODO: -// addDetailToError -> newErrorWithDetail -func addDetailToError(err error, detail string) string { - finalErr := ufmt.Errorf("%s || %s", err.Error(), detail) - return finalErr.Error() -} - -// newErrorWithDetail returns a new error with the given detail -// e.g. newErrorWithDetail(err, "detail") +// newErrorWithDetail appends additional context or details to an existing error message. +// +// Parameters: +// - err: The original error (error). +// - detail: Additional context or detail to append to the error message (string). // -// input: err error, detail string -// output: "err.Error() || detail" +// Returns: +// - string: The combined error message in the format " || ". func newErrorWithDetail(err error, detail string) string { return ufmt.Errorf("%s || %s", err.Error(), detail).Error() } diff --git a/position/gno_helper.gno b/position/gno_helper.gno deleted file mode 100644 index fcc414377..000000000 --- a/position/gno_helper.gno +++ /dev/null @@ -1,11 +0,0 @@ -package position - -import ( - "std" - - "gno.land/r/gnoswap/v1/consts" -) - -func GetOrigPkgAddr() std.Address { - return consts.POSITION_ADDR -} diff --git a/position/helper.gno b/position/helper.gno deleted file mode 100644 index 532e0fdc9..000000000 --- a/position/helper.gno +++ /dev/null @@ -1,131 +0,0 @@ -package position - -import ( - "std" - "strconv" - - "gno.land/p/demo/grc/grc721" - "gno.land/p/demo/ufmt" - "gno.land/r/gnoswap/v1/common" - "gno.land/r/gnoswap/v1/consts" - "gno.land/r/gnoswap/v1/gnft" -) - -// nextId is the next tokenId to be minted -func getNextId() uint64 { - return nextId -} - -// tokenIdFrom converts tokenId to grc721.TokenID type -// NOTE: input parameter tokenId can be string, int, uint64, or grc721.TokenID -// if tokenId is nil or not supported, it will panic -// if tokenId is not found, it will panic -// input: tokenId interface{} -// output: grc721.TokenID -func tokenIdFrom(tokenId interface{}) grc721.TokenID { - if tokenId == nil { - panic(newErrorWithDetail(errInvalidInput, "tokenId is nil")) - } - - switch tokenId.(type) { - case string: - return grc721.TokenID(tokenId.(string)) - case int: - return grc721.TokenID(strconv.Itoa(tokenId.(int))) - case uint64: - return grc721.TokenID(strconv.Itoa(int(tokenId.(uint64)))) - case grc721.TokenID: - return tokenId.(grc721.TokenID) - default: - panic(newErrorWithDetail(errInvalidInput, "unsupported tokenId type")) - } -} - -// exists checks whether tokenId exists -// If tokenId doesn't exist, return false, otherwise return true -// input: tokenId uint64 -// output: bool -func exists(tokenId uint64) bool { - return gnft.Exists(tokenIdFrom(tokenId)) -} - -// isOwner checks whether the caller is the owner of the tokenId -// If the caller is the owner of the tokenId, return true, otherwise return false -// input: tokenId uint64, addr std.Address -// output: bool -func isOwner(tokenId uint64, addr std.Address) bool { - owner := gnft.OwnerOf(tokenIdFrom(tokenId)) - if owner == addr { - return true - } - return false -} - -// isOperator checks whether the caller is the approved operator of the tokenId -// If the caller is the approved operator of the tokenId, return true, otherwise return false -// input: tokenId uint64, addr std.Address -// output: bool -func isOperator(tokenId uint64, addr std.Address) bool { - operator, ok := gnft.GetApproved(tokenIdFrom(tokenId)) - if ok && operator == addr { - return true - } - return false -} - -// isStaked checks whether tokenId is staked -// If tokenId is staked, owner of tokenId is staker contract -// If tokenId is staked, return true, otherwise return false -// input: tokenId grc721.TokenID -// output: bool -func isStaked(tokenId grc721.TokenID) bool { - exist := gnft.Exists(tokenId) - if exist { - owner := gnft.OwnerOf(tokenId) - if owner == consts.STAKER_ADDR { - return true - } - } - return false -} - -// isOwnerOrOperator checks whether the caller is the owner or approved operator of the tokenId -// If the caller is the owner or approved operator of the tokenId, return true, otherwise return false -// input: addr std.Address, tokenId uint64 -// output: bool -func isOwnerOrOperator(addr std.Address, tokenId uint64) bool { - assertOnlyValidAddress(addr) - if !exists(tokenId) { - return false - } - if isOwner(tokenId, addr) || isOperator(tokenId, addr) { - return true - } - if isStaked(tokenIdFrom(tokenId)) { - position, exist := positions[tokenId] - if exist && addr == position.operator { - return true - } - } - return false -} - -// splitOf divides poolKey into pToken0, pToken1, and pFee -// If poolKey is invalid, it will panic -// -// input: poolKey string -// output: -// - token0Path string -// - token1Path string -// - fee uint32 -func splitOf(poolKey string) (string, string, uint32) { - res, err := common.Split(poolKey, ":", 3) - if err != nil { - panic(newErrorWithDetail(errInvalidInput, ufmt.Sprintf("invalid poolKey(%s)", poolKey))) - } - - pToken0, pToken1, pFeeStr := res[0], res[1], res[2] - - pFee, _ := strconv.Atoi(pFeeStr) - return pToken0, pToken1, uint32(pFee) -} diff --git a/position/helper_test.gno b/position/helper_test.gno deleted file mode 100644 index 35be8cb56..000000000 --- a/position/helper_test.gno +++ /dev/null @@ -1,397 +0,0 @@ -package position - -import ( - "std" - "testing" - - "gno.land/p/demo/grc/grc721" - "gno.land/p/demo/uassert" - pusers "gno.land/p/demo/users" - "gno.land/r/demo/users" -) - -func TestGetNextId(t *testing.T) { - tests := []struct { - name string - newMint bool - expected uint64 - }{ - { - name: "Success - initial nextId", - newMint: false, - expected: 1, - }, - { - name: "Success - after mint", - newMint: true, - expected: 2, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if tc.newMint { - MakeMintPositionWithoutFee(t) - } - got := getNextId() - uassert.Equal(t, tc.expected, got) - }) - } -} - -func TestTokenIdFrom(t *testing.T) { - - tests := []struct { - name string - input interface{} - expected string - shouldPanic bool - }{ - { - name: "Panic - nil", - input: nil, - expected: "[GNOSWAP-POSITION-005] invalid input data || tokenId is nil", - shouldPanic: true, - }, - { - name: "Panic - unsupported type", - input: float64(1), - expected: "[GNOSWAP-POSITION-005] invalid input data || unsupported tokenId type", - shouldPanic: true, - }, - { - name: "Success - string", - input: "1", - expected: "1", - shouldPanic: false, - }, - { - name: "Success - int", - input: int(1), - expected: "1", - shouldPanic: false, - }, - { - name: "Success - uint64", - input: uint64(1), - expected: "1", - shouldPanic: false, - }, - { - name: "Success - grc721.TokenID", - input: grc721.TokenID("1"), - expected: "1", - shouldPanic: false, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - defer func() { - r := recover() - if r == nil { - if tc.shouldPanic { - t.Errorf(">>> %s: expected panic but got none", tc.name) - return - } - } else { - switch r.(type) { - case string: - if r.(string) != tc.expected { - t.Errorf(">>> %s: got panic %v, want %v", tc.name, r, tc.expected) - } - case error: - if r.(error).Error() != tc.expected { - t.Errorf(">>> %s: got panic %v, want %v", tc.name, r.(error).Error(), tc.expected) - } - default: - t.Errorf(">>> %s: got panic %v, want %v", tc.name, r, tc.expected) - } - } - }() - - if !tc.shouldPanic { - got := tokenIdFrom(tc.input) - uassert.Equal(t, tc.expected, string(got)) - } else { - tokenIdFrom(tc.input) - } - }) - } -} - -func TestExists(t *testing.T) { - tests := []struct { - name string - tokenId uint64 - expected bool - }{ - { - name: "Fail - not exists", - tokenId: 2, - expected: false, - }, - { - name: "Success - exists", - tokenId: 1, - expected: true, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - got := exists(tc.tokenId) - uassert.Equal(t, tc.expected, got) - }) - } -} - -func TestIsOwner(t *testing.T) { - tests := []struct { - name string - tokenId uint64 - addr std.Address - expected bool - }{ - { - name: "Fail - is not owner", - tokenId: 1, - addr: users.Resolve(alice), - expected: false, - }, - { - name: "Success - is owner", - tokenId: 1, - addr: users.Resolve(admin), - expected: true, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - MakeMintPositionWithoutFee(t) - got := isOwner(tc.tokenId, tc.addr) - uassert.Equal(t, tc.expected, got) - }) - } -} - -func TestIsOperator(t *testing.T) { - MakeMintPositionWithoutFee(t) - tests := []struct { - name string - tokenId uint64 - addr pusers.AddressOrName - expected bool - }{ - { - name: "Fail - is not operator", - tokenId: 1, - addr: alice, - expected: false, - }, - { - name: "Success - is operator", - tokenId: 1, - addr: bob, - expected: true, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if tc.expected { - LPTokenApprove(t, admin, tc.addr, tc.tokenId) - } - got := isOperator(tc.tokenId, users.Resolve(tc.addr)) - uassert.Equal(t, tc.expected, got) - }) - } -} - -func TestIsStaked(t *testing.T) { - MakeMintPositionWithoutFee(t) - tests := []struct { - name string - owner pusers.AddressOrName - operator pusers.AddressOrName - tokenId uint64 - expected bool - }{ - { - name: "Fail - is not staked", - owner: bob, - operator: alice, - tokenId: 1, - expected: false, - }, - { - name: "Fail - is not exist tokenId", - owner: admin, - operator: bob, - tokenId: 100, - expected: false, - }, - { - name: "Success - is staked", - owner: admin, - operator: admin, - tokenId: 1, - expected: true, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if tc.expected && tc.owner == tc.operator { - LPTokenStake(t, tc.owner, tc.tokenId) - } - got := isStaked(tokenIdFrom(tc.tokenId)) - uassert.Equal(t, tc.expected, got) - if tc.expected && tc.owner == tc.operator { - LPTokenUnStake(t, tc.owner, tc.tokenId, false) - } - }) - } -} - -func TestIsOwnerOrOperator(t *testing.T) { - MakeMintPositionWithoutFee(t) - tests := []struct { - name string - owner pusers.AddressOrName - operator pusers.AddressOrName - tokenId uint64 - expected bool - }{ - { - name: "Fail - is not owner or operator", - owner: admin, - operator: alice, - tokenId: 1, - expected: false, - }, - { - name: "Success - is operator", - owner: admin, - operator: bob, - tokenId: 1, - expected: true, - }, - { - name: "Success - is owner", - owner: admin, - operator: admin, - tokenId: 1, - expected: true, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if tc.expected && tc.owner != tc.operator { - LPTokenApprove(t, tc.owner, tc.operator, tc.tokenId) - } - var got bool - if tc.owner == tc.operator { - got = isOwnerOrOperator(users.Resolve(tc.owner), tc.tokenId) - } else { - got = isOwnerOrOperator(users.Resolve(tc.operator), tc.tokenId) - } - uassert.Equal(t, tc.expected, got) - }) - } -} - -func TestIsOwnerOrOperatorWithStake(t *testing.T) { - MakeMintPositionWithoutFee(t) - tests := []struct { - name string - owner pusers.AddressOrName - operator pusers.AddressOrName - tokenId uint64 - isStake bool - expected bool - }{ - { - name: "Fail - is not token staked", - owner: admin, - operator: alice, - tokenId: 1, - isStake: false, - expected: false, - }, - { - name: "Success - is token staked (position operator)", - owner: admin, - operator: admin, - tokenId: 1, - isStake: true, - expected: true, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if tc.isStake { - LPTokenStake(t, tc.owner, tc.tokenId) - } - got := isOwnerOrOperator(users.Resolve(tc.operator), tc.tokenId) - uassert.Equal(t, tc.expected, got) - }) - } -} - -func TestPoolKeyDivide(t *testing.T) { - tests := []struct { - name string - poolKey string - expectedPath0 string - expectedPath1 string - expectedFee uint32 - expectedError string - shouldPanic bool - }{ - { - name: "Fail - invalid poolKey", - poolKey: "gno.land/r/onbloc", - expectedError: "[GNOSWAP-POSITION-005] invalid input data || invalid poolKey(gno.land/r/onbloc)", - shouldPanic: true, - }, - { - name: "Success - split poolKey", - poolKey: "gno.land/r/gnoswap/v1/gns:gno.land/r/demo/wugnot:500", - expectedPath0: gnsPath, - expectedPath1: wugnotPath, - expectedFee: fee500, - shouldPanic: false, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - defer func() { - r := recover() - if r == nil { - if tc.shouldPanic { - t.Errorf(">>> %s: expected panic but got none", tc.name) - return - } - } else { - switch r.(type) { - case string: - if r.(string) != tc.expectedError { - t.Errorf(">>> %s: got panic %v, want %v", tc.name, r, tc.expectedError) - } - case error: - if r.(error).Error() != tc.expectedError { - t.Errorf(">>> %s: got panic %v, want %v", tc.name, r.(error).Error(), tc.expectedError) - } - default: - t.Errorf(">>> %s: got panic %v, want %v", tc.name, r, tc.expectedError) - } - } - }() - - if !tc.shouldPanic { - gotToken0, gotToken1, gotFee := splitOf(tc.poolKey) - uassert.Equal(t, tc.expectedPath0, gotToken0) - uassert.Equal(t, tc.expectedPath1, gotToken1) - uassert.Equal(t, tc.expectedFee, gotFee) - } else { - splitOf(tc.poolKey) - } - }) - } -} diff --git a/position/liquidity_management.gno b/position/liquidity_management.gno index d700bfb33..f64248ebd 100644 --- a/position/liquidity_management.gno +++ b/position/liquidity_management.gno @@ -47,9 +47,9 @@ func addLiquidity(params AddLiquidityParams) (*u256.Uint, *u256.Uint, *u256.Uint amount1Cond := amount1Uint.Gte(params.amount1Min) if !(amount0Cond && amount1Cond) { - panic(addDetailToError( + panic(newErrorWithDetail( errSlippage, - ufmt.Sprintf("liquidity_management.gno__addLiquidity() || LM_Price Slippage Check(amount0(%s) >= params.amount0Min(%s), amount1(%s) >= params.amount1Min(%s))", amount0Uint.ToString(), params.amount0Min.ToString(), amount1Uint.ToString(), params.amount1Min.ToString()), + ufmt.Sprintf("LM_Price Slippage Check(amount0(%s) >= params.amount0Min(%s), amount1(%s) >= params.amount1Min(%s))", amount0Uint.ToString(), params.amount0Min.ToString(), amount1Uint.ToString(), params.amount1Min.ToString()), )) } diff --git a/position/native_token.gno b/position/native_token.gno new file mode 100644 index 000000000..e072e3efc --- /dev/null +++ b/position/native_token.gno @@ -0,0 +1,94 @@ +package position + +import ( + "std" + + "gno.land/r/demo/wugnot" + + "gno.land/p/demo/ufmt" + "gno.land/r/gnoswap/v1/consts" +) + +// wrap wraps the specified amount of the native token `ugnot` into the wrapped token `wugnot`. +// +// Parameters: +// - ugnotAmount (uint64): The amount of `ugnot` tokens to wrap into `wugnot`. +// - to (std.Address): The recipient's address to receive the wrapped tokens. +// +// Returns: +// - error: An error if the `ugnot` amount is zero, below the minimum wrapping threshold, or any other issue occurs. +// +// Example: +// +// wrap(1000, userAddress) +// - Wraps 1000 UGNOT into WUGNOT and transfers the WUGNOT to `userAddress`. +// +// Errors: +// - Returns an error if `ugnotAmount` is zero or less than the minimum deposit threshold. +func wrap(ugnotAmount uint64, to std.Address) error { + if ugnotAmount == 0 || ugnotAmount < consts.UGNOT_MIN_DEPOSIT_TO_WRAP { + return ufmt.Errorf("amount(%d) < minimum(%d)", ugnotAmount, consts.UGNOT_MIN_DEPOSIT_TO_WRAP) + } + + wugnotAddr := std.DerivePkgAddr(consts.WRAPPED_WUGNOT) + transferUGNOT(consts.POSITION_ADDR, wugnotAddr, ugnotAmount) + + wugnot.Deposit() // POSITION HAS WUGNOT + wugnot.Transfer(a2u(to), ugnotAmount) // SEND WUGNOT: POSITION -> USER + + return nil +} + +// unwrap converts a specified amount of `WUGNOT` tokens into `UGNOT` tokens +// and transfers the resulting `UGNOT` back to the specified recipient address. +// +// Parameters: +// - `wugnotAmount`: The amount of `WUGNOT` tokens to unwrap (uint64). +// - `to`: The recipient's address (std.Address) to receive the unwrapped `UGNOT`. +// +// Example: +// unwrap(100, userAddress) +// - Converts 100 WUGNOT into UGNOT and sends the resulting UGNOT to `userAddress`. +func unwrap(wugnotAmount uint64, to std.Address) error { + if wugnotAmount == 0 { + return ufmt.Errorf("amount(%d) is zero", wugnotAmount) + } + + wugnot.TransferFrom(a2u(to), a2u(consts.POSITION_ADDR), wugnotAmount) // SEND WUGNOT: USER -> POSITION + wugnot.Withdraw(wugnotAmount) // POSITION HAS UGNOT + transferUGNOT(consts.POSITION_ADDR, to, wugnotAmount) // SEND UGNOT: POSITION -> USER + return nil +} + +// transferUGNOT transfers a specified amount of `UGNOT` tokens from one address to another. +// The function ensures that no transaction occurs if the transfer amount is zero. +// It uses the `std.BankerTypeRealmSend` banker type to facilitate the transfer. +// +// Parameters: +// - `from`: The sender's address (std.Address). +// - `to`: The recipient's address (std.Address). +// - `amount`: The amount of UGNOT tokens to transfer (uint64). +// +// Example: +// transferUGNOT(sender, receiver, 100) // Transfers 100 UGNOT from `sender` to `receiver`. +func transferUGNOT(from, to std.Address, amount uint64) { + if amount == 0 { + return + } + + banker := std.GetBanker(std.BankerTypeRealmSend) + banker.SendCoins(from, to, std.Coins{ + {Denom: consts.UGNOT, Amount: int64(amount)}, + }) +} + +// refundUGNOT refunds a specified amount of `UGNOT` tokens to the provided address. +// This function uses `transferUGNOT` to perform the transfer from the contract's position address +// (`POSITION_ADDR`) to the recipient. +// +// Parameters: +// - `to`: The recipient's address (std.Address) who will receive the refund. +// - `amount`: The amount of `UGNOT` tokens to refund (uint64). +func refundUGNOT(to std.Address, amount uint64) { + transferUGNOT(consts.POSITION_ADDR, to, amount) +} diff --git a/position/native_token_test.gno b/position/native_token_test.gno new file mode 100644 index 000000000..2da2db4dc --- /dev/null +++ b/position/native_token_test.gno @@ -0,0 +1,253 @@ +package position + +import ( + "std" + "strconv" + "testing" + + "gno.land/p/demo/uassert" + pusers "gno.land/p/demo/users" + "gno.land/r/demo/users" + "gno.land/r/gnoswap/v1/consts" +) + +func TestTransferUGNOT(t *testing.T) { + tests := []struct { + name string + action func(t *testing.T, from, to std.Address) + verify func(t *testing.T, to std.Address) uint64 + from std.Address + to std.Address + expected string + shouldPanic bool + }{ + { + name: "Success - Zero amount", + action: func(t *testing.T, from, to std.Address) { + transferUGNOT(from, to, 0) + }, + verify: func(t *testing.T, to std.Address) uint64 { + return ugnotBalanceOf(t, to) + }, + from: users.Resolve(alice), + to: users.Resolve(bob), + expected: "0", + shouldPanic: false, + }, + { + name: "Success - Valid transfer", + action: func(t *testing.T, from, to std.Address) { + ugnotFaucet(t, from, 100) + std.TestSetRealm(std.NewUserRealm(from)) + transferUGNOT(from, to, 100) + }, + verify: func(t *testing.T, to std.Address) uint64 { + return ugnotBalanceOf(t, to) + }, + from: consts.POSITION_ADDR, + to: users.Resolve(bob), + expected: "100", + shouldPanic: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + defer func() { + r := recover() + if r == nil { + if tc.shouldPanic { + t.Errorf(">>> %s: expected panic but got none", tc.name) + return + } + } else { + switch r.(type) { + case string: + if r.(string) != tc.expected { + t.Errorf(">>> %s: got panic %v, want %v", tc.name, r, tc.expected) + } + case error: + if r.(error).Error() != tc.expected { + t.Errorf(">>> %s: got panic %v, want %v", tc.name, r.(error).Error(), tc.expected) + } + default: + t.Errorf(">>> %s: got panic %v, want %v", tc.name, r, tc.expected) + } + } + }() + + if !tc.shouldPanic { + tc.action(t, tc.from, tc.to) + if tc.verify != nil { + balance := tc.verify(t, tc.to) + uassert.Equal(t, tc.expected, strconv.FormatUint(balance, 10)) + } + } else { + tc.action(t, tc.from, tc.to) + } + }) + } +} + +func TestWrap(t *testing.T) { + tests := []struct { + name string + action func(t *testing.T, from, to std.Address) error + verify func(t *testing.T, to std.Address) uint64 + from std.Address + to std.Address + expected string + shouldPanic bool + }{ + { + name: "Failure - Amount less than minimum", + action: func(t *testing.T, from, to std.Address) error { + return wrap(999, to) + }, + verify: nil, + from: users.Resolve(alice), + to: users.Resolve(bob), + expected: "amount(999) < minimum(1000)", + shouldPanic: true, + }, + { + name: "Failure - Zero amount", + action: func(t *testing.T, from, to std.Address) error { + return wrap(0, to) + }, + verify: nil, + from: users.Resolve(alice), + to: users.Resolve(bob), + expected: "amount(0) < minimum(1000)", + shouldPanic: true, + }, + { + name: "Success - Valid amount", + action: func(t *testing.T, from, to std.Address) error { + ugnotFaucet(t, from, 1000) + std.TestSetRealm(std.NewUserRealm(from)) + return wrap(1000, to) + }, + verify: func(t *testing.T, to std.Address) uint64 { + return TokenBalance(t, wugnotPath, pusers.AddressOrName(to)) + }, + from: consts.POSITION_ADDR, + to: users.Resolve(bob), + expected: "1000", + shouldPanic: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + defer func() { + r := recover() + if r != nil { + switch r.(type) { + case string: + if r.(string) != tc.expected { + t.Errorf(">>> %s: got panic %v, want %v", tc.name, r, tc.expected) + } + case error: + if r.(error).Error() != tc.expected { + t.Errorf(">>> %s: got panic %v, want %v", tc.name, r.(error).Error(), tc.expected) + } + default: + t.Errorf(">>> %s: got panic %v, want %v", tc.name, r, tc.expected) + } + } + }() + + if !tc.shouldPanic { + err := tc.action(t, tc.from, tc.to) + if err == nil && tc.verify != nil { + balance := tc.verify(t, tc.to) + uassert.Equal(t, tc.expected, strconv.FormatUint(balance, 10)) + } + } else { + err := tc.action(t, tc.from, tc.to) + if err != nil { + uassert.Equal(t, tc.expected, err.Error()) + } else { + t.Errorf(">>> %s: expected panic but got none", tc.name) + } + } + }) + } +} + +func TestUnWrap(t *testing.T) { + tests := []struct { + name string + action func(t *testing.T, from, to std.Address) error + verify func(t *testing.T, to std.Address) uint64 + from std.Address + to std.Address + expected string + shouldPanic bool + }{ + { + name: "Failure - Zero amount", + action: func(t *testing.T, from, to std.Address) error { + return unwrap(0, to) + }, + verify: nil, + from: users.Resolve(alice), + to: users.Resolve(bob), + expected: "amount(0) is zero", + shouldPanic: true, + }, + { + name: "Success - Valid amount", + action: func(t *testing.T, from, to std.Address) error { + ugnotFaucet(t, from, 1000) + std.TestSetRealm(std.NewUserRealm(from)) + wrap(1000, to) + std.TestSetRealm(std.NewUserRealm(to)) + TokenApprove(t, wugnotPath, pusers.AddressOrName(to), pusers.AddressOrName(from), 1000) + return unwrap(1000, to) + }, + verify: func(t *testing.T, to std.Address) uint64 { + return TokenBalance(t, wugnotPath, pusers.AddressOrName(to)) + }, + from: consts.POSITION_ADDR, + to: users.Resolve(bob), + expected: "1000", + shouldPanic: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + defer func() { + r := recover() + if r != nil { + switch r.(type) { + case string: + if r.(string) != tc.expected { + t.Errorf(">>> %s: got panic %v, want %v", tc.name, r, tc.expected) + } + case error: + if r.(error).Error() != tc.expected { + t.Errorf(">>> %s: got panic %v, want %v", tc.name, r.(error).Error(), tc.expected) + } + default: + t.Errorf(">>> %s: got panic %v, want %v", tc.name, r, tc.expected) + } + } + }() + + if !tc.shouldPanic { + err := tc.action(t, tc.from, tc.to) + if err == nil && tc.verify != nil { + balance := tc.verify(t, tc.to) + uassert.Equal(t, tc.expected, strconv.FormatUint(balance, 10)) + } + } else { + err := tc.action(t, tc.from, tc.to) + if err != nil { + uassert.Equal(t, tc.expected, err.Error()) + } else { + t.Errorf(">>> %s: expected panic but got none", tc.name) + } + } + }) + } +} diff --git a/position/position.gno b/position/position.gno index 42503da34..add004cd6 100644 --- a/position/position.gno +++ b/position/position.gno @@ -1,29 +1,120 @@ package position import ( + "encoding/base64" "std" + "strconv" + "gno.land/p/demo/avl" "gno.land/p/demo/ufmt" - "gno.land/r/demo/wugnot" - "gno.land/r/gnoswap/v1/gnft" - u256 "gno.land/p/gnoswap/uint256" + "gno.land/r/demo/wugnot" "gno.land/r/gnoswap/v1/common" "gno.land/r/gnoswap/v1/consts" + "gno.land/r/gnoswap/v1/gnft" en "gno.land/r/gnoswap/v1/emission" pl "gno.land/r/gnoswap/v1/pool" ) +const ( + ZERO_LIQUIDITY_FOR_FEE_COLLECTION = "0" +) + var ( - positions map[uint64]Position = make(map[uint64]Position) // tokenId -> Position - nextId uint64 = 1 // lp token id + positions = avl.NewTree() // tokenId[uint64] -> Position + nextId = uint64(1) ) -// Mint creates a new liquidity position and mints liquidity tokens. -// It also handles the conversion between GNOT and WUGNOT transparently for the user. -// Returns minted tokenId, liquidity, amount0, amount1 +// nextId is the next tokenId to be minted +func getNextId() uint64 { + return nextId +} + +// region: Mint + +type MintInput struct { + token0 string + token1 string + fee uint32 + tickLower int32 + tickUpper int32 + amount0Desired string + amount1Desired string + amount0Min string + amount1Min string + deadline int64 + mintTo std.Address + caller std.Address +} + +type TokenPair struct { + token0 string + token1 string + token0IsNative bool + token1IsNative bool + wrappedAmount uint64 +} + +type ProcessedMintInput struct { + tokenPair TokenPair + amount0Desired *u256.Uint + amount1Desired *u256.Uint + amount0Min *u256.Uint + amount1Min *u256.Uint + tickLower int32 + tickUpper int32 + poolPath string +} + +// Mint creates a new liquidity position by depositing token pairs into the pool and minting a new LP token. +// +// Parameters: +// - token0: The address of token0. +// - token1: The address of token1. +// - fee: The fee tier of the pool, in basis points. +// - tickLower: The lower tick boundary of the position. +// - tickUpper: The upper tick boundary of the position. +// - amount0Desired: Desired amount of token0 to add as liquidity, as a string. +// - amount1Desired: Desired amount of token1 to add as liquidity, as a string. +// - amount0Min: Minimum acceptable amount of token0 to add as liquidity, as a string. +// - amount1Min: Minimum acceptable amount of token1 to add as liquidity, as a string. +// - deadline: Expiration timestamp for the transaction. +// - mintTo: Address to receive the minted LP token. +// - caller: The address of the entity (contract or user) providing liquidity; assets will be withdrawn from this address. +// +// Returns: +// - uint64: The ID of the newly minted liquidity position. +// - string: The amount of liquidity provided to the position. +// - string: The amount of token0 used in the mint. +// - string: The amount of token1 used in the mint. +// +// Behavior: +// 1. **Validation**: +// - Ensures the contract is not halted. +// - Validates that the caller is either a user or a staker contract. +// - If the caller is a user, validates the `mintTo` and `caller` addresses to ensure they match. +// - Checks the transaction's deadline to prevent expired transactions. +// 2. **Pre-Mint Setup**: +// - Calls `MintAndDistributeGns` to handle GNS emissions. +// - Processes the input parameters for minting (`processMintInput`) to standardize and validate the inputs. +// 3. **Mint Execution**: +// - Executes the mint operation using the processed parameters. +// - Withdraws the required token amounts (`token0` and `token1`) from the `caller` address. +// - Mints a new LP token, and the resulting LP token is sent to the `mintTo` address. +// 4. **Post-Mint Cleanup**: +// - If native tokens were used (e.g., `ugnot`), unwraps any leftover wrapped tokens (`wugnot`) and refunds them to the `caller` address. +// 5. **Event Emission**: +// - Emits a "Mint" event containing detailed information about the mint operation. +// +// Panics: +// - If the contract is halted. +// - If the caller is not authorized. +// - If the transaction deadline has passed. +// - If input validation fails. +// - If errors occur during the minting process or leftover token unwrapping. +// // ref: https://docs.gnoswap.io/contracts/position/position.gno#mint func Mint( token0 string, @@ -31,60 +122,27 @@ func Mint( fee uint32, tickLower int32, tickUpper int32, - _amount0Desired string, // *u256.Uint - _amount1Desired string, // *u256.Uint - _amount0Min string, // *u256.Uint - _amount1Min string, // *u256.Uint + amount0Desired string, + amount1Desired string, + amount0Min string, + amount1Min string, deadline int64, mintTo std.Address, caller std.Address, ) (uint64, string, string, string) { - common.IsHalted() - en.MintAndDistributeGns() - - prev := std.PrevRealm() - isUserCalled := prev.PkgPath() == "" - isStakerCalled := prev.Addr() == consts.STAKER_ADDR - - if common.GetLimitCaller() { - // only user or staker can call - if !(isUserCalled || isStakerCalled) { - panic(addDetailToError( - errNoPermission, - ufmt.Sprintf("position.gno__Mint() || only user or staker can call isUserCalled(%t) || isStakerCalled(%t), but called from %s", isUserCalled, isStakerCalled, prev.Addr().String()), - )) - } - } - - // if user called, set caller & mintTo to user address - if isUserCalled { - caller = prev.Addr() - mintTo = prev.Addr() - } - - token0, token1, token0IsNative, token1IsNative := processTokens(token0, token1) - userWugnotBalance := wugnot.BalanceOf(a2u(caller)) - - if token1 < token0 { - token0, token1 = token1, token0 - _amount0Desired, _amount1Desired = _amount1Desired, _amount0Desired - _amount0Min, _amount1Min = _amount1Min, _amount0Min - tickLower, tickUpper = -tickUpper, -tickLower - token0IsNative, token1IsNative = token1IsNative, token0IsNative + assertOnlyNotHalted() + prevCaller := getPrevRealm() + assertOnlyUserOrStaker(prevCaller) + if isUserCall() { + // if user called, validate the prev address and input addresses(mintTo, caller) + assertOnlyValidAddressWith(prevCaller.Addr(), mintTo) + assertOnlyValidAddressWith(prevCaller.Addr(), caller) } + checkDeadline(deadline) - amount0Desired := u256.MustFromDecimal(_amount0Desired) - amount1Desired := u256.MustFromDecimal(_amount1Desired) - amount0Min := u256.MustFromDecimal(_amount0Min) - amount1Min := u256.MustFromDecimal(_amount1Min) - - // one of token amount can be 0 if position is out of range - // check this condition by using DryMint() - poolPath := ufmt.Sprintf("%s:%s:%d", token0, token1, fee) - - handleNativeToken(token0IsNative, token1IsNative, caller) + en.MintAndDistributeGns() - mintParams := MintParams{ + mintInput := MintInput{ token0: token0, token1: token1, fee: fee, @@ -99,11 +157,31 @@ func Mint( caller: caller, } + processedInput, err := processMintInput(mintInput) + if err != nil { + panic(newErrorWithDetail(errInvalidInput, err.Error())) + } + + // perform mint operation + mintParams := newMintParams(processedInput, mintInput) tokenId, liquidity, amount0, amount1 := mint(mintParams) - handleLeftoverNativeToken(token0IsNative, token1IsNative, userWugnotBalance, caller) + if processedInput.tokenPair.token0IsNative && processedInput.tokenPair.wrappedAmount > amount0.Uint64() { + // unwrap leftover wugnot + err = unwrap(processedInput.tokenPair.wrappedAmount-amount0.Uint64(), caller) + if err != nil { + panic(newErrorWithDetail(errWrapUnwrap, err.Error())) + } + } + if processedInput.tokenPair.token1IsNative && processedInput.tokenPair.wrappedAmount > amount1.Uint64() { + // unwrap leftover wugnot + err = unwrap(processedInput.tokenPair.wrappedAmount-amount1.Uint64(), caller) + if err != nil { + panic(newErrorWithDetail(errWrapUnwrap, err.Error())) + } + } - poolSqrtPriceX96 := pl.PoolGetSlot0SqrtPriceX96(poolPath) + poolSqrtPriceX96 := pl.PoolGetSlot0SqrtPriceX96(processedInput.poolPath) prevAddr, prevPkgPath := getPrevAsString() @@ -111,9 +189,9 @@ func Mint( "Mint", "prevAddr", prevAddr, "prevRealm", prevPkgPath, - "tickLower", ufmt.Sprintf("%d", tickLower), - "tickUpper", ufmt.Sprintf("%d", tickUpper), - "poolPath", poolPath, + "tickLower", ufmt.Sprintf("%d", processedInput.tickLower), + "tickUpper", ufmt.Sprintf("%d", processedInput.tickUpper), + "poolPath", processedInput.poolPath, "mintTo", mintTo.String(), "caller", caller.String(), "internal_lpTokenId", ufmt.Sprintf("%d", tokenId), @@ -126,50 +204,270 @@ func Mint( return tokenId, liquidity.ToString(), amount0.ToString(), amount1.ToString() } -func processTokens(token0, token1 string) (string, string, bool, bool) { +func processMintInput(input MintInput) (ProcessedMintInput, error) { + var result ProcessedMintInput + + // process tokens + token0, token1, token0IsNative, token1IsNative, wrappedAmount := processTokens(input.token0, input.token1, input.amount0Desired, input.amount1Desired, input.caller) + pair := TokenPair{ + token0: token0, + token1: token1, + token0IsNative: token0IsNative, + token1IsNative: token1IsNative, + wrappedAmount: wrappedAmount, + } + + // parse amounts + amount0Desired, amount1Desired, amount0Min, amount1Min := parseAmounts(input.amount0Desired, input.amount1Desired, input.amount0Min, input.amount1Min) + + tickLower, tickUpper := input.tickLower, input.tickUpper + + // swap if token1 < token0 + if token1 < token0 { + pair.token0, pair.token1 = pair.token1, pair.token0 + amount0Desired, amount1Desired = amount1Desired, amount0Desired + amount0Min, amount1Min = amount1Min, amount0Min + tickLower, tickUpper = -tickUpper, -tickLower + pair.token0IsNative, pair.token1IsNative = pair.token1IsNative, pair.token0IsNative + } + + poolPath := renderPoolPath(pair.token0, pair.token1, input.fee) + + result = ProcessedMintInput{ + tokenPair: pair, + amount0Desired: amount0Desired.Clone(), + amount1Desired: amount1Desired.Clone(), + amount0Min: amount0Min.Clone(), + amount1Min: amount1Min.Clone(), + tickLower: tickLower, + tickUpper: tickUpper, + poolPath: poolPath, + } + + return result, nil +} + +// processTokens processes two token paths, validates them, and handles the wrapping of native tokens into wrapped tokens if applicable. +// +// Parameters: +// - token0: The first token path to process. +// - token1: The second token path to process. +// - caller: The address of the user initiating the token processing. +// +// Returns: +// - string: Processed token0 path (potentially modified if it was a native token). +// - string: Processed token1 path (potentially modified if it was a native token). +// - bool: Indicates whether token0 was a native token (`true` if native, `false` otherwise). +// - bool: Indicates whether token1 was a native token (`true` if native, `false` otherwise). +// - uint64: The amount of the native token that was wrapped into the wrapped token. +// +// Behavior: +// 1. Validates the token paths using `validateTokenPath`. +// - Panics with a detailed error if validation fails. +// 2. Checks if `token0` or `token1` is a native token using `isNative`. +// - If a token is native, it is replaced with the wrapped token path (`WRAPPED_WUGNOT`). +// - The native token is then wrapped into the wrapped token using `safeWrapNativeToken`. +// 3. Returns the processed token paths, flags indicating if the tokens were native, and the wrapped amount. +// +// Panics: +// - If `validateTokenPath` fails validation. +// - If wrapping the native token using `safeWrapNativeToken` encounters an issue. +func processTokens( + token0 string, + token1 string, + amount0Desired string, + amount1Desired string, + caller std.Address, +) (string, string, bool, bool, uint64) { + err := validateTokenPath(token0, token1) + if err != nil { + panic(newErrorWithDetail(err, ufmt.Sprintf("token0(%s), token1(%s)", token0, token1))) + } + token0IsNative := false token1IsNative := false - if token0 == consts.GNOT { + wrappedAmount := uint64(0) + + if isNative(token0) { token0 = consts.WRAPPED_WUGNOT token0IsNative = true - } else if token1 == consts.GNOT { + + wrappedAmount, _ = safeWrapNativeToken(amount0Desired, caller) + } else if isNative(token1) { token1 = consts.WRAPPED_WUGNOT token1IsNative = true + + wrappedAmount, _ = safeWrapNativeToken(amount1Desired, caller) } - return token0, token1, token0IsNative, token1IsNative + + return token0, token1, token0IsNative, token1IsNative, wrappedAmount } -func handleNativeToken(token0IsNative, token1IsNative bool, caller std.Address) { - if token0IsNative || token1IsNative { - oldUserWugnotBalance := wugnot.BalanceOf(a2u(caller)) - sent := std.GetOrigSend() - ugnotSent := uint64(sent.AmountOf("ugnot")) - if ugnotSent > 0 { - wrap(ugnotSent, caller) - newUserWugnotBalance := wugnot.BalanceOf(a2u(caller)) - if (newUserWugnotBalance - oldUserWugnotBalance) != ugnotSent { - panic(addDetailToError( - errWrapUnwrap, - ufmt.Sprintf("position.gno__Mint() || ugnot sent(%d) != wugnot received(%d)", ugnotSent, newUserWugnotBalance-oldUserWugnotBalance), - )) - } - } +// validateTokenPath validates the relationship and format of token paths. +// Ensures that token paths are not identical, not conflicting (e.g., GNOT and WUGNOT), +// and each token path is in a valid format. +// +// Parameters: +// - token0: The first token path to validate. +// - token1: The second token path to validate. +// +// Returns: +// - error: Returns `errInvalidTokenPath` or nil +// +// Example: +// +// validateTokenPath("tokenA", "tokenB") -> nil +// validateTokenPath("tokenA", "tokenA") -> errInvalidTokenPath +// validateTokenPath(GNOT, WUGNOT) -> errInvalidTokenPath +func validateTokenPath(token0, token1 string) error { + if token0 == token1 { + return errInvalidTokenPath + } + if (token0 == consts.GNOT && token1 == consts.WRAPPED_WUGNOT) || + (token0 == consts.WRAPPED_WUGNOT && token1 == consts.GNOT) { + return errInvalidTokenPath + } + if !isValidTokenPath(token0) || !isValidTokenPath(token1) { + return errInvalidTokenPath } + return nil +} + +// isValidTokenPath checks whether the given token path is valid and registered. +// +// Parameters: +// - tokenPath: The token path to validate. +// +// Returns: +// - bool: Returns `true` if the token path is valid and registered; otherwise, `false`. +func isValidTokenPath(tokenPath string) bool { + err := common.IsRegistered(tokenPath) + if err != nil { + return false + } + return true +} + +// isNative checks whether the given token is a native token. +func isNative(token string) bool { + return token == consts.GNOT +} + +// safeWrapNativeToken safely wraps the native token `ugnot` into the wrapped token `wugnot` for a user. +// +// Parameters: +// - amountDesired: The desired amount of `ugnot` to be wrapped, provided as a string. +// - userAddress: The address of the user initiating the wrapping process. +// +// Returns: +// - uint64: The amount of `ugnot` that was successfully wrapped into `wugnot`. +// - error: Returns an error if the wrapping process encounters an issue. +// +// Panics: +// - If the sent `ugnot` amount is zero. +// - If the `amountDesired` is invalid. +// - If the sent `ugnot` amount is insufficient to meet the `amountDesired`. +// - If the `wrap` function fails. +// - If there is a mismatch between the sent `ugnot` and the resulting `wugnot` balance. +// +// Example: +// safeWrapNativeToken("100", userAddress) +// - Wraps 100 UGNOT into WUGNOT for the `userAddress`. +// - If the user sends more than 100 UGNOT, the excess is refunded. +func safeWrapNativeToken(amountDesired string, userAddress std.Address) (uint64, error) { + beforeWugnotBalance := wugnot.BalanceOf(a2u(userAddress)) + sentNative := std.GetOrigSend() + sentUgnotAmount := uint64(sentNative.AmountOf(consts.UGNOT)) + + if sentUgnotAmount <= 0 { + panic(newErrorWithDetail(errZeroUGNOT, "amount of ugnot is zero")) + } + + amount, err := strconv.ParseUint(amountDesired, 10, 64) + if err != nil { + panic(newErrorWithDetail(errWrapUnwrap, err.Error())) + } + + if sentUgnotAmount < amount { + panic(newErrorWithDetail(errInsufficientUGNOT, "amount of ugnot is less than desired amount")) + } + + if sentUgnotAmount > amount { + exceed := sentUgnotAmount - amount + refundUGNOT(userAddress, exceed) + transferUGNOT(consts.POSITION_ADDR, userAddress, amount) + sentUgnotAmount = amount + } + + if err = wrap(sentUgnotAmount, userAddress); err != nil { + panic(newErrorWithDetail(errWugnotMinimum, err.Error())) + } + + afterWugnotBalance := wugnot.BalanceOf(a2u(userAddress)) + diff := afterWugnotBalance - beforeWugnotBalance + + if diff != sentUgnotAmount { + panic(newErrorWithDetail( + errWrapUnwrap, + ufmt.Sprintf("amount of ugnot (%d) is not equal to amount of wugnot. (diff: %d)", sentUgnotAmount, diff), + )) + } + return sentUgnotAmount, nil +} + +func parseAmounts(amount0Desired, amount1Desired, amount0Min, amount1Min string) (*u256.Uint, *u256.Uint, *u256.Uint, *u256.Uint) { + return u256.MustFromDecimal(amount0Desired), u256.MustFromDecimal(amount1Desired), u256.MustFromDecimal(amount0Min), u256.MustFromDecimal(amount1Min) +} + +func renderPoolPath(token0, token1 string, fee uint32) string { + return ufmt.Sprintf("%s:%s:%d", token0, token1, fee) +} + +func hasNativeToken(token0IsNative, token1IsNative bool) bool { + return token0IsNative || token1IsNative +} + +func handleNativeToken(token0IsNative, token1IsNative bool, caller std.Address) error { + if !hasNativeToken(token0IsNative, token1IsNative) { + return nil + } + + oldUserWugnotBalance := wugnot.BalanceOf(a2u(caller)) + sent := std.GetOrigSend() + ugnotSent := uint64(sent.AmountOf("ugnot")) + + if ugnotSent <= 0 { + return nil + } + + if err := wrap(ugnotSent, caller); err != nil { + return err + } + + newUserWugnotBalance := wugnot.BalanceOf(a2u(caller)) + diff := newUserWugnotBalance - oldUserWugnotBalance + + if diff != ugnotSent { + return ufmt.Errorf("amount of ugnot (%d) is not equal to amount of wugnot. (diff: %d)", ugnotSent, diff) + } + + return nil } func handleLeftoverNativeToken(token0IsNative, token1IsNative bool, userWugnotBalance uint64, caller std.Address) { - if token0IsNative || token1IsNative { - userWugnotAfterMint := wugnot.BalanceOf(a2u(caller)) - leftOver := userWugnotAfterMint - userWugnotBalance - if leftOver > 0 { - unwrap(leftOver, caller) - } + if !hasNativeToken(token0IsNative, token1IsNative) { + return + } + + userWugnotAfterMint := wugnot.BalanceOf(a2u(caller)) + + leftOver := userWugnotAfterMint - userWugnotBalance + if leftOver > 0 { + unwrap(leftOver, caller) } } func mint(params MintParams) (uint64, *u256.Uint, *u256.Uint, *u256.Uint) { - checkDeadline(params.deadline) - pool := pl.GetPool(params.token0, params.token1, params.fee) liquidity, amount0, amount1 := addLiquidity( AddLiquidityParams{ @@ -207,29 +505,39 @@ func mint(params MintParams) (uint64, *u256.Uint, *u256.Uint, *u256.Uint) { tokensOwed1: u256.Zero(), burned: false, } - positions[tokenId] = position + updated := setPosition(tokenId, position) + if updated { + panic(newErrorWithDetail( + errPositionExist, + ufmt.Sprintf("tokenId(%d) already exists", tokenId), + )) + } return tokenId, liquidity, amount0, amount1 } +//////////////////////////// + +// region: IncreaseLiquidity + // IncreaseLiquidity increases liquidity of the existing position // Returns tokenId, liquidity, amount0, amount1, poolPath // ref: https://docs.gnoswap.io/contracts/position/position.gno#increaseliquidity func IncreaseLiquidity( tokenId uint64, - _amount0Desired string, // uint256 - _amount1Desired string, // uint256 - _amount0Min string, // uint256 - _amount1Min string, // uint256 + amount0DesiredStr string, + amount1DesiredStr string, + amount0MinStr string, + amount1MinStr string, deadline int64, ) (uint64, string, string, string, string) { common.IsHalted() en.MintAndDistributeGns() - amount0Desired := u256.MustFromDecimal(_amount0Desired) - amount1Desired := u256.MustFromDecimal(_amount1Desired) - amount0Min := u256.MustFromDecimal(_amount0Min) - amount1Min := u256.MustFromDecimal(_amount1Min) + amount0Desired := u256.MustFromDecimal(amount0DesiredStr) + amount1Desired := u256.MustFromDecimal(amount1DesiredStr) + amount0Min := u256.MustFromDecimal(amount0MinStr) + amount1Min := u256.MustFromDecimal(amount1MinStr) increaseLiquidityParams := IncreaseLiquidityParams{ tokenId: tokenId, amount0Desired: amount0Desired, @@ -240,32 +548,33 @@ func IncreaseLiquidity( } // wrap if target pool has wugnot - position := positions[tokenId] + position := MustGetPosition(tokenId) pToken0, pToken1, _ := splitOf(position.poolKey) isToken0Wugnot := pToken0 == consts.WRAPPED_WUGNOT isToken1Wugnot := pToken1 == consts.WRAPPED_WUGNOT - userOldWugnotBalance := wugnot.BalanceOf(a2u(std.PrevRealm().Addr())) // before wrap, user's origin wugnot balance - if isToken0Wugnot || isToken1Wugnot { + // before wrap, user's origin wugnot balance + userOldWugnotBalance := wugnot.BalanceOf(a2u(std.PrevRealm().Addr())) + + if hasWugnotToken(isToken0Wugnot, isToken1Wugnot) { sent := std.GetOrigSend() ugnotSent := uint64(sent.AmountOf("ugnot")) - wrap(ugnotSent, std.PrevRealm().Addr()) + + assertWrapNativeToken(ugnotSent, std.PrevRealm().Addr()) } // increase liquidity _, liquidity, amount0, amount1, poolPath := increaseLiquidity(increaseLiquidityParams) // unwrap left - if isToken0Wugnot || isToken1Wugnot { + if hasWugnotToken(isToken0Wugnot, isToken1Wugnot) { userNewWugnotBalance := wugnot.BalanceOf(a2u(std.PrevRealm().Addr())) leftOver := userNewWugnotBalance - userOldWugnotBalance unwrap(leftOver, std.PrevRealm().Addr()) } - poolSqrtPriceX96 := pl.PoolGetSlot0SqrtPriceX96(poolPath) - prevAddr, prevPkgPath := getPrevAsString() std.Emit( @@ -277,35 +586,76 @@ func IncreaseLiquidity( "internal_liquidity", liquidity.ToString(), "internal_amount0", amount0.ToString(), "internal_amount1", amount1.ToString(), - "internal_sqrtPriceX96", poolSqrtPriceX96, + "internal_sqrtPriceX96", pl.PoolGetSlot0SqrtPriceX96(poolPath), ) return tokenId, liquidity.ToString(), amount0.ToString(), amount1.ToString(), poolPath } -func increaseLiquidity(params IncreaseLiquidityParams) (uint64, *u256.Uint, *u256.Uint, *u256.Uint, string) { - // verify tokenId exists - if !exists(params.tokenId) { - panic(addDetailToError( - errDataNotFound, - ufmt.Sprintf("position.gno__increaseLiquidity() || tokenId(%d) doesn't exist", params.tokenId), - )) - } +func hasWugnotToken(isToken0Wugnot, isToken1Wugnot bool) bool { + return isToken0Wugnot || isToken1Wugnot +} - // MUST BE OWNER TO INCREASE LIQUIDITY - // can not be approved address ≈ staked position can't be modified - owner := gnft.OwnerOf(tokenIdFrom(params.tokenId)) - caller := std.PrevRealm().Addr() - if owner != caller { - panic(addDetailToError( - errNoPermission, - ufmt.Sprintf("position.gno__increaseLiquidity() || only owner(%s) can increase liquidity for tokenId(%d), but called from %s", owner, params.tokenId, caller), - )) +// FeeGrowthInside represents fee growth inside ticks +type FeeGrowthInside struct { + feeGrowthInside0LastX128 *u256.Uint + feeGrowthInside1LastX128 *u256.Uint +} + +// PositionFeeUpdate represents fee update calculation result +type PositionFeeUpdate struct { + tokensOwed0 *u256.Uint + tokensOwed1 *u256.Uint + feeGrowthInside0LastX128 *u256.Uint + feeGrowthInside1LastX128 *u256.Uint +} + +func calculatePositionFeeUpdate( + position Position, + currentFeeGrowth FeeGrowthInside, +) PositionFeeUpdate { + tokensOwed0 := calculateTokensOwed( + currentFeeGrowth.feeGrowthInside0LastX128, + position.feeGrowthInside0LastX128, + position.liquidity, + ) + + tokensOwed1 := calculateTokensOwed( + currentFeeGrowth.feeGrowthInside1LastX128, + position.feeGrowthInside1LastX128, + position.liquidity, + ) + + return PositionFeeUpdate{ + tokensOwed0: new(u256.Uint).Add(position.tokensOwed0, tokensOwed0), + tokensOwed1: new(u256.Uint).Add(position.tokensOwed1, tokensOwed1), + feeGrowthInside0LastX128: currentFeeGrowth.feeGrowthInside0LastX128.Clone(), + feeGrowthInside1LastX128: currentFeeGrowth.feeGrowthInside1LastX128.Clone(), } +} + +// updatePosition updates the position with new liquidity and fee data +func updatePosition( + position Position, + feeUpdate PositionFeeUpdate, + newLiquidity *u256.Uint, +) Position { + position.tokensOwed0 = feeUpdate.tokensOwed0 + position.tokensOwed1 = feeUpdate.tokensOwed1 + position.feeGrowthInside0LastX128 = feeUpdate.feeGrowthInside0LastX128 + position.feeGrowthInside1LastX128 = feeUpdate.feeGrowthInside1LastX128 + position.liquidity = new(u256.Uint).Add(position.liquidity, newLiquidity) + position.burned = false + + return position +} + +func increaseLiquidity(params IncreaseLiquidityParams) (uint64, *u256.Uint, *u256.Uint, *u256.Uint, string) { + assertTokenOwnership(params.tokenId) checkDeadline(params.deadline) - position := positions[params.tokenId] + position := MustGetPosition(params.tokenId) liquidity, amount0, amount1 := addLiquidity( AddLiquidityParams{ poolKey: position.poolKey, @@ -321,35 +671,54 @@ func increaseLiquidity(params IncreaseLiquidityParams) (uint64, *u256.Uint, *u25 pool := pl.GetPoolFromPoolPath(position.poolKey) positionKey := positionKeyCompute(GetOrigPkgAddr(), position.tickLower, position.tickUpper) - _feeGrowthInside0LastX128 := pool.PositionFeeGrowthInside0LastX128(positionKey) - _feeGrowthInside1LastX128 := pool.PositionFeeGrowthInside1LastX128(positionKey) - feeGrowthInside0LastX128 := u256.MustFromDecimal(_feeGrowthInside0LastX128.ToString()) - feeGrowthInside1LastX128 := u256.MustFromDecimal(_feeGrowthInside1LastX128.ToString()) - { - diff := new(u256.Uint).Sub(feeGrowthInside0LastX128, position.feeGrowthInside0LastX128) - mulDiv := u256.MulDiv(diff, position.liquidity, u256.MustFromDecimal(consts.Q128)) - - position.tokensOwed0 = new(u256.Uint).Add(position.tokensOwed0, mulDiv) + // get current fee growth + currentFeeGrowth := FeeGrowthInside{ + feeGrowthInside0LastX128: u256.MustFromDecimal( + pool.PositionFeeGrowthInside0LastX128(positionKey).ToString(), + ), + feeGrowthInside1LastX128: u256.MustFromDecimal( + pool.PositionFeeGrowthInside1LastX128(positionKey).ToString(), + ), } - { - diff := new(u256.Uint).Sub(feeGrowthInside1LastX128, position.feeGrowthInside1LastX128) - mulDiv := u256.MulDiv(diff, position.liquidity, u256.MustFromDecimal(consts.Q128)) + feeUpdate := calculatePositionFeeUpdate(position, currentFeeGrowth) - position.tokensOwed1 = new(u256.Uint).Add(position.tokensOwed1, mulDiv) + updatedPosition := updatePosition(position, feeUpdate, liquidity) + updated := setPosition(params.tokenId, updatedPosition) + if !updated { + panic(newErrorWithDetail( + errPositionDoesNotExist, + ufmt.Sprintf("can not increase liquidity for non-existent position(%d)", params.tokenId), + )) } - position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128 - position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128 - position.liquidity = new(u256.Uint).Add(position.liquidity, liquidity) - position.burned = false + return params.tokenId, liquidity, amount0, amount1, position.poolKey +} - positions[params.tokenId] = position +func calculateTokensOwed( + currentFeeGrowth *u256.Uint, + lastFeeGrowth *u256.Uint, + liquidity *u256.Uint, +) *u256.Uint { + diff := new(u256.Uint).Sub(currentFeeGrowth, lastFeeGrowth) + return u256.MulDiv(diff, liquidity, u256.MustFromDecimal(consts.Q128)) +} - return params.tokenId, liquidity, amount0, amount1, position.poolKey +func updateTokensOwed( + feeGrowthInsideLastX128 *u256.Uint, + positionFeeGrowthInsideLastX128 *u256.Uint, + positionLiquidity *u256.Uint, + burnedAmount *u256.Uint, + tokensOwed *u256.Uint, +) *u256.Uint { + additionalTokensOwed := calculateTokensOwed(feeGrowthInsideLastX128, positionFeeGrowthInsideLastX128, positionLiquidity) + add := new(u256.Uint).Add(burnedAmount, additionalTokensOwed) + return new(u256.Uint).Add(tokensOwed, add) } +// region: DecreaseLiquidity + // DecreaseLiquidity decreases liquidity of the existing position // It also handles the conversion between GNOT and WUGNOT transparently for the user. // Returns tokenId, liquidity, fee0, fee1, amount0, amount1, poolPath @@ -357,8 +726,8 @@ func increaseLiquidity(params IncreaseLiquidityParams) (uint64, *u256.Uint, *u25 func DecreaseLiquidity( tokenId uint64, liquidityRatio uint64, - _amount0Min string, // uint256 - _amount1Min string, // uint256 + amount0MinStr string, + amount1MinStr string, deadline int64, unwrapResult bool, ) (uint64, string, string, string, string, string, string) { @@ -367,14 +736,14 @@ func DecreaseLiquidity( isNormalRange := liquidityRatio >= 1 && liquidityRatio <= 100 if !isNormalRange { - panic(addDetailToError( + panic(newErrorWithDetail( errOutOfRange, - ufmt.Sprintf("position.gno__decreaseLiquidity() || liquidityRatio(%d) should be in range 1 ~ 100", liquidityRatio), + ufmt.Sprintf("liquidityRatio(%d) should be in range 1 ~ 100", liquidityRatio), )) } - amount0Min := u256.MustFromDecimal(_amount0Min) - amount1Min := u256.MustFromDecimal(_amount1Min) + amount0Min := u256.MustFromDecimal(amount0MinStr) + amount1Min := u256.MustFromDecimal(amount1MinStr) decreaseLiquidityParams := DecreaseLiquidityParams{ tokenId: tokenId, liquidityRatio: liquidityRatio, @@ -424,13 +793,13 @@ func decreaseLiquidity(params DecreaseLiquidityParams) (uint64, *u256.Uint, *u25 fee0 := u256.MustFromDecimal(fee0Str) fee1 := u256.MustFromDecimal(fee1Str) - position := positions[params.tokenId] + position := MustGetPosition(params.tokenId) positionLiquidity := position.liquidity if positionLiquidity.IsZero() { - panic(addDetailToError( + panic(newErrorWithDetail( errZeroLiquidity, - ufmt.Sprintf("position.gno__decreaseLiquidity() || position(tokenId:%d) has 0 liquidity", params.tokenId), + ufmt.Sprintf("position(tokenId:%d) has 0 liquidity", params.tokenId), )) } @@ -472,7 +841,13 @@ func decreaseLiquidity(params DecreaseLiquidityParams) (uint64, *u256.Uint, *u25 position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128 position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128 position.liquidity = new(u256.Uint).Sub(positionLiquidity, liquidityToRemove) - positions[params.tokenId] = position + updated := setPosition(params.tokenId, position) + if !updated { + panic(newErrorWithDetail( + errPositionDoesNotExist, + ufmt.Sprintf("can not decrease liquidity for non-existent position(%d)", params.tokenId), + )) + } // GIVE BACK TO USER _amount0, _amount1 := pl.Collect( @@ -498,7 +873,7 @@ func decreaseLiquidity(params DecreaseLiquidityParams) (uint64, *u256.Uint, *u25 if overflow { position.tokensOwed1 = u256.Zero() } - positions[params.tokenId] = position + setPosition(params.tokenId, position) if position.isClear() { burnPosition(params.tokenId) // just update flag (we don't want to burn actual position) @@ -522,44 +897,21 @@ func Reposition( tokenId uint64, tickLower int32, tickUpper int32, - _amount0Desired string, // uint256 - _amount1Desired string, // uint256 - _amount0Min string, // *u256.Uint - _amount1Min string, // *u256.Uint + amount0DesiredStr string, + amount1DesiredStr string, + amount0MinStr string, + amount1MinStr string, ) (uint64, string, int32, int32, string, string) { common.IsHalted() en.MintAndDistributeGns() - - // verify tokenId exists - if !exists(tokenId) { - panic(addDetailToError( - errDataNotFound, - ufmt.Sprintf("position.gno__Reposition() || tokenId(%d) doesn't exist", tokenId), - )) - } - - // MUST BE OWNER TO REPOSITION - // can not be approved address > staked position can't be modified - owner := gnft.OwnerOf(tokenIdFrom(tokenId)) - caller := std.PrevRealm().Addr() - if owner != caller { - panic(addDetailToError( - errNoPermission, - ufmt.Sprintf("position.gno__Reposition() || only owner(%s) can reposition for tokenId(%d), but called from %s", owner, tokenId, caller), - )) - } + assertTokenOwnership(tokenId) // position should be burned to reposition - position := positions[tokenId] + position := MustGetPosition(tokenId) oldTickLower := position.tickLower oldTickUpper := position.tickUpper - if !(position.isClear()) { - panic(addDetailToError( - errNotClear, - ufmt.Sprintf("position.gno__Reposition() || position(%d) isn't clear(liquidity:%s, tokensOwed0:%s, tokensOwed1:%s)", tokenId, position.liquidity.ToString(), position.tokensOwed0.ToString(), position.tokensOwed1.ToString()), - )) - } + checkPositionHasClear(position) token0, token1, _ := splitOf(position.poolKey) // check if gnot pool @@ -577,7 +929,7 @@ func Reposition( sent := std.GetOrigSend() ugnotSent = uint64(sent.AmountOf("ugnot")) - wrap(ugnotSent, std.PrevRealm().Addr()) + assertWrapNativeToken(ugnotSent, std.PrevRealm().Addr()) } liquidity, amount0, amount1 := addLiquidity( @@ -585,10 +937,10 @@ func Reposition( poolKey: position.poolKey, tickLower: tickLower, tickUpper: tickUpper, - amount0Desired: u256.MustFromDecimal(_amount0Desired), - amount1Desired: u256.MustFromDecimal(_amount1Desired), - amount0Min: u256.MustFromDecimal(_amount0Min), - amount1Min: u256.MustFromDecimal(_amount1Min), + amount0Desired: u256.MustFromDecimal(amount0DesiredStr), + amount1Desired: u256.MustFromDecimal(amount1DesiredStr), + amount0Min: u256.MustFromDecimal(amount0MinStr), + amount1Min: u256.MustFromDecimal(amount1MinStr), caller: std.PrevRealm().Addr(), }, ) @@ -615,7 +967,13 @@ func Reposition( position.tokensOwed1 = u256.Zero() position.burned = false - positions[tokenId] = position + updated := setPosition(tokenId, position) + if !updated { + panic(newErrorWithDetail( + errPositionDoesNotExist, + ufmt.Sprintf("can not reposition non-existent position(%d)", tokenId), + )) + } poolSqrtPriceX96 := pl.PoolGetSlot0SqrtPriceX96(position.poolKey) @@ -647,77 +1005,37 @@ func CollectFee(tokenId uint64, unwrapResult bool) (uint64, string, string, stri common.IsHalted() en.MintAndDistributeGns() - // verify tokenId - if !exists(tokenId) { - panic(addDetailToError( - errDataNotFound, - ufmt.Sprintf("position.gno__CollectFee() || tokenId(%d) doesn't exist", tokenId), - )) - } - - // verify owner or approved + assertTokenExists(tokenId) isAuthorizedForToken(tokenId) // verify position - position, exist := positions[tokenId] - if !exist { - panic(addDetailToError( - errDataNotFound, - ufmt.Sprintf("position.gno__CollectFee() || position(%d) doesn't exist", tokenId), - )) - } + position := MustGetPosition(tokenId) token0, token1, fee := splitOf(position.poolKey) pl.Burn( - token0, - token1, - fee, - position.tickLower, - position.tickUpper, - "0", // burn '0' liquidity to collect fee + token0, token1, fee, + position.tickLower, position.tickUpper, + ZERO_LIQUIDITY_FOR_FEE_COLLECTION, ) - positionKey := positionKeyCompute(GetOrigPkgAddr(), position.tickLower, position.tickUpper) - pool := pl.GetPoolFromPoolPath(position.poolKey) - _feeGrowthInside0LastX128 := pool.PositionFeeGrowthInside0LastX128(positionKey) - _feeGrowthInside1LastX128 := pool.PositionFeeGrowthInside1LastX128(positionKey) - feeGrowthInside0LastX128 := u256.MustFromDecimal(_feeGrowthInside0LastX128.ToString()) - feeGrowthInside1LastX128 := u256.MustFromDecimal(_feeGrowthInside1LastX128.ToString()) - - tokensOwed0 := position.tokensOwed0 - tokensOwed1 := position.tokensOwed1 - - { - diff := new(u256.Uint).Sub(feeGrowthInside0LastX128, position.feeGrowthInside0LastX128) - mulDiv := u256.MulDiv(diff, position.liquidity, u256.MustFromDecimal(consts.Q128)) - - tokensOwed0 = new(u256.Uint).Add(tokensOwed0, mulDiv) + currentFeeGrowth, err := getCurrentFeeGrowth(position, token0, token1, fee) + if err != nil { + panic(newErrorWithDetail(err, "failed to get current fee growth")) } - { - diff := new(u256.Uint).Sub(feeGrowthInside1LastX128, position.feeGrowthInside1LastX128) - mulDiv := u256.MulDiv(diff, position.liquidity, u256.MustFromDecimal(consts.Q128)) - - tokensOwed1 = new(u256.Uint).Add(tokensOwed1, mulDiv) - } - - position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128 - position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128 + tokensOwed0, tokensOwed1 := calculateFees(position, currentFeeGrowth) // check user wugnot amount // need this value to unwrap fee userWugnot := wugnot.BalanceOf(a2u(std.PrevRealm().Addr())) + // collect fee amount0, amount1 := pl.Collect( - token0, - token1, - fee, + token0, token1, fee, std.PrevRealm().Addr(), - position.tickLower, - position.tickUpper, - tokensOwed0.ToString(), - tokensOwed1.ToString(), + position.tickLower, position.tickUpper, + tokensOwed0.ToString(), tokensOwed1.ToString(), ) // sometimes there will be a few less uBase amount than expected due to rounding down in core, but we just subtract the full amount expected @@ -725,7 +1043,13 @@ func CollectFee(tokenId uint64, unwrapResult bool) (uint64, string, string, stri position.tokensOwed0 = new(u256.Uint).Sub(tokensOwed0, u256.MustFromDecimal(amount0)) position.tokensOwed1 = new(u256.Uint).Sub(tokensOwed1, u256.MustFromDecimal(amount1)) - positions[tokenId] = position + updated := setPosition(tokenId, position) + if !updated { + panic(newErrorWithDetail( + errPositionDoesNotExist, + ufmt.Sprintf("can not collect fee for non-existent position(%d)", tokenId), + )) + } // handle withdrawal fee withoutFee0, withoutFee1 := pl.HandleWithdrawalFee(tokenId, token0, amount0, token1, amount1, position.poolKey, std.PrevRealm().Addr()) @@ -757,78 +1081,65 @@ func CollectFee(tokenId uint64, unwrapResult bool) (uint64, string, string, stri return tokenId, withoutFee0, withoutFee1, position.poolKey, amount0, amount1 } -func calculateTokensOwed( - feeGrowthInsideLastX128 *u256.Uint, - positionFeeGrowthInsideLastX128 *u256.Uint, - positionLiquidity *u256.Uint, -) *u256.Uint { - diff := new(u256.Uint).Sub(feeGrowthInsideLastX128, positionFeeGrowthInsideLastX128) - return u256.MulDiv(diff, positionLiquidity, u256.MustFromDecimal(consts.Q128)) -} +// calculateFees calculates the fees for the current position. +func calculateFees(position Position, currentFeeGrowth FeeGrowthInside) (*u256.Uint, *u256.Uint) { + fee0 := calculateTokensOwed( + currentFeeGrowth.feeGrowthInside0LastX128, + position.feeGrowthInside0LastX128, + position.liquidity, + ) -func updateTokensOwed( - feeGrowthInsideLastX128 *u256.Uint, - positionFeeGrowthInsideLastX128 *u256.Uint, - positionLiquidity *u256.Uint, - burnedAmount *u256.Uint, - tokensOwed *u256.Uint, -) *u256.Uint { - additionalTokensOwed := calculateTokensOwed(feeGrowthInsideLastX128, positionFeeGrowthInsideLastX128, positionLiquidity) - add := new(u256.Uint).Add(burnedAmount, additionalTokensOwed) - return new(u256.Uint).Add(tokensOwed, add) + fee1 := calculateTokensOwed( + currentFeeGrowth.feeGrowthInside1LastX128, + position.feeGrowthInside1LastX128, + position.liquidity, + ) + + tokensOwed0 := new(u256.Uint).Add(position.tokensOwed0.Clone(), fee0) + tokensOwed1 := new(u256.Uint).Add(position.tokensOwed1.Clone(), fee1) + + return tokensOwed0, tokensOwed1 } -func burnNFT(tokenId uint64) { - isAuthorizedForToken(tokenId) +func getCurrentFeeGrowth(postion Position, token0, token1 string, fee uint32) (FeeGrowthInside, error) { + pool := pl.GetPoolFromPoolPath(postion.poolKey) + positionKey := positionKeyCompute(GetOrigPkgAddr(), postion.tickLower, postion.tickUpper) - position := positions[tokenId] + feeGrowthInside0 := pool.PositionFeeGrowthInside0LastX128(positionKey) + feeGrowthInside1 := pool.PositionFeeGrowthInside1LastX128(positionKey) - if !(position.isClear()) { - panic(addDetailToError( - errNotClear, - ufmt.Sprintf("position.gno__burnNFT() || position(%d) isn't clear(liquidity:%s, tokensOwed0:%s, tokensOwed1:%s)", tokenId, position.liquidity.ToString(), position.tokensOwed0.ToString(), position.tokensOwed1.ToString()), - )) + feeGrowthInside := FeeGrowthInside{ + feeGrowthInside0LastX128: feeGrowthInside0, + feeGrowthInside1LastX128: feeGrowthInside1, } - delete(positions, tokenId) - gnft.Burn(tokenIdFrom(tokenId)) + + return feeGrowthInside, nil } func burnPosition(tokenId uint64) { - position := positions[tokenId] + position := MustGetPosition(tokenId) if !(position.isClear()) { - panic(addDetailToError( + panic(newErrorWithDetail( errNotClear, - ufmt.Sprintf("position.gno__burnPosition() || position(%d) isn't clear(liquidity:%s, tokensOwed0:%s, tokensOwed1:%s)", tokenId, position.liquidity.ToString(), position.tokensOwed0.ToString(), position.tokensOwed1.ToString()), + ufmt.Sprintf("position(%d) isn't clear(liquidity:%s, tokensOwed0:%s, tokensOwed1:%s)", tokenId, position.liquidity.ToString(), position.tokensOwed0.ToString(), position.tokensOwed1.ToString()), )) } position.burned = true - positions[tokenId] = position -} - -func isAuthorizedForToken(tokenId uint64) { - if !(isOwnerOrOperator(std.PrevRealm().Addr(), tokenId)) { - panic(addDetailToError( - errNoPermission, - ufmt.Sprintf("position.gno__isAuthorizedForToken() || caller(%s) is not approved or owner of tokenId(%d)", std.PrevRealm().Addr(), tokenId), - )) - } -} - -func verifyTokenIdAndOwnership(tokenId uint64) { - if !exists(tokenId) { - panic(addDetailToError( + updated := setPosition(tokenId, position) + if !updated { + panic(newErrorWithDetail( errDataNotFound, - ufmt.Sprintf("position.gno__verifyTokenIdAndOwnership() || tokenId(%d) doesn't exist", tokenId), + ufmt.Sprintf("can not burn non-existent position(%d)", tokenId), )) } +} - owner := gnft.OwnerOf(tokenIdFrom(tokenId)) - caller := std.PrevRealm().Addr() - if owner != caller { - panic(addDetailToError( +func isAuthorizedForToken(tokenId uint64) { + if !(isOwnerOrOperator(std.PrevRealm().Addr(), tokenId)) { + panic(newErrorWithDetail( errNoPermission, - ufmt.Sprintf("position.gno__verifyTokenIdAndOwnership() || only owner(%s) can decrease liquidity(tokenId:%d), but called from %s", owner, tokenId, caller), + ufmt.Sprintf("caller(%s) is not approved or owner of tokenId(%d)", std.PrevRealm().Addr(), tokenId), )) } } @@ -836,21 +1147,13 @@ func verifyTokenIdAndOwnership(tokenId uint64) { func calculateLiquidityToRemove(positionLiquidity *u256.Uint, liquidityRatio uint64) *u256.Uint { liquidityToRemove := new(u256.Uint).Mul(positionLiquidity, u256.NewUint(liquidityRatio)) liquidityToRemove = new(u256.Uint).Div(liquidityToRemove, u256.NewUint(100)) + if positionLiquidity.Lt(liquidityToRemove) || liquidityRatio == 100 { return positionLiquidity } return liquidityToRemove } -func verifyBurnedAmounts(burnedAmount0, burnedAmount1, amount0Min, amount1Min *u256.Uint) { - if !(burnedAmount0.Gte(amount0Min) && burnedAmount1.Gte(amount1Min)) { - panic(addDetailToError( - errSlippage, - ufmt.Sprintf("position.gno__verifyBurnedAmounts() || burnedAmount0(%s) >= amount0Min(%s) && burnedAmount1(%s) >= amount1Min(%s)", burnedAmount0.ToString(), amount0Min.ToString(), burnedAmount1.ToString(), amount1Min.ToString()), - )) - } -} - func handleUnwrap(pToken0, pToken1 string, unwrapResult bool, userOldWugnotBalance uint64, to std.Address) { if (pToken0 == consts.WRAPPED_WUGNOT || pToken1 == consts.WRAPPED_WUGNOT) && unwrapResult { userNewWugnotBalance := wugnot.BalanceOf(a2u(to)) @@ -862,20 +1165,68 @@ func handleUnwrap(pToken0, pToken1 string, unwrapResult bool, userOldWugnotBalan func SetPositionOperator(tokenId uint64, operator std.Address) { caller := std.PrevRealm().PkgPath() if caller != consts.STAKER_PATH { - panic(addDetailToError( + panic(newErrorWithDetail( errNoPermission, - ufmt.Sprintf("position.gno__SetPositionOperator() || caller(%s) is not staker", caller), + ufmt.Sprintf("caller(%s) is not staker", caller), )) } - position, exist := positions[tokenId] + position := MustGetPosition(tokenId) + position.operator = operator + updated := setPosition(tokenId, position) + if !updated { + panic(newErrorWithDetail( + errPositionDoesNotExist, + ufmt.Sprintf("can not set operator for non-existent position(%d)", tokenId), + )) + } +} + +// MustGetPosition returns a position for a given tokenId +// panics if position doesn't exist +func MustGetPosition(tokenId uint64) Position { + position, exist := GetPosition(tokenId) if !exist { - panic(addDetailToError( + panic(newErrorWithDetail( errDataNotFound, - ufmt.Sprintf("position.gno__SetPositionOperator() || position(%d) doesn't exist", tokenId), + ufmt.Sprintf("position with tokenId(%d) doesn't exist", tokenId), )) } + return position +} - position.operator = operator - positions[tokenId] = position +// GetPosition returns a position for a given tokenId +// Returns false if position doesn't exist +func GetPosition(tokenId uint64) (Position, bool) { + tokenIdStr := strconv.FormatUint(tokenId, 10) + iPosition, exist := positions.Get(tokenIdStr) + if !exist { + return Position{}, false + } + + return iPosition.(Position), true +} + +// setPosition sets a position for a given tokenId +// Returns true if position is newly created, false if position already exists and just updated. +func setPosition(tokenId uint64, position Position) bool { + tokenIdStr := strconv.FormatUint(tokenId, 10) + return positions.Set(tokenIdStr, position) +} + +// removePosition removes a position for a given tokenId +func removePosition(tokenId uint64) { + tokenIdStr := strconv.FormatUint(tokenId, 10) + positions.Remove(tokenIdStr) +} + +func positionKeyCompute( + owner std.Address, + tickLower int32, + tickUpper int32, +) string { + key := ufmt.Sprintf("%s__%d__%d", owner.String(), tickLower, tickUpper) + + encoded := base64.StdEncoding.EncodeToString([]byte(key)) + return encoded } diff --git a/position/position_key.gno b/position/position_key.gno deleted file mode 100644 index cde00f2e1..000000000 --- a/position/position_key.gno +++ /dev/null @@ -1,19 +0,0 @@ -package position - -import ( - "encoding/base64" - "std" - - "gno.land/p/demo/ufmt" -) - -func positionKeyCompute( - owner std.Address, - tickLower int32, - tickUpper int32, -) string { - key := ufmt.Sprintf("%s__%d__%d", owner.String(), tickLower, tickUpper) - - encoded := base64.StdEncoding.EncodeToString([]byte(key)) - return encoded -} diff --git a/position/rewrite/README.md b/position/rewrite/README.md new file mode 100644 index 000000000..ae877842d --- /dev/null +++ b/position/rewrite/README.md @@ -0,0 +1,7 @@ +# Description + +TODO: remove this directory. + +temporal directory for saving files that are being rewritten. + +need to check if the files are passing the tests. diff --git a/position/rewrite/position.txt b/position/rewrite/position.txt new file mode 100644 index 000000000..4bfeb31f3 --- /dev/null +++ b/position/rewrite/position.txt @@ -0,0 +1,1635 @@ +package position + +// postion/position.gno implements liquidity position manangement for the Gnoswap protocol. +// It handles the creation, modification, and fee collection od liquidity positions. + +import ( + "encoding/base64" + "std" + + "gno.land/p/demo/ufmt" + "gno.land/r/demo/wugnot" + "gno.land/r/gnoswap/v1/gnft" + + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoswap/v1/common" + "gno.land/r/gnoswap/v1/consts" + + en "gno.land/r/gnoswap/v1/emission" + pl "gno.land/r/gnoswap/v1/pool" +) + +const ( + MAX_RATIO uint64 = 100 + ZERO_LIQUIDITY_FOR_FEE_COLLECTION = "0" +) + +var ( + // TODO (@notJoon): use avl tree, after #440 is merged + positions map[uint64]Position = make(map[uint64]Position) // tokenId -> Position + nextId uint64 = 1 // lp token id +) + +// nextId is the next tokenId to be minted +func getNextId() uint64 { + return nextId +} + +// PositionOperation defines the interface fort all position-related operations. +// Each operation must implement `Validate()` and `Process()` methods. +type PositionOperation interface { + // Validate checks if the operation parameters are valid (wrapper for assert functions). + // Returns an error/panic if validation fails. + Validate() error + + // Process executes the core logic of the operation. + // Returns an `OperationResult` containing all relevant data from the operation. + Process() (*OperationResult, error) +} + +// OperationResult encapsulates the outcomes of a successful mint operation. +// it includes all relavant position details and amounts. +// +// TODO (@notJoon): change type name to `MintResult` +type OperationResult struct { + TokenId uint64 + Liquidity *u256.Uint + Amount0 *u256.Uint + Amount1 *u256.Uint + Fee0 *u256.Uint + Fee1 *u256.Uint + PoolPath string + TickLower int32 + TickUpper int32 +} + +// baseOperation handler +func executeOperation(op PositionOperation) (*OperationResult, error) { + common.IsHalted() + en.MintAndDistributeGns() + + // validate operation + if err := op.Validate(); err != nil { + return nil, err + } + + result, err := op.Process() + if err != nil { + return nil, err + } + + // emiot event after execution + return result, nil +} + +///////////////// MINT /////////////////// +// region: mint + +// Mint creates a new liquidity position with the specified parameters. +// This is the main public interface for creating positions. +// It handles both normal token pairs and pairs involving the native token. +// +// Parameters: +// - token0, token1: Addresses of the tokens in the pair +// - fee: The fee tier for the pool +// - tickLower, tickUpper: Price range boundaries in tick form +// - amount0Desired, amount1Desired: Desired token amounts to deposit +// - amount0Min, amount1Min: Minimum acceptable amounts (slippage protection) +// - deadline: Transaction timeout timestamp +// - mintTo: Recipient address for the position token +// - caller: Transaction initiator address +// +// Returns: +// - tokenId: Unique identifier for the minted position +// - liquidity: Amount of liquidity tokens minted +// - amount0, amount1: Actual amounts of tokens deposited +// +// May panic with: +// - errInvalidInput: If input validation fails +// - errProcessing: If the mint operation fails +// +// for more details, see: https://docs.gnoswap.io/contracts/position/position.gno#mint +func Mint( + token0, token1 string, + fee uint32, + tickLower, tickUpper int32, + amount0Desired, amount1Desired string, + amount0Min, amount1Min string, + deadline int64, + mintTo, caller std.Address, +) (uint64, string, string, string) { + input := MintInput{ + token0: token0, + token1: token1, + fee: fee, + tickLower: tickLower, + tickUpper: tickUpper, + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + amount0Min: amount0Min, + amount1Min: amount1Min, + deadline: deadline, + mintTo: mintTo, + caller: caller, + } + + op := NewMintOperation(input) + result, err := executeOperation(op) + if err != nil { + panic(err) + } + + prevAddr, prevPkgPath := getPrevAsString() + poolSqrtPriceX96 := pl.PoolGetSlot0SqrtPriceX96(result.PoolPath) + + std.Emit( + "Mint", + "prevAddr", prevAddr, + "prevRealm", prevPkgPath, + "tickLower", ufmt.Sprintf("%d", result.TickLower), + "tickUpper", ufmt.Sprintf("%d", result.TickUpper), + "poolPath", result.PoolPath, + "caller", op.input.caller.String(), + "internal_lpTokenId", ufmt.Sprintf("%d", result.TokenId), + "internal_liquidity", result.Liquidity.ToString(), + "internal_amount0", result.Amount0.ToString(), + "internal_amount1", result.Amount1.ToString(), + "internal_sqrtPriceX96", poolSqrtPriceX96, + ) + + return result.TokenId, result.Liquidity.ToString(), result.Amount0.ToString(), result.Amount1.ToString() +} + +// MintOperation encasulates the logic for creating a new liquidity position. +// it handles input validation, position creation, and token transfers. +type MintOperation struct { + input MintInput + processedInput *ProcessedMintInput +} + +// NewMintOperation creates a new instance of MintOperation with the given input parameters. +// It initializes the operation without processing the input data. +func NewMintOperation(input MintInput) *MintOperation { + return &MintOperation{input: input} +} + +// MintInput represents all necessary parameters for creating a new liquidity position. +// All values are passes are as strings to eradiacate original value pollution (especially large amounts).. +type MintInput struct { + // Token addresses for the pool pair + token0 string + token1 string + + // fee tier for the pool (e.g., 0.3% = 3000) + fee uint32 + + // Price range boundaries in tick form + tickLower int32 + tickUpper int32 + + // Desired amounts of tokens to deposit + amount0Desired string + amount1Desired string + + // Minimum amounts to prevent excessive slippage + amount0Min string + amount1Min string + + // Transaction deadline timestamp + deadline int64 + + // Address to receive the LP token + mintTo std.Address + + // Transaction initiator (or, caller) address + caller std.Address +} + +type TokenPair struct { + token0 string + token1 string + token0IsNative bool + token1IsNative bool +} + +type ProcessedMintInput struct { + tokenPair TokenPair + amount0Desired *u256.Uint + amount1Desired *u256.Uint + amount0Min *u256.Uint + amount1Min *u256.Uint + tickLower int32 + tickUpper int32 + poolPath string +} + +// Validate performs all necessary checks on the mint input parameters. +// +// It verifies: +// - Caller permission +// - Token pair validity +// - Price range validity +// - Amounts validity +// +// Returns an error or panic if any of the checks fail. +func (op *MintOperation) Validate() error { + assertCallerPermission(std.PrevRealm()) + + processedInput, err := processMintInput(op.input) + if err != nil { + return err + } + + op.processedInput = &processedInput + + return nil +} + +// Process executes the mint operation after validation. +// +// It handles: +// - Token transfers and wrapping/unwrapping of native tokens +// - Liquidity position creation +// - Position token minting +// +// Returns the operation result or an error if the process fails. +func (op *MintOperation) Process() (*OperationResult, error) { + // Track initial WUGNOT balance + usrWugnotBalance := wugnot.BalanceOf(a2u(op.input.caller)) + + // handle native token conversion if needed + if err := handleNativeToken( + op.processedInput.tokenPair.token0IsNative, + op.processedInput.tokenPair.token1IsNative, + op.input.caller, + ); err != nil { + return nil, err + } + + // create mint params and execute mint + mintParams := newMintParams(*op.processedInput, op.input) + tokenId, liquidity, amount0, amount1 := mint(mintParams) + + // handle leftover native token + // TODO (@notJoon): Are there any cases where this function shouldn't be called? + handleLeftoverNativeToken( + op.processedInput.tokenPair.token0IsNative, + op.processedInput.tokenPair.token1IsNative, + usrWugnotBalance, + op.input.caller, + ) + + return &OperationResult{ + TokenId: tokenId, + Liquidity: liquidity, + Amount0: amount0, + Amount1: amount1, + PoolPath: op.processedInput.poolPath, + TickLower: op.processedInput.tickLower, + TickUpper: op.processedInput.tickUpper, + }, nil +} + +func mint(params MintParams) (uint64, *u256.Uint, *u256.Uint, *u256.Uint) { + checkDeadline(params.deadline) + + pool := pl.GetPool(params.token0, params.token1, params.fee) + liquidity, amount0, amount1, err := addLiquidity( + AddLiquidityParams{ + poolKey: pl.GetPoolPath(params.token0, params.token1, params.fee), + tickLower: params.tickLower, + tickUpper: params.tickUpper, + amount0Desired: params.amount0Desired, + amount1Desired: params.amount1Desired, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + caller: params.caller, + }, + ) + if err != nil { + panic(addDetailToError(errSlippage, err.Error())) + } + + tokenId := nextId + gnft.Mint(a2u(params.mintTo), tokenIdFrom(tokenId)) // owner, tokenId + nextId++ + + /* State Update */ + + positionKey := positionKeyCompute(GetOrigPkgAddr(), params.tickLower, params.tickUpper) + feeGrowthInside0LastX128 := pool.PositionFeeGrowthInside0LastX128(positionKey) + feeGrowthInside1LastX128 := pool.PositionFeeGrowthInside1LastX128(positionKey) + + position := Position{ + nonce: u256.Zero(), + operator: consts.ZERO_ADDRESS, + poolKey: pl.GetPoolPath(params.token0, params.token1, params.fee), + tickLower: params.tickLower, + tickUpper: params.tickUpper, + liquidity: liquidity, + feeGrowthInside0LastX128: feeGrowthInside0LastX128, + feeGrowthInside1LastX128: feeGrowthInside1LastX128, + tokensOwed0: u256.Zero(), + tokensOwed1: u256.Zero(), + burned: false, + } + positions[tokenId] = position + + return tokenId, liquidity, amount0, amount1 +} + +///////////////// INCREASE LIQUIDITY /////////////////// +// region: increaseLiquidity + +// IncreaseLiquidity represents the parameters needed to add liquidity to an existing position. +type IncreaseLiquidityInput struct { + // Unique identifier of the existing position. + // this data comes from the `position` type. + TokenId uint64 + + // Additional amounts to deposit + Amount0Desired *u256.Uint + Amount1Desired *u256.Uint + + // Minimum acceptable amounts (slippage protection) + Amount0Min *u256.Uint + Amount1Min *u256.Uint + + // Transaction deadline timestamp + Deadline int64 +} + +// IncreaseLiquidityResult encapsulates the outcome of a liquidity increase operation. +func IncreaseLiquidity( + tokenId uint64, + amount0Desired string, + amount1Desired string, + amount0Min string, + amount1Min string, + deadline int64, +) (uint64, string, string, string, string) { + input := IncreaseLiquidityInput{ + TokenId: tokenId, + Amount0Desired: u256.MustFromDecimal(amount0Desired), + Amount1Desired: u256.MustFromDecimal(amount1Desired), + Amount0Min: u256.MustFromDecimal(amount0Min), + Amount1Min: u256.MustFromDecimal(amount1Min), + Deadline: deadline, + } + + op := NewIncreaseLiquidityOperation(input) + + if err := op.Validate(); err != nil { + panic(addDetailToError(errInvalidInput, err.Error())) + } + + result, err := op.Process() + if err != nil { + panic(err) + } + + prevAddr, prevPkgPath := getPrevAsString() + + std.Emit( + "IncreaseLiquidity", + "prevAddr", prevAddr, + "prevRealm", prevPkgPath, + "lpTokenId", ufmt.Sprintf("%d", result.TokenId), + "internal_poolPath", result.PoolPath, + "internal_liquidity", result.Liquidity.ToString(), + "internal_amount0", result.Amount0.ToString(), + "internal_amount1", result.Amount1.ToString(), + "internal_sqrtPriceX96", pl.PoolGetSlot0SqrtPriceX96(result.PoolPath), + ) + + return result.TokenId, result.Liquidity.ToString(), result.Amount0.ToString(), result.Amount1.ToString(), result.PoolPath +} + +// IncreaseLiquidityResult contains the outcome of an increase liquidity operation. +// It provides detailed information about the liquidity added and tokens used. +type IncreaseLiquidityResult struct { + // Position identifier + TokenId uint64 + + // Additional liquidity minted + Liquidity *u256.Uint + + // Actual amounts of tokens deposited + Amount0 *u256.Uint + Amount1 *u256.Uint + + // pool path + PoolPath string +} + +// IncreaseLiquidityOperation represents the increase liquidity operation. +// It handles the validation and execution of adding liquidity to an existing position. +type IncreaseLiquidityOperation struct { + // input contains the parameters for the increase operation + input IncreaseLiquidityInput + + // position holds the current state of the position being modified + position Position +} + +// NewIncreaseLiquidityOperation creates a new instance of IncreaseLiquidityOperation. +// It initializes the operation with the provided input parameters. +// +// Parameters: +// - input: The increase liquidity parameters +// +// Returns: +// - *IncreaseLiquidityOperation: A new operation instance +func NewIncreaseLiquidityOperation(input IncreaseLiquidityInput) *IncreaseLiquidityOperation { + return &IncreaseLiquidityOperation{input: input} +} + +// Validate performs validation checks on the increase liquidity operation. +// It ensures the operation can be executed safely and the caller has proper permissions. +// +// Checks performed: +// - Position exists +// - Caller owns the position token +// - Position is not burned +// +// Returns: +// - error: nil if validation passes, error otherwise +func (op *IncreaseLiquidityOperation) Validate() error { + position, exists := positions[op.input.TokenId] + if !exists { + return ufmt.Errorf("position %d not found", op.input.TokenId) + } + op.position = position + + assertTokenOwnership(op.input.TokenId) + + return nil +} + +// Process executes the increase liquidity operation after validation. +// It handles the core logic of adding liquidity and updating the position state. +// +// Steps performed: +// 1. Tracks initial WUGNOT balance if native token is involved +// 2. Adds the new liquidity to the pool +// 3. Calculates and updates fee growth tracking +// 4. Updates position state with new liquidity and fee data +// 5. Handles native token unwrapping if needed +// +// Returns: +// - *IncreaseLiquidityResult: The operation results +// - error: Any errors that occurred during processing +func (op *IncreaseLiquidityOperation) Process() (*IncreaseLiquidityResult, error) { + // get initial WUGNOT balance + userOldWugnotBalance := wugnot.BalanceOf(a2u(std.PrevRealm().Addr())) + + // handle native token if needed + // TODO (@notJoon): Extract this logic to a separate function + pToken0, pToken1, _ := splitOf(op.position.poolKey) + isToken0Wugnot := pToken0 == consts.WRAPPED_WUGNOT + isToken1Wugnot := pToken1 == consts.WRAPPED_WUGNOT + + if hasWugnotToken(isToken0Wugnot, isToken1Wugnot) { + sent := std.GetOrigSend() + ugnotSent := uint64(sent.AmountOf("ugnot")) + + assertWrapNativeToken(ugnotSent, std.PrevRealm().Addr()) + } + + liqParams := AddLiquidityParams{ + poolKey: op.position.poolKey, + tickLower: op.position.tickLower, + tickUpper: op.position.tickUpper, + amount0Desired: op.input.Amount0Desired, + amount1Desired: op.input.Amount1Desired, + amount0Min: op.input.Amount0Min, + amount1Min: op.input.Amount1Min, + caller: std.PrevRealm().Addr(), + } + + liquidity, amount0, amount1, err := addLiquidity(liqParams) + if err != nil { + return nil, err + } + + pool := pl.GetPoolFromPoolPath(op.position.poolKey) + positionKey := positionKeyCompute(GetOrigPkgAddr(), op.position.tickLower, op.position.tickUpper) + + currentFeeGrowth := FeeGrowthInside{ + feeGrowthInside0LastX128: pool.PositionFeeGrowthInside0LastX128(positionKey), + feeGrowthInside1LastX128: pool.PositionFeeGrowthInside1LastX128(positionKey), + } + + feeUpdate := calculatePositionFeeUpdate(op.position, currentFeeGrowth) + updatedPosition := updatePosition(op.position, feeUpdate, liquidity) + + positions[op.input.TokenId] = updatedPosition + + if hasWugnotToken(isToken0Wugnot, isToken1Wugnot) { + usrNewWugnotBalance := wugnot.BalanceOf(a2u(std.PrevRealm().Addr())) + leftover := usrNewWugnotBalance - userOldWugnotBalance + unwrap(leftover, std.PrevRealm().Addr()) + } + + return &IncreaseLiquidityResult{ + TokenId: op.input.TokenId, + Liquidity: liquidity, + Amount0: amount0, + Amount1: amount1, + PoolPath: op.position.poolKey, + }, nil +} + +// FeeGrowthInside represents fee growth inside ticks +type FeeGrowthInside struct { + feeGrowthInside0LastX128 *u256.Uint + feeGrowthInside1LastX128 *u256.Uint +} + +// PositionFeeUpdate represents fee update calculation result +type PositionFeeUpdate struct { + tokensOwed0 *u256.Uint + tokensOwed1 *u256.Uint + feeGrowthInside0LastX128 *u256.Uint + feeGrowthInside1LastX128 *u256.Uint +} + +// calculateTokensOwed determines the amount of fees owed based on fee growth and liquidity. +// +// Parameters: +// - currentFeeGrowth: Current cumulative fee growth +// - lastFeeGrowth: Last recorded fee growth for the position +// - liquidity: Amount of liquidity for fee calculation +// +// Returns: +// - *u256.Uint: Amount of tokens owed as fees +func calculatePositionFeeUpdate( + position Position, + currentFeeGrowth FeeGrowthInside, +) PositionFeeUpdate { + tokensOwed0 := calculateTokensOwed( + currentFeeGrowth.feeGrowthInside0LastX128, + position.feeGrowthInside0LastX128, + position.liquidity, + ) + + tokensOwed1 := calculateTokensOwed( + currentFeeGrowth.feeGrowthInside1LastX128, + position.feeGrowthInside1LastX128, + position.liquidity, + ) + + return PositionFeeUpdate{ + tokensOwed0: new(u256.Uint).Add(position.tokensOwed0, tokensOwed0), + tokensOwed1: new(u256.Uint).Add(position.tokensOwed1, tokensOwed1), + feeGrowthInside0LastX128: currentFeeGrowth.feeGrowthInside0LastX128.Clone(), + feeGrowthInside1LastX128: currentFeeGrowth.feeGrowthInside1LastX128.Clone(), + } +} + +func calculateTokensOwed( + currentFeeGrowth *u256.Uint, + lastFeeGrowth *u256.Uint, + liquidity *u256.Uint, +) *u256.Uint { + diff := new(u256.Uint).Sub(currentFeeGrowth, lastFeeGrowth) + return u256.MulDiv(diff, liquidity, u256.MustFromDecimal(consts.Q128)) +} + +func updateTokensOwed( + feeGrowthInsideLastX128 *u256.Uint, + positionFeeGrowthInsideLastX128 *u256.Uint, + positionLiquidity *u256.Uint, + burnedAmount *u256.Uint, + tokensOwed *u256.Uint, +) *u256.Uint { + additionalTokensOwed := calculateTokensOwed(feeGrowthInsideLastX128, positionFeeGrowthInsideLastX128, positionLiquidity) + add := new(u256.Uint).Add(burnedAmount, additionalTokensOwed) + return new(u256.Uint).Add(tokensOwed, add) +} + +// updatePosition updates the position state with new liquidity and fee data. +// +// Parameters: +// - position: Current position state +// - feeUpdate: New fee data to apply +// - newLiquidity: Additional liquidity being added +// +// Returns: +// - Position: Updated position state +// +// The function applies the fee updates and adds the new liquidity while maintaining +// the position's other properties. +func updatePosition( + position Position, + feeUpdate PositionFeeUpdate, + newLiquidity *u256.Uint, +) Position { + position.tokensOwed0 = feeUpdate.tokensOwed0 + position.tokensOwed1 = feeUpdate.tokensOwed1 + position.feeGrowthInside0LastX128 = feeUpdate.feeGrowthInside0LastX128 + position.feeGrowthInside1LastX128 = feeUpdate.feeGrowthInside1LastX128 + position.liquidity = new(u256.Uint).Add(position.liquidity, newLiquidity) + position.burned = false + + return position +} + +///////////////// DECREASE LIQUIDITY /////////////////// +// region: decreaseLiquidity + +// DecreaseLiquidity represents the parameters needed to reduce liquidity from an existing position. +func DecreaseLiquidity( + tokenId uint64, + liquidityRatio uint64, + amount0Min string, + amount1Min string, + deadline int64, + unwrapResult bool, +) (uint64, string, string, string, string, string, string) { + common.IsHalted() + en.MintAndDistributeGns() + + input := DecreaseLiquidityInput{ + TokenId: tokenId, + LiquidityRatio: liquidityRatio, + Amount0Min: u256.MustFromDecimal(amount0Min), + Amount1Min: u256.MustFromDecimal(amount1Min), + Deadline: deadline, + Unwrap: unwrapResult, + } + + op := NewDecreaseLiquidityOperation(input) + + if err := op.Validate(); err != nil { + panic(addDetailToError(errInvalidInput, err.Error())) + } + + result, err := op.Process() + if err != nil { + panic(err) + } + + prevAddr, prevPkgPath := getPrevAsString() + + std.Emit( + "decreaseLiquidity", + "prevAddr", prevAddr, + "prevRealm", prevPkgPath, + "lpTokenId", ufmt.Sprintf("%d", result.TokenId), + "internal_poolPath", result.PoolPath, + "internal_liquidity", result.Liquidity.ToString(), + "internal_amount0", result.Amount0.ToString(), + "internal_amount1", result.Amount1.ToString(), + "internal_sqrtPriceX96", pl.PoolGetSlot0SqrtPriceX96(result.PoolPath), + ) + + return result.TokenId, result.Liquidity.ToString(), result.Fee0.ToString(), result.Fee1.ToString(), result.Amount0.ToString(), result.Amount1.ToString(), result.PoolPath +} + +// DecreaseLiquidityInput encapsulates the parameters required for reducing a position's liquidity. +// It contains all necessary information to safely remove liquidity from an existing position. +type DecreaseLiquidityInput struct { + // TokenId uniquely identifies the position to be decrease liquidity from + TokenId uint64 + + // LiquidityRatio represents the percentage (1-100) of liquidity + LiquidityRatio uint64 + + // Amount0Min and Amount1Min specify the minimum acceptable token amounts + // to protect against price slippage during the decrease operation + Amount0Min *u256.Uint + Amount1Min *u256.Uint + + // Deadline specifies the timestamp by which the transaction must be executed + Deadline int64 + + // Unwrap determines whether native tokens should be unwrapped after + // the liquidity decrease operation + Unwrap bool +} + +// DecreaseLiquidityResult contains the outcome of a decrease liquidity operation. +// It provides comprehensive information about the amounts of tokens and fees involved. +type DecreaseLiquidityResult struct { + // TokenId identifies the affected position + TokenId uint64 + + // Liquidity represents the amount of liquidity removed from the position + Liquidity *u256.Uint + + // Fee0 and Fee1 represent the accumulated fees collected during the operation + Fee0 *u256.Uint + Fee1 *u256.Uint + + // Amount0 and Amount1 represent the actual amounts of tokens withdrawn + Amount0 *u256.Uint + Amount1 *u256.Uint + + // PoolPath identifies the pool where the operation occurred + PoolPath string +} + +// DecreaseLiquidityOperation represents the decrease liquidity operation. +// It handles the validation and execution of liquidity removal from a position. +type DecreaseLiquidityOperation struct { + // input contains the parameters for the decrease operation + input DecreaseLiquidityInput + + // position holds the current state of the position being modified + position Position +} + +// NewDecreaseLiquidityOperation creates a new instance of DecreaseLiquidityOperation. +// It initializes the operation with the provided input parameters. +// +// Parameters: +// - input: The decrease liquidity parameters +// +// Returns: +// - *DecreaseLiquidityOperation: A new operation instance +func NewDecreaseLiquidityOperation(input DecreaseLiquidityInput) *DecreaseLiquidityOperation { + return &DecreaseLiquidityOperation{input: input} +} + +// Validate performs validation checks on the decrease liquidity operation. +// It ensures all parameters are valid and the operation can be executed safely. +// +// Checks performed: +// - LiquidityRatio is between 1 and 100 +// - Position exists and has liquidity +// - Caller has permission to modify the position +// +// Returns: +// - error: nil if validation passes, error otherwise +func (op *DecreaseLiquidityOperation) Validate() error { + // check range + if op.input.LiquidityRatio < 1 || op.input.LiquidityRatio > MAX_RATIO { + return ufmt.Errorf("liquidity ratio must be between 1 and 100") + } + + tokenId := op.input.TokenId + + position, exists := positions[tokenId] + if !exists { + return ufmt.Errorf("position %d not found", tokenId) + } + op.position = position + + if op.position.liquidity.IsZero() { + return ufmt.Errorf("position(tokenId: %d) has no liquidity", op.input.TokenId) + } + + verifyTokenIdAndOwnership(tokenId) + + return nil +} + +// Process executes the decrease liquidity operation after validation. +// It handles the core logic of removing liquidity and collecting fees. +// +// Steps performed: +// 1. Collects any accumulated fees +// 2. Calculates the amount of liquidity to remove +// 3. Burns the specified amount of liquidity +// 4. Updates position state +// 5. Handles native token unwrapping if requested +// +// Returns: +// - *DecreaseLiquidityResult: The operation results +// - error: Any errors that occurred during processing +func (op *DecreaseLiquidityOperation) Process() (*DecreaseLiquidityResult, error) { + userOldWugnotBalance := wugnot.BalanceOf(a2u(std.PrevRealm().Addr())) + + _, fee0Str, fee1Str, _, _, _ := CollectFee(op.input.TokenId, op.input.Unwrap) + fee0 := u256.MustFromDecimal(fee0Str) + fee1 := u256.MustFromDecimal(fee1Str) + + liquidityToRemove := calculateLiquidityToRemove(op.position.liquidity, op.input.LiquidityRatio) + + pToken0, pToken1, pFee := splitOf(op.position.poolKey) + burned0, burned1 := pl.Burn(pToken0, pToken1, pFee, op.position.tickLower, op.position.tickUpper, liquidityToRemove.ToString()) + + burnedAmount0 := u256.MustFromDecimal(burned0) + burnedAmount1 := u256.MustFromDecimal(burned1) + + verifyBurnedAmounts(burnedAmount0, burnedAmount1, op.input.Amount0Min, op.input.Amount1Min) + + pool := pl.GetPoolFromPoolPath(op.position.poolKey) + positionKey := positionKeyCompute(GetOrigPkgAddr(), op.position.tickLower, op.position.tickUpper) + + // TODO: create New function + currentFeeGrowth := FeeGrowthInside{ + feeGrowthInside0LastX128: pool.PositionFeeGrowthInside0LastX128(positionKey), + feeGrowthInside1LastX128: pool.PositionFeeGrowthInside1LastX128(positionKey), + } + + op.position.tokensOwed0 = updateTokensOwed( + currentFeeGrowth.feeGrowthInside0LastX128, + op.position.feeGrowthInside0LastX128, + op.position.liquidity, + burnedAmount0, + op.position.tokensOwed0, + ) + + op.position.tokensOwed1 = updateTokensOwed( + currentFeeGrowth.feeGrowthInside1LastX128, + op.position.feeGrowthInside1LastX128, + op.position.liquidity, + burnedAmount1, + op.position.tokensOwed1, + ) + + // update position fields + op.position.liquidity = new(u256.Uint).Sub(op.position.liquidity, liquidityToRemove) + op.position.feeGrowthInside0LastX128 = currentFeeGrowth.feeGrowthInside0LastX128 + op.position.feeGrowthInside1LastX128 = currentFeeGrowth.feeGrowthInside1LastX128 + + collectedAmount0, collectedAmount1 := pl.Collect( + pToken0, pToken1, pFee, + std.PrevRealm().Addr(), + op.position.tickLower, op.position.tickUpper, + burned0, burned1, + ) + + amount0 := u256.MustFromDecimal(collectedAmount0) + amount1 := u256.MustFromDecimal(collectedAmount1) + + // Update tokens owed + overflow := false + op.position.tokensOwed0, overflow = new(u256.Uint).SubOverflow(op.position.tokensOwed0, amount0) + if overflow { + op.position.tokensOwed0 = u256.Zero() + } + + op.position.tokensOwed1, overflow = new(u256.Uint).SubOverflow(op.position.tokensOwed1, amount1) + if overflow { + op.position.tokensOwed1 = u256.Zero() + } + + positions[op.input.TokenId] = op.position + + if op.position.isClear() { + burnPosition(op.input.TokenId) + } + + if op.input.Unwrap { + handleUnwrap(pToken0, pToken1, op.input.Unwrap, userOldWugnotBalance, std.PrevRealm().Addr()) + } + + return &DecreaseLiquidityResult{ + TokenId: op.input.TokenId, + Liquidity: liquidityToRemove, + Fee0: fee0, + Fee1: fee1, + Amount0: amount0, + Amount1: amount1, + PoolPath: op.position.poolKey, + }, nil +} + +// calculateLiquidityToRemove determines the amount of liquidity to remove based on +// the current position liquidity and the requested ratio. +// +// Parameters: +// - positionLiquidity: Current liquidity in the position +// - liquidityRatio: Percentage of liquidity to remove (1-100) +// +// Returns: +// - *u256.Uint: Amount of liquidity to remove +// +// Note: If liquidityRatio is 100 or the calculated amount exceeds position liquidity, +// the entire position liquidity is removed. +func calculateLiquidityToRemove(positionLiquidity *u256.Uint, liquidityRatio uint64) *u256.Uint { + liquidityToRemove := new(u256.Uint).Mul(positionLiquidity, u256.NewUint(liquidityRatio)) + liquidityToRemove = new(u256.Uint).Div(liquidityToRemove, u256.NewUint(MAX_RATIO)) + + if positionLiquidity.Lt(liquidityToRemove) || liquidityRatio == MAX_RATIO { + return positionLiquidity + } + + return liquidityToRemove +} + +// burnPosition marks a position as burned when it no longer has any remaining liquidity +// or uncollected fees. +func burnPosition(tokenId uint64) { + position := positions[tokenId] + checkPositionHasClear(position) + + position.burned = true + positions[tokenId] = position +} + +// handleUnwrap manages the unwrapping of native tokens after a decrease liquidity operation. +// +// The function calculates the difference in WUGNOT balance and unwraps any excess +// back to native tokens if requested. +func handleUnwrap(pToken0, pToken1 string, unwrapResult bool, userOldWugnotBalance uint64, to std.Address) { + if (pToken0 == consts.WRAPPED_WUGNOT || pToken1 == consts.WRAPPED_WUGNOT) && unwrapResult { + userNewWugnotBalance := wugnot.BalanceOf(a2u(to)) + leftOver := userNewWugnotBalance - userOldWugnotBalance + unwrap(leftOver, to) + } +} + +///////////////// CollectFee /////////////////// +// region: collectFee + +// CollectFee collects accumulated fees from a liquidity position. +// This is the main entry point for fee collection in the protocol. +// +// Parameters: +// - tokenId: The ID of the position to collect fees from +// - unwrapResult: Whether to unwrap native tokens after collection +// +// Returns: +// - tokenId: Position identifier +// - fee0, fee1: Collected fee amounts after protocol fee +// - poolPath: Pool identifier +// - origFee0, origFee1: Original fee amounts before protocol fee +// +// Events emitted: +// - CollectSwapFee: When fees are successfully collected +// +// The function handles both standard ERC20 fees and wrapped native token fees. +// It ensures proper calculation and distribution of protocol fees. +func CollectFee(tokenId uint64, unwrapResult bool) (uint64, string, string, string, string, string) { + common.IsHalted() + en.MintAndDistributeGns() + + input := CollectFeeInput{ + TokenId: tokenId, + Unwrap: unwrapResult, + } + + op := NewCollectFeeOperation(input) + + if err := op.Validate(); err != nil { + panic(addDetailToError(errInvalidInput, err.Error())) + } + + result, err := op.Process() + if err != nil { + panic(err) + } + + prevAddr, prevPkgPath := getPrevAsString() + + std.Emit( + "CollectSwapFee", + "prevAddr", prevAddr, + "prevRealm", prevPkgPath, + "lpTokenId", ufmt.Sprintf("%d", result.TokenId), + "internal_fee0", result.Fee0.ToString(), + "internal_fee1", result.Fee1.ToString(), + "internal_poolPath", result.PoolPath, + "internal_unwrapResult", ufmt.Sprintf("%t", op.input.Unwrap), + ) + + return result.TokenId, result.Fee0.ToString(), result.Fee1.ToString(), result.PoolPath, result.OrigFee0, result.OrigFee1 +} + +// CollectFeeInput encapsulates the parameters needed to collect accumulated fees +// from a liquidity position. It provides the necessary information to identify +// the position and handle native token unwrapping. +type CollectFeeInput struct { + // TokenId uniquely identifies the position to collect fees from + TokenId uint64 + + // Unwrap determines whether native tokens should be unwrapped + // after fee collection. This is relevant when one of the tokens + // in the pool is the wrapped native token (WUGNOT). + Unwrap bool +} + +// CollectFeeResult contains the outcome of a fee collection operation. +// It provides detailed information about the collected fees and their origin. +type CollectFeeResult struct { + // TokenId identifies the position fees were collected from + TokenId uint64 + + // Fee0 and Fee1 represent the amounts of collected fees for each token + // after protocol fee deduction + Fee0 *u256.Uint + Fee1 *u256.Uint + + // PoolPath identifies the pool where the fees were collected + PoolPath string + + // OrigFee0 and OrigFee1 represent the original amounts of fees before + // protocol fee deduction + OrigFee0 string + OrigFee1 string +} + +// CollectFeeOperation represents the fee collection operation. +// It handles the validation and execution of collecting accumulated fees +// from a liquidity position. +type CollectFeeOperation struct { + // input contains the parameters for the fee collection. + input CollectFeeInput + + // position holds the current state of the position being modified + position Position +} + +// NewCollectFeeOperation creates a new instance of CollectFeeOperation. +// It initializes the operation with the provided input parameters. +// +// Parameters: +// - input: The fee collection parameters +// +// Returns: +// - *CollectFeeOperation: A new operation instance +func NewCollectFeeOperation(input CollectFeeInput) *CollectFeeOperation { + return &CollectFeeOperation{ input: input } +} + +// Validate performs validation checks on the fee collection operation. +// It ensures the operation can be executed safely and the caller has proper permissions. +// +// Checks performed: +// - Position exists +// - Caller is authorized to collect fees +// - Position is not burned +// +// Returns: +// - error: nil if validation passes, error otherwise +func (op *CollectFeeOperation) Validate() error { + assertTokenExists(op.input.TokenId) + isAuthorizedForToken(op.input.TokenId) + + // get and verify position + position, exists := positions[op.input.TokenId] + if !exists { + return ufmt.Errorf("position(tokenId: %d) not found", op.input.TokenId) + } + op.position = position + + return nil +} + +// Process executes the fee collection operation after validation. +// It handles the core logic of collecting fees and updating position state. +// +// Steps performed: +// 1. Tracks initial WUGNOT balance if native token unwrap is requested +// 2. Updates fees by performing a zero-liquidity burn +// 3. Calculates current fee growth and collectable amounts +// 4. Collects fees from the pool +// 5. Updates position state +// 6. Handles protocol fee deduction +// 7. Unwraps native tokens if requested +// +// Returns: +// - *CollectFeeResult: The operation results +// - error: Any errors that occurred during processing +func (op *CollectFeeOperation) Process() (*CollectFeeResult, error) { + token0, token1, fee := splitOf(op.position.poolKey) + + prevRealmAddr := std.PrevRealm().Addr() + + // check initial WUGNOT balance for potential unwrap + userWugnot := wugnot.BalanceOf(a2u(prevRealmAddr)) + + // burn with zero liquidity to update fees + pl.Burn( + token0, token1, fee, + op.position.tickLower, op.position.tickUpper, + ZERO_LIQUIDITY_FOR_FEE_COLLECTION, + ) + + // get current fee growth + currentFeeGrowth, err := getCurrentFeeGrowth(op.position, token0, token1, fee) + if err != nil { + return nil, err + } + + tokensOwed0, tokensOwed1 := calculateFees(op.position, currentFeeGrowth) + + amount0, amount1 := pl.Collect( + token0, token1, fee, + prevRealmAddr, + op.position.tickLower, op.position.tickUpper, + tokensOwed0.ToString(), tokensOwed1.ToString(), + ) + + // sometimes there will be a few less uBase amount than expected due to rounding down in core, but we just subtract the full amount expected + // instead of the actual amount so we can burn the token + op.position.tokensOwed0 = new(u256.Uint).Sub(tokensOwed0, u256.MustFromDecimal(amount0)) + op.position.tokensOwed1 = new(u256.Uint).Sub(tokensOwed1, u256.MustFromDecimal(amount1)) + positions[op.input.TokenId] = op.position + + // handle withdrawal fee + withoutFee0, withoutFee1 := pl.HandleWithdrawalFee( + op.input.TokenId, + token0, amount0, + token1, amount1, + op.position.poolKey, + prevRealmAddr, + ) + + // handle WUGNOT unwrap if needed + if op.input.Unwrap { + pToken0, pToken1, _ := splitOf(op.position.poolKey) + if pToken0 == consts.WUGNOT_PATH || pToken1 == consts.WUGNOT_PATH { + userNewWugnot := wugnot.BalanceOf(a2u(prevRealmAddr)) + unwrapAmount := userNewWugnot - userWugnot + + if unwrapAmount > 0 { + unwrap(unwrapAmount, std.PrevRealm().Addr()) + } + } + } + + return &CollectFeeResult{ + TokenId: op.input.TokenId, + Fee0: u256.MustFromDecimal(withoutFee0), + Fee1: u256.MustFromDecimal(withoutFee1), + PoolPath: op.position.poolKey, + OrigFee0: amount0, + OrigFee1: amount1, + }, nil +} + +// getCurrentFeeGrowth retrieves the current fee growth values from the pool +// for a specific position. +// +// Parameters: +// - position: The position to get fee growth for +// - token0, token1: Pool token addresses +// - fee: Pool fee tier +// +// Returns: +// - FeeGrowthInside: Current fee growth values +// - error: Any errors during retrieval +// +// This function is used to calculate the fees accumulated since the last collection. +func getCurrentFeeGrowth(postion Position, token0, token1 string, fee uint32) (FeeGrowthInside, error) { + pool := pl.GetPoolFromPoolPath(postion.poolKey) + positionKey := positionKeyCompute(GetOrigPkgAddr(), postion.tickLower, postion.tickUpper) + + feeGrowthInside0 := pool.PositionFeeGrowthInside0LastX128(positionKey) + feeGrowthInside1 := pool.PositionFeeGrowthInside1LastX128(positionKey) + + feeGrowthInside := FeeGrowthInside{ + feeGrowthInside0LastX128: feeGrowthInside0, + feeGrowthInside1LastX128: feeGrowthInside1, + } + + return feeGrowthInside, nil +} + +// calculateFees computes the amount of fees that can be collected from a position. +// It uses the difference between current and last fee growth to determine uncollected fees. +// +// Parameters: +// - position: Current position state +// - currentFeeGrowth: Current fee growth values from the pool +// +// Returns: +// - *u256.Uint: Token0 fees to collect +// - *u256.Uint: Token1 fees to collect +// +// The calculation accounts for the position's liquidity and the fee growth +// inside the position's tick range. +func calculateFees(position Position, currentFeeGrowth FeeGrowthInside) (*u256.Uint, *u256.Uint) { + fee0 := calculateTokensOwed( + currentFeeGrowth.feeGrowthInside0LastX128, + position.feeGrowthInside0LastX128, + position.liquidity, + ) + + fee1 := calculateTokensOwed( + currentFeeGrowth.feeGrowthInside1LastX128, + position.feeGrowthInside1LastX128, + position.liquidity, + ) + + tokensOwed0 := new(u256.Uint).Add(position.tokensOwed0.Clone(), fee0) + tokensOwed1 := new(u256.Uint).Add(position.tokensOwed1.Clone(), fee1) + + return tokensOwed0, tokensOwed1 +} + +///////////////// Reposition /////////////////// +// region: reposition + +// Reposition adjusts a position's price range by moving liquidity to a new range. +// This is the main entry point for repositioning liquidity in the protocol. +// +// Parameters: +// - tokenId: The ID of the position to reposition +// - tickLower, tickUpper: New price range boundaries +// - amount0Desired, amount1Desired: Desired token amounts for the new position +// - amount0Min, amount1Min: Minimum acceptable amounts for slippage protection +// +// Returns: +// - tokenId: Position identifier +// - liquidity: New liquidity amount +// - tickLower, tickUpper: New price range boundaries +// - amount0, amount1: Actual token amounts used +// +// Events emitted: +// - Reposition: When liquidity is successfully repositioned +// +// The function ensures atomic execution of the reposition operation: +// 1. Validates the caller's ownership and position state +// 2. Moves liquidity to the new price range +// 3. Updates position state +// 4. Handles any native token conversions +// +// TODO (@notJoon): need to validate this function. +func Reposition( + tokenId uint64, + tickLower int32, + tickUpper int32, + amount0Desired string, + amount1Desired string, + amount0Min string, + amount1Min string, +) (uint64, string, int32, int32, string, string) { + common.IsHalted() + en.MintAndDistributeGns() + + input := RepositionInput{ + TokenId: tokenId, + TickLower: tickLower, + TickUpper: tickUpper, + Amount0Desired: u256.MustFromDecimal(amount0Desired), + Amount1Desired: u256.MustFromDecimal(amount1Desired), + Amount0Min: u256.MustFromDecimal(amount0Min), + Amount1Min: u256.MustFromDecimal(amount1Min), + } + + op := NewRepositionOperation(input) + + if err := op.Validate(); err != nil { + panic(addDetailToError(errInvalidInput, err.Error())) + } + + result, err := op.Process() + if err != nil { + panic(err) + } + + prevAddr, prevPkgPath := getPrevAsString() + poolSqrtPriceX96 := pl.PoolGetSlot0SqrtPriceX96(result.PoolPath) + + std.Emit( + "Reposition", + "prevAddr", prevAddr, + "prevRealm", prevPkgPath, + "lpTokenId", ufmt.Sprintf("%d", result.TokenId), + "tickLower", ufmt.Sprintf("%d", result.TickLower), + "tickUpper", ufmt.Sprintf("%d", result.TickUpper), + "liquidity", result.Liquidity.ToString(), + "internal_amount0", result.Amount0.ToString(), + "internal_amount1", result.Amount1.ToString(), + "internal_oldTickLower", ufmt.Sprintf("%d", result.OldTickLower), + "internal_oldTickUpper", ufmt.Sprintf("%d", result.OldTickUpper), + "internal_poolPath", result.PoolPath, + "internal_sqrtPriceX96", poolSqrtPriceX96, + ) + + return result.TokenId, result.Liquidity.ToString(), result.TickLower, result.TickUpper, result.Amount0.ToString(), result.Amount1.ToString() +} + +// RepositionInput encapsulates the parameters required for repositioning a liquidity position. +// It contains all necessary information to safely move liquidity to a new price range. +type RepositionInput struct { + // TokenId uniquely identifies the position to be repositioned + TokenId uint64 + + // TickLower and TickUpper define the new price range for the position + TickLower int32 + TickUpper int32 + + // Amount0Desired and Amount1Desired specify the desired amounts of tokens to add + Amount0Desired *u256.Uint + Amount1Desired *u256.Uint + + // Amount0Min and Amount1Min ensure the minimum amounts of tokens are met + Amount0Min *u256.Uint + Amount1Min *u256.Uint +} + +// RepositionResult contains the outcome of a reposition operation. +// It provides comprehensive information about both the old and new position states. +type RepositionResult struct { + // TokenId identifies the position being repositioned + TokenId uint64 + + // Liquidity represents the new liquidity added to the position + Liquidity *u256.Uint + + // TickLower and TickUpper define the new price range for the position + TickLower int32 + TickUpper int32 + + // Amount0 and Amount1 represent the amounts of tokens added to the position + Amount0 *u256.Uint + Amount1 *u256.Uint + + // OldTickLower and OldTickUpper represent the old tick range of the position + OldTickLower int32 + OldTickUpper int32 + + // PoolPath identifies the pool where the reposition occurred + PoolPath string +} + +// RepositionOperation represents the reposition operation. +// It handles the validation and execution of moving liquidity to a new price range. +type RepositionOperation struct { + // input contains the parameters for the reposition operation + input RepositionInput + + // position holds the current state of the position being modified + position Position +} + +// NewRepositionOperation creates a new instance of RepositionOperation. +// It initializes the operation with the provided input parameters. +// +// Parameters: +// - input: The reposition parameters +// +// Returns: +// - *RepositionOperation: A new operation instance +func NewRepositionOperation(input RepositionInput) *RepositionOperation { + return &RepositionOperation{ input: input } +} + +// Validate performs validation checks on the reposition operation. +// It ensures the operation can be executed safely and the caller has proper permissions. +// +// Checks performed: +// - Caller owns the position token +// - Position exists and is not burned +// - Position has cleared any existing fees +// +// Returns: +// - error: nil if validation passes, error otherwise +func (op *RepositionOperation) Validate() error { + tokenId := op.input.TokenId + assertTokenOwnership(tokenId) + + position, exists := positions[tokenId] + if !exists { + return ufmt.Errorf("position(tokenId: %d) not found", tokenId) + } + op.position = position + + checkPositionHasClear(op.position) + + return nil +} + +// Process executes the reposition operation after validation. +// It handles the core logic of moving liquidity to a new price range. +// +// Steps performed: +// 1. Handles native token wrapping if needed +// 2. Adds liquidity to the new position range +// 3. Updates fee growth tracking +// 4. Updates position state with new price range and liquidity +// 5. Clears accumulated fees +// +// Returns: +// - *RepositionResult: The operation results +// - error: Any errors that occurred during processing +func (op *RepositionOperation) Process() (*RepositionResult, error) { + poolKey := op.position.poolKey + token0, token1, _ := splitOf(poolKey) + + // Check if GNOT pool + // TODO (@notJoon): extract this to a helper function + token0IsNative := token0 == consts.WRAPPED_WUGNOT + token1IsNative := token1 == consts.WRAPPED_WUGNOT + + // Handle native token if needed + if token0IsNative || token1IsNative { + sent := std.GetOrigSend() + ugnotSent := uint64(sent.AmountOf("ugnot")) + + assertWrapNativeToken(ugnotSent, std.PrevRealm().Addr()) + } + + liqParams := AddLiquidityParams{ + poolKey: op.position.poolKey, + tickLower: op.input.TickLower, + tickUpper: op.input.TickUpper, + amount0Desired: op.input.Amount0Desired, + amount1Desired: op.input.Amount1Desired, + amount0Min: op.input.Amount0Min, + amount1Min: op.input.Amount1Min, + caller: std.PrevRealm().Addr(), + } + + liquidity, amount0, amount1, err := addLiquidity(liqParams) + if err != nil { + return nil, ufmt.Errorf("%v: add liquidity failed", errSlippage) + } + + pool := pl.GetPoolFromPoolPath(poolKey) + positionKey := positionKeyCompute(GetOrigPkgAddr(), op.input.TickLower, op.input.TickUpper) + + feeGrowthInside0LastX128 := pool.PositionFeeGrowthInside0LastX128(positionKey) + feeGrowthInside1LastX128 := pool.PositionFeeGrowthInside1LastX128(positionKey) + + /* Update State */ + + // Store old ticks for event emission + oldTickLower := op.position.tickLower + oldTickUpper := op.position.tickUpper + + // Update position + op.position.tickLower = op.input.TickLower + op.position.tickUpper = op.input.TickUpper + op.position.liquidity = liquidity + op.position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128 + op.position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128 + op.position.tokensOwed0 = u256.Zero() + op.position.tokensOwed1 = u256.Zero() + op.position.burned = false + + positions[op.input.TokenId] = op.position + + return &RepositionResult{ + TokenId: op.input.TokenId, + Liquidity: liquidity, + TickLower: op.input.TickLower, + TickUpper: op.input.TickUpper, + Amount0: amount0, + Amount1: amount1, + OldTickLower: oldTickLower, + OldTickUpper: oldTickUpper, + PoolPath: op.position.poolKey, + }, nil +} + +///////////////// HELPER /////////////////// +// region: helper + +func assertCallerPermission(prev std.Realm) { + isUserCalled := prev.PkgPath() == "" + isStakerCalled := prev.Addr() == consts.STAKER_ADDR + + if !common.GetLimitCaller() { + return + } + + if !(isUserCalled || isStakerCalled) { + panic(addDetailToError( + errNoPermission, + ufmt.Sprintf("only user or staker can call, but called from %s", prev.Addr().String()), + )) + } +} + +func processMintInput(input MintInput) (ProcessedMintInput, error) { + var result ProcessedMintInput + + // process tokens + token0, token1, token0IsNative, token1IsNative := processTokens(input.token0, input.token1) + pair := TokenPair{ + token0: token0, + token1: token1, + token0IsNative: token0IsNative, + token1IsNative: token1IsNative, + } + + // parse amounts + amount0Desired, amount1Desired, amount0Min, amount1Min := parseAmounts(input.amount0Desired, input.amount1Desired, input.amount0Min, input.amount1Min) + + tickLower, tickUpper := input.tickLower, input.tickUpper + + // swap if token1 < token0 + if token1 < token0 { + pair.token0, pair.token1 = pair.token1, pair.token0 + amount0Desired, amount1Desired = amount1Desired, amount0Desired + amount0Min, amount1Min = amount1Min, amount0Min + tickLower, tickUpper = -tickUpper, -tickLower + pair.token0IsNative, pair.token1IsNative = pair.token1IsNative, pair.token0IsNative + } + + poolPath := renderPoolPath(pair.token0, pair.token1, input.fee) + + result = ProcessedMintInput{ + tokenPair: pair, + amount0Desired: amount0Desired.Clone(), + amount1Desired: amount1Desired.Clone(), + amount0Min: amount0Min.Clone(), + amount1Min: amount1Min.Clone(), + tickLower: tickLower, + tickUpper: tickUpper, + poolPath: poolPath, + } + + return result, nil +} + +func processTokens(token0, token1 string) (string, string, bool, bool) { + token0IsNative := false + token1IsNative := false + if token0 == consts.GNOT { + token0 = consts.WRAPPED_WUGNOT + token0IsNative = true + } else if token1 == consts.GNOT { + token1 = consts.WRAPPED_WUGNOT + token1IsNative = true + } + return token0, token1, token0IsNative, token1IsNative +} + +func parseAmounts(amount0Desired, amount1Desired, amount0Min, amount1Min string) (*u256.Uint, *u256.Uint, *u256.Uint, *u256.Uint) { + return u256.MustFromDecimal(amount0Desired), u256.MustFromDecimal(amount1Desired), u256.MustFromDecimal(amount0Min), u256.MustFromDecimal(amount1Min) +} + +func renderPoolPath(token0, token1 string, fee uint32) string { + return ufmt.Sprintf("%s:%s:%d", token0, token1, fee) +} + +func hasNativeToken(token0IsNative, token1IsNative bool) bool { + return token0IsNative || token1IsNative +} + +func handleNativeToken(token0IsNative, token1IsNative bool, caller std.Address) error { + if !hasNativeToken(token0IsNative, token1IsNative) { + return nil + } + + oldUserWugnotBalance := wugnot.BalanceOf(a2u(caller)) + sent := std.GetOrigSend() + ugnotSent := uint64(sent.AmountOf("ugnot")) + + if ugnotSent <= 0 { + return nil + } + + if err := wrap(ugnotSent, caller); err != nil { + return err + } + + newUserWugnotBalance := wugnot.BalanceOf(a2u(caller)) + diff := newUserWugnotBalance - oldUserWugnotBalance + + if diff != ugnotSent { + return ufmt.Errorf("amount of ugnot (%d) is not equal to amount of wugnot. (diff: %d)", ugnotSent, diff) + } + + return nil +} + +func positionKeyCompute( + owner std.Address, + tickLower int32, + tickUpper int32, +) string { + key := ufmt.Sprintf("%s__%d__%d", owner.String(), tickLower, tickUpper) + + encoded := base64.StdEncoding.EncodeToString([]byte(key)) + return encoded +} + +func handleLeftoverNativeToken(token0IsNative, token1IsNative bool, userWugnotBalance uint64, caller std.Address) { + if !hasNativeToken(token0IsNative, token1IsNative) { + return + } + + userWugnotAfterMint := wugnot.BalanceOf(a2u(caller)) + + leftOver := userWugnotAfterMint - userWugnotBalance + if leftOver > 0 { + unwrap(leftOver, caller) + } +} + + +func hasWugnotToken(isToken0Wugnot, isToken1Wugnot bool) bool { + return isToken0Wugnot || isToken1Wugnot +} + +func SetPositionOperator(tokenId uint64, operator std.Address) { + caller := std.PrevRealm().PkgPath() + if caller != consts.STAKER_PATH { + panic(addDetailToError( + errNoPermission, + ufmt.Sprintf("caller(%s) is not staker", caller), + )) + } + + position, exist := positions[tokenId] + if !exist { + panic(addDetailToError( + errDataNotFound, + ufmt.Sprintf("position(%d) doesn't exist", tokenId), + )) + } + + position.operator = operator + positions[tokenId] = position +} \ No newline at end of file diff --git a/position/rewrite/utils.txt b/position/rewrite/utils.txt new file mode 100644 index 000000000..a0c76236d --- /dev/null +++ b/position/rewrite/utils.txt @@ -0,0 +1,314 @@ +package position + +import ( + "std" + "strconv" + "time" + + "gno.land/p/demo/grc/grc721" + "gno.land/p/demo/ufmt" + pusers "gno.land/p/demo/users" + + "gno.land/r/gnoswap/v1/common" + "gno.land/r/gnoswap/v1/consts" + "gno.land/r/gnoswap/v1/gnft" + + u256 "gno.land/p/gnoswap/uint256" +) + +// a2u converts std.Address to pusers.AddressOrName. +// pusers is a package that contains the user-related functions. +// +// Input: +// - addr: the address to convert +// +// Output: +// - pusers.AddressOrName: the converted address +func a2u(addr std.Address) pusers.AddressOrName { + return pusers.AddressOrName(addr) +} + +// derivePkgAddr derives the Realm address from it's pkgpath parameter +func derivePkgAddr(pkgPath string) std.Address { + return std.DerivePkgAddr(pkgPath) +} + +// getOrigPkgAddr returns the original package address. +// In position contract, original package address is the position address. +func getOrigPkgAddr() std.Address { + return consts.POSITION_ADDR +} + +// getPrevRealm returns object of the previous realm. +func getPrevRealm() std.Realm { + return std.PrevRealm() +} + +// getPrevAddr returns the address of the previous realm. +func getPrevAddr() std.Address { + return std.PrevRealm().Addr() +} + +// getPrev returns the address and package path of the previous realm. +func getPrevAsString() (string, string) { + prev := std.PrevRealm() + return prev.Addr().String(), prev.PkgPath() +} + +// isUserCall returns true if the caller is a user. +func isUserCall() bool { + return std.PrevRealm().IsUser() +} + +// checkDeadline checks if the deadline is expired. +// If the deadline is expired, it panics. +// The deadline is expired if the current time is greater than the deadline. +// Input: +// - deadline: the deadline to check +func checkDeadline(deadline int64) { + now := time.Now().Unix() + if now > deadline { + panic(newErrorWithDetail( + errExpired, + ufmt.Sprintf("transaction too old, now(%d) > deadline(%d)", now, deadline), + )) + } +} + +func checkPositionHasClear(position Position) { + if !position.isClear() { + panic(newErrorWithDetail( + errNotClear, + ufmt.Sprintf( + "position(%d) isn't clear(liquidity:%s, tokensOwed0:%s, tokensOwed1:%s)", + position.poolKey, position.liquidity.ToString(), position.tokensOwed0.ToString(), position.tokensOwed1.ToString(), + ), + )) + } +} + +func assertTokenOwnership(tokenId uint64) { + assertTokenExists(tokenId) + + // MUST BE OWNER TO INCREASE LIQUIDITY + // can not be approved address ≈ staked position can't be modified + owner := gnft.OwnerOf(tokenIdFrom(tokenId)) + caller := std.PrevRealm().Addr() + assertCallerIsOwner(tokenId, owner, caller) +} + +func assertTokenExists(tokenId uint64) { + if !exists(tokenId) { + panic(newErrorWithDetail( + errDataNotFound, + ufmt.Sprintf("tokenId(%d) doesn't exist", tokenId), + )) + } +} + +func assertCallerIsOwner(tokenId uint64, owner, caller std.Address) { + if owner != caller { + panic(newErrorWithDetail( + errNoPermission, + ufmt.Sprintf("only owner(%s) can increase liquidity for tokenId(%d), but called from %s", owner, tokenId, caller), + )) + } +} + +// assertOnlyUserOrStaker panics if the caller is not a user or staker. +func assertOnlyUserOrStaker(caller std.Realm) { + if !caller.IsUser() { + if err := common.StakerOnly(caller.Addr()); err != nil { + panic(newErrorWithDetail( + errNoPermission, + ufmt.Sprintf("from (%s)", caller.Addr()), + )) + } + } +} + +// assertOnlyNotHalted panics if the contract is halted. +func assertOnlyNotHalted() { + common.IsHalted() +} + +// assertOnlyValidAddress panics if the address is invalid. +func assertOnlyValidAddress(addr std.Address) { + if !addr.IsValid() { + panic(newErrorWithDetail( + errInvalidAddress, + ufmt.Sprintf("(%s)", addr), + )) + } +} + +func assertWrapNativeToken(ugnotSent uint64, prevRealm std.Address) { + if err := wrap(ugnotSent, prevRealm); err != nil { + panic(newErrorWithDetail( + errWrapUnwrap, + ufmt.Sprintf("wrap error: %s", err.Error()), + )) + } +} + +// assertOnlyValidAddress panics if the address is invalid or previous address is not +// different from the other address. +func assertOnlyValidAddressWith(prevAddr, otherAddr std.Address) { + assertOnlyValidAddress(prevAddr) + assertOnlyValidAddress(otherAddr) + + if prevAddr != otherAddr { + panic(newErrorWithDetail( + errInvalidAddress, + ufmt.Sprintf("(%s, %s)", prevAddr, otherAddr), + )) + } +} + +func isAuthorizedForToken(tokenId uint64) { + if !(isOwnerOrOperator(std.PrevRealm().Addr(), tokenId)) { + panic(addDetailToError( + errNoPermission, + ufmt.Sprintf("caller(%s) is not approved or owner of tokenId(%d)", std.PrevRealm().Addr(), tokenId), + )) + } +} + +func verifyTokenIdAndOwnership(tokenId uint64) { + assertTokenExists(tokenId) + + owner := gnft.OwnerOf(tokenIdFrom(tokenId)) + caller := std.PrevRealm().Addr() + assertCallerIsOwner(tokenId, owner, caller) +} + +func verifyBurnedAmounts(burnedAmount0, burnedAmount1, amount0Min, amount1Min *u256.Uint) { + if !(burnedAmount0.Gte(amount0Min) && burnedAmount1.Gte(amount1Min)) { + panic(addDetailToError( + errSlippage, + ufmt.Sprintf("burnedAmount0(%s) >= amount0Min(%s) && burnedAmount1(%s) >= amount1Min(%s)", burnedAmount0.ToString(), amount0Min.ToString(), burnedAmount1.ToString(), amount1Min.ToString()), + )) + } +} + +// region: helper + +func GetOrigPkgAddr() std.Address { + return consts.POSITION_ADDR +} + +// tokenIdFrom converts tokenId to grc721.TokenID type +// NOTE: input parameter tokenId can be string, int, uint64, or grc721.TokenID +// if tokenId is nil or not supported, it will panic +// if tokenId is not found, it will panic +// input: tokenId interface{} +// output: grc721.TokenID +func tokenIdFrom(tokenId interface{}) grc721.TokenID { + if tokenId == nil { + panic(newErrorWithDetail(errInvalidInput, "tokenId is nil")) + } + + switch tokenId.(type) { + case string: + return grc721.TokenID(tokenId.(string)) + case int: + return grc721.TokenID(strconv.Itoa(tokenId.(int))) + case uint64: + return grc721.TokenID(strconv.Itoa(int(tokenId.(uint64)))) + case grc721.TokenID: + return tokenId.(grc721.TokenID) + default: + panic(newErrorWithDetail(errInvalidInput, "unsupported tokenId type")) + } +} + +// exists checks whether tokenId exists +// If tokenId doesn't exist, return false, otherwise return true +// input: tokenId uint64 +// output: bool +func exists(tokenId uint64) bool { + return gnft.Exists(tokenIdFrom(tokenId)) +} + +// isOwner checks whether the caller is the owner of the tokenId +// If the caller is the owner of the tokenId, return true, otherwise return false +// input: tokenId uint64, addr std.Address +// output: bool +func isOwner(tokenId uint64, addr std.Address) bool { + owner := gnft.OwnerOf(tokenIdFrom(tokenId)) + if owner == addr { + return true + } + return false +} + +// isOperator checks whether the caller is the approved operator of the tokenId +// If the caller is the approved operator of the tokenId, return true, otherwise return false +// input: tokenId uint64, addr std.Address +// output: bool +func isOperator(tokenId uint64, addr std.Address) bool { + operator, ok := gnft.GetApproved(tokenIdFrom(tokenId)) + if ok && operator == addr { + return true + } + return false +} + +// isStaked checks whether tokenId is staked +// If tokenId is staked, owner of tokenId is staker contract +// If tokenId is staked, return true, otherwise return false +// input: tokenId grc721.TokenID +// output: bool +func isStaked(tokenId grc721.TokenID) bool { + exist := gnft.Exists(tokenId) + if exist { + owner := gnft.OwnerOf(tokenId) + if owner == consts.STAKER_ADDR { + return true + } + } + return false +} + +// isOwnerOrOperator checks whether the caller is the owner or approved operator of the tokenId +// If the caller is the owner or approved operator of the tokenId, return true, otherwise return false +// input: addr std.Address, tokenId uint64 +// output: bool +func isOwnerOrOperator(addr std.Address, tokenId uint64) bool { + assertOnlyValidAddress(addr) + if !exists(tokenId) { + return false + } + if isOwner(tokenId, addr) || isOperator(tokenId, addr) { + return true + } + if isStaked(tokenIdFrom(tokenId)) { + position, exist := positions[tokenId] + if exist && addr == position.operator { + return true + } + } + return false +} + +// splitOf divides poolKey into pToken0, pToken1, and pFee +// If poolKey is invalid, it will panic +// +// input: poolKey string +// output: +// - token0Path string +// - token1Path string +// - fee uint32 +func splitOf(poolKey string) (string, string, uint32) { + res, err := common.Split(poolKey, ":", 3) + if err != nil { + panic(newErrorWithDetail(errInvalidInput, ufmt.Sprintf("invalid poolKey(%s)", poolKey))) + } + pToken0, pToken1, pFeeStr := res[0], res[1], res[2] + + pFee, err := strconv.Atoi(pFeeStr) + if err != nil { + panic(newErrorWithDetail(errInvalidInput, ufmt.Sprintf("invalid fee(%s)", pFeeStr))) + } + return pToken0, pToken1, uint32(pFee) +} diff --git a/position/tests/__TEST_0_INIT_VARS_HELPERS_test.gnoA b/position/tests/__TEST_0_INIT_VARS_HELPERS_test.gnoA index f7972bb9a..a3732dab5 100644 --- a/position/tests/__TEST_0_INIT_VARS_HELPERS_test.gnoA +++ b/position/tests/__TEST_0_INIT_VARS_HELPERS_test.gnoA @@ -63,7 +63,6 @@ func isOwner(t *testing.T, tokenId uint64, addr std.Address) bool { } func getPoolFromLpTokenId(lpTokenId uint64) *pl.Pool { - position := positions[lpTokenId] - + position := MustGetPosition(lpTokenId) return pl.GetPoolFromPoolPath(position.poolKey) } diff --git a/position/tests/__TEST_position_full_test.gnoA b/position/tests/__TEST_position_full_test.gnoA index 3577b249c..abf32fa4b 100644 --- a/position/tests/__TEST_position_full_test.gnoA +++ b/position/tests/__TEST_position_full_test.gnoA @@ -7,12 +7,11 @@ import ( "gno.land/r/gnoswap/v1/common" "gno.land/r/gnoswap/v1/consts" + "gno.land/r/gnoswap/v1/gns" pl "gno.land/r/gnoswap/v1/pool" "gno.land/r/onbloc/bar" "gno.land/r/onbloc/foo" - - "gno.land/r/gnoswap/v1/gns" ) func init() { diff --git a/position/tests/__TEST_position_increase_burned_position_test.gnoA b/position/tests/__TEST_position_increase_burned_position_test.gnoA index f2fcf71fa..1fcfe4cc7 100644 --- a/position/tests/__TEST_position_increase_burned_position_test.gnoA +++ b/position/tests/__TEST_position_increase_burned_position_test.gnoA @@ -103,7 +103,7 @@ func TestDecreaseLiquidity(t *testing.T) { uassert.Equal(t, fee0, "0") uassert.Equal(t, fee1, "0") - position := positions[uint64(1)] + position := MustGetPosition(uint64(1)) uassert.Equal(t, position.burned, false) // not burned yet } @@ -119,7 +119,7 @@ func TestDecreaseLiquidityToBurnPosition(t *testing.T) { max_timeout, // deadline true, // unwrapResult ) - position := positions[uint64(1)] + position := MustGetPosition(uint64(1)) uassert.Equal(t, position.burned, true) // it is burned uassert.Equal(t, position.liquidity.ToString(), "0") // and liquidity is 0 } @@ -132,7 +132,7 @@ func TestIncreaseLiquidityBurnedPosition(t *testing.T) { pool := getPoolFromLpTokenId(uint64(1)) oldLiquidity := pool.PoolGetLiquidity() - position := positions[uint64(1)] + position := MustGetPosition(uint64(1)) uassert.Equal(t, position.burned, true) // it is burned _, _, m0, m1, _ := IncreaseLiquidity( @@ -150,6 +150,6 @@ func TestIncreaseLiquidityBurnedPosition(t *testing.T) { newLiquidity := pool.PoolGetLiquidity() uassert.Equal(t, newLiquidity.Gt(oldLiquidity), true) - position = positions[uint64(1)] + position = MustGetPosition(uint64(1)) uassert.Equal(t, position.burned, false) } diff --git a/position/tests/__TEST_position_increase_decrease_test.gnoA b/position/tests/__TEST_position_increase_decrease_test.gnoA index cf356b53c..5abf8059a 100644 --- a/position/tests/__TEST_position_increase_decrease_test.gnoA +++ b/position/tests/__TEST_position_increase_decrease_test.gnoA @@ -200,7 +200,7 @@ func TestDecreaseLiquidityAllThenAgainShouldPanic(t *testing.T) { // decreasing position(with 0 liquidity) should panic uassert.PanicsWithMessage( t, - `[GNOSWAP-POOL-010] zero liquidity || position.gno__positionUpdate() || both liquidityDelta and (self)liquidity are zero`, + `[GNOSWAP-POOL-010] zero liquidity || both liquidityDelta and current position's liquidity are zero`, func() { DecreaseLiquidity( uint64(1), // tokenId diff --git a/position/tests/__TEST_position_mint_gnot_grc20_in-range_out-range_test.gnoA b/position/tests/__TEST_position_mint_gnot_grc20_in-range_out-range_test.gnoA index f9bc8af49..e0d17d967 100644 --- a/position/tests/__TEST_position_mint_gnot_grc20_in-range_out-range_test.gnoA +++ b/position/tests/__TEST_position_mint_gnot_grc20_in-range_out-range_test.gnoA @@ -110,6 +110,7 @@ func TestOneSideOnlyUgnot(t *testing.T) { wugnot.Approve(a2u(consts.POSITION_ADDR), consts.UINT64_MAX) // POSITION FOR WRAP std.TestSetOrigSend(std.Coins{{"ugnot", 100_000_000}}, nil) + std.TestIssueCoins(consts.POSITION_ADDR, std.Coins{{"ugnot", 100_000_000}}) uassert.Equal(t, ugnotBalanceOf(fresh01), uint64(0)) std.TestSetRealm(fresh01Realm) @@ -144,6 +145,7 @@ func TestBothWithFresh(t *testing.T) { wugnot.Approve(a2u(consts.POSITION_ADDR), consts.UINT64_MAX) // POSITION FOR WRAP + std.TestIssueCoins(consts.POSITION_ADDR, std.Coins{{"ugnot", 100_000_000}}) std.TestSetOrigSend(std.Coins{{"ugnot", 100_000_000}}, nil) uassert.Equal(t, ugnotBalanceOf(fresh02), uint64(0)) @@ -184,7 +186,7 @@ func TestBothWithFreshButNoSend(t *testing.T) { std.TestSetRealm(fresh02Realm) uassert.PanicsWithMessage( t, - `WUGNOT:insufficient balance`, + `insufficient balance`, func() { Mint(consts.GNS_PATH, consts.GNOT, fee500, 6000, 16000, "70000000", "70000000", "0", "0", max_timeout, fresh02, fresh02) }, diff --git a/position/tests/__TEST_position_native_increase_decrease_test.gnoA b/position/tests/__TEST_position_native_increase_decrease_test.gnoA index b9837dfce..9b676a9fa 100644 --- a/position/tests/__TEST_position_native_increase_decrease_test.gnoA +++ b/position/tests/__TEST_position_native_increase_decrease_test.gnoA @@ -44,6 +44,8 @@ func testMintPosition(t *testing.T) { // prepare 50000005ugnot (5 for refund test) std.TestIssueCoins(admin, std.Coins{{"ugnot", 50000005}}) uassert.Equal(t, ugnotBalanceOf(admin), uint64(50000005)) + + std.TestIssueCoins(consts.POSITION_ADDR, std.Coins{{"ugnot", 200000000}}) uassert.Equal(t, ugnotBalanceOf(consts.POSITION_ADDR), uint64(200000000)) // send & set orig send @@ -76,7 +78,7 @@ func testMintPosition(t *testing.T) { uassert.Equal(t, amount0, "50000000") uassert.Equal(t, amount1, "18394892") - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.poolKey, "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:500") // SPEND ALL WUGNOT diff --git a/position/tests/__TEST_position_native_mint_swap_burn_test.gnoA b/position/tests/__TEST_position_native_mint_swap_burn_test.gnoA index 75eff2491..0d9c7c2fb 100644 --- a/position/tests/__TEST_position_native_mint_swap_burn_test.gnoA +++ b/position/tests/__TEST_position_native_mint_swap_burn_test.gnoA @@ -47,6 +47,8 @@ func testMintPosition(t *testing.T) { std.TestIssueCoins(admin, std.Coins{{"ugnot", 50000005}}) uassert.Equal(t, ugnotBalanceOf(admin), uint64(50000005)) + + std.TestIssueCoins(consts.POSITION_ADDR, std.Coins{{"ugnot", 200000000}}) uassert.Equal(t, ugnotBalanceOf(consts.POSITION_ADDR), uint64(200000000)) banker := std.GetBanker(std.BankerTypeRealmIssue) @@ -78,7 +80,7 @@ func testMintPosition(t *testing.T) { uassert.Equal(t, amount0, "49984837") uassert.Equal(t, amount1, "50000000") - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:500") @@ -133,7 +135,7 @@ func testSwap(t *testing.T) { uassert.Equal(t, amount0, "1234567") uassert.Equal(t, amount1, "-1224110") - position := positions[uint64(1)] + position := MustGetPosition(uint64(1)) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:500") diff --git a/position/tests/__TEST_position_reposition_gnot_pair_test.gnoA b/position/tests/__TEST_position_reposition_gnot_pair_test.gnoA index 0b72ad8d4..a0dc361e2 100644 --- a/position/tests/__TEST_position_reposition_gnot_pair_test.gnoA +++ b/position/tests/__TEST_position_reposition_gnot_pair_test.gnoA @@ -47,6 +47,8 @@ func testMintPosition01InRange(t *testing.T) { std.TestIssueCoins(admin, std.Coins{{"ugnot", 50000005}}) uassert.Equal(t, ugnotBalanceOf(admin), uint64(50000005)) + + std.TestIssueCoins(consts.POSITION_ADDR, std.Coins{{"ugnot", 200000000}}) uassert.Equal(t, ugnotBalanceOf(consts.POSITION_ADDR), uint64(200000000)) banker := std.GetBanker(std.BankerTypeRealmIssue) @@ -78,7 +80,7 @@ func testMintPosition01InRange(t *testing.T) { uassert.Equal(t, amount0, "18394892") uassert.Equal(t, amount1, "50000000") - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:500") @@ -118,7 +120,7 @@ func testSwap(t *testing.T) { uassert.Equal(t, amount0, "-452903") uassert.Equal(t, amount1, "1234567") - position := positions[uint64(1)] + position := MustGetPosition(uint64(1)) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:500") @@ -147,7 +149,7 @@ func testCollectFee01(t *testing.T) { std.TestSetRealm(adminRealm) tokenId, tokensOwed0, tokensOwed1, poolPath, fee0, fee1 := CollectFee(1, false) - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.tokensOwed0.ToString(), "0") uassert.Equal(t, position.tokensOwed1.ToString(), "0") }) @@ -175,7 +177,7 @@ func testDecreaseLiquidityInPosition(t *testing.T) { ownerOfPosition = gnft.OwnerOf(tid(lpTokenId)) uassert.Equal(t, ownerOfPosition, admin) - position := positions[lpTokenId] + position := MustGetPosition(lpTokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:500") @@ -201,7 +203,7 @@ func testReposition(t *testing.T) { lpTokenId := uint64(1) // check current state - position := positions[lpTokenId] + position := MustGetPosition(lpTokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:500") @@ -237,7 +239,7 @@ func testReposition(t *testing.T) { // user ugnot uassert.Equal(t, ugnotBalanceOf(admin), uint64(81600118)) - position = positions[lpTokenId] + position = MustGetPosition(lpTokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:500") diff --git a/position/tests/__TEST_position_reposition_grc20_pair_test.gnoA b/position/tests/__TEST_position_reposition_grc20_pair_test.gnoA index 944827793..44eb7723b 100644 --- a/position/tests/__TEST_position_reposition_grc20_pair_test.gnoA +++ b/position/tests/__TEST_position_reposition_grc20_pair_test.gnoA @@ -48,7 +48,7 @@ func TestMintPosition01InRange(t *testing.T) { uassert.Equal(t, amount0, "18394892") uassert.Equal(t, amount1, "50000000") - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -81,7 +81,7 @@ func TestSwap1(t *testing.T) { uassert.Equal(t, amount0, "1234567") uassert.Equal(t, amount1, "-3332779") - position := positions[uint64(1)] + position := MustGetPosition(uint64(1)) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -120,7 +120,7 @@ func TestMintPosition02InRange(t *testing.T) { uassert.Equal(t, amount0, "21030652") uassert.Equal(t, amount1, "50000000") - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -169,7 +169,7 @@ func TestDecreaseLiquidityInPosition(t *testing.T) { ownerOfPosition = gnft.OwnerOf(tid(_lpTokenId)) uassert.Equal(t, ownerOfPosition, admin) - position := positions[_lpTokenId] + position := MustGetPosition(_lpTokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -208,7 +208,7 @@ func TestMintPosition03InRange(t *testing.T) { uassert.Equal(t, amount0, "21030652") uassert.Equal(t, amount1, "50000000") - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -226,7 +226,7 @@ func TestRepositionNotOwner(t *testing.T) { std.TestSetRealm(std.NewUserRealm(test1)) uassert.PanicsWithMessage( t, - `[GNOSWAP-POSITION-001] caller has no permission || position.gno__Reposition() || only owner(g17290cwvmrapvp869xfnhhawa8sm9edpufzat7d) can reposition for tokenId(1), but called from g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5`, + `[GNOSWAP-POSITION-001] caller has no permission || only owner(g17290cwvmrapvp869xfnhhawa8sm9edpufzat7d) can reposition for tokenId(1), but called from g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5`, func() { Reposition(uint64(1), -1000, 1000, "500", "500", "0", "0") }) @@ -236,7 +236,7 @@ func TestRepositionOwnerButNowBurned(t *testing.T) { std.TestSetRealm(adminRealm) uassert.PanicsWithMessage( t, - `[GNOSWAP-POSITION-009] position is not clear || position.gno__Reposition() || position(2) isn't clear(liquidity:341464938, tokensOwed0:0, tokensOwed1:0)`, + `[GNOSWAP-POSITION-009] position is not clear || position(2) isn't clear(liquidity:341464938, tokensOwed0:0, tokensOwed1:0)`, func() { Reposition(uint64(2), -1000, 1000, "500", "500", "0", "0") }) @@ -246,7 +246,7 @@ func TestRepositionSlippageTooLarge(t *testing.T) { std.TestSetRealm(adminRealm) uassert.PanicsWithMessage( t, - `[GNOSWAP-POSITION-002] slippage failed || liquidity_management.gno__addLiquidity() || LM_Price Slippage Check(amount0(0) >= params.amount0Min(100000000000), amount1(500) >= params.amount1Min(100000000000))`, + `[GNOSWAP-POSITION-002] slippage failed || LM_Price Slippage Check(amount0(0) >= params.amount0Min(100000000000), amount1(500) >= params.amount1Min(100000000000))`, func() { Reposition(uint64(1), -1000, 1000, "500", "500", "100000000000", "100000000000") }) @@ -258,7 +258,7 @@ func TestReposition(t *testing.T) { _lpTokenId := uint64(1) // check current state - position := positions[_lpTokenId] + position := MustGetPosition(_lpTokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -281,7 +281,7 @@ func TestReposition(t *testing.T) { "100", // amount1Min ) - position = positions[_lpTokenId] + position = MustGetPosition(_lpTokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") diff --git a/position/tests/__TEST_position_reposition_grc20_pair_with_swap_test.gnoA b/position/tests/__TEST_position_reposition_grc20_pair_with_swap_test.gnoA index 8a366488e..eb14c5f72 100644 --- a/position/tests/__TEST_position_reposition_grc20_pair_with_swap_test.gnoA +++ b/position/tests/__TEST_position_reposition_grc20_pair_with_swap_test.gnoA @@ -48,7 +48,7 @@ func TestMintPosition01InRange(t *testing.T) { uassert.Equal(t, amount0, "18394892") uassert.Equal(t, amount1, "50000000") - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -87,7 +87,7 @@ func TestMintPosition02InRange(t *testing.T) { uassert.Equal(t, amount0, "18394892") uassert.Equal(t, amount1, "50000000") - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -126,7 +126,7 @@ func TestMintPosition03InRange(t *testing.T) { uassert.Equal(t, amount0, "18394892") uassert.Equal(t, amount1, "50000000") - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -159,7 +159,7 @@ func TestSwap1(t *testing.T) { uassert.Equal(t, amount0, "1234567") uassert.Equal(t, amount1, "-3345595") - position := positions[uint64(1)] + position := MustGetPosition(uint64(1)) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -218,7 +218,7 @@ func TestDecreaseLiquidity03(t *testing.T) { ownerOfPosition = gnft.OwnerOf(tid(_lpTokenId)) uassert.Equal(t, ownerOfPosition, admin) - position := positions[_lpTokenId] + position := MustGetPosition(_lpTokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -238,7 +238,7 @@ func TestReposition(t *testing.T) { _lpTokenId := uint64(3) // check current state - position := positions[_lpTokenId] + position := MustGetPosition(_lpTokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -261,7 +261,7 @@ func TestReposition(t *testing.T) { "0", // amount1Min ) - position = positions[_lpTokenId] + position = MustGetPosition(_lpTokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -304,7 +304,7 @@ func TestSwap2(t *testing.T) { uassert.Equal(t, amount0, "1234567") uassert.Equal(t, amount1, "-3330062") - position := positions[uint64(1)] + position := MustGetPosition(uint64(1)) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -345,7 +345,7 @@ func TestUnclaimedFee03_AfterSwap2(t *testing.T) { foo.Approve(a2u(consts.POOL_ADDR), consts.UINT64_MAX) tokenId, afterFee0, afterFee1, poolPath, fee0, fee1 := CollectFee(3, true) - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.tokensOwed0.ToString(), "0") uassert.Equal(t, position.tokensOwed1.ToString(), "0") uassert.Equal(t, fee0, "204") diff --git a/position/tests/__TEST_position_same_user_same_pool_diff_range_diff_position_swap_fee_test.gnoA b/position/tests/__TEST_position_same_user_same_pool_diff_range_diff_position_swap_fee_test.gnoA index 423119509..ba58c6ee6 100644 --- a/position/tests/__TEST_position_same_user_same_pool_diff_range_diff_position_swap_fee_test.gnoA +++ b/position/tests/__TEST_position_same_user_same_pool_diff_range_diff_position_swap_fee_test.gnoA @@ -47,7 +47,7 @@ func TestMintPosition01InRange(t *testing.T) { uassert.Equal(t, amount0, "18394892") uassert.Equal(t, amount1, "50000000") - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -86,7 +86,7 @@ func TestMintPosition02InRange(t *testing.T) { uassert.Equal(t, amount0, "18394892") uassert.Equal(t, amount1, "50000000") - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -132,7 +132,7 @@ func TestUnclaimedFee01(t *testing.T) { foo.Approve(a2u(consts.POOL_ADDR), consts.UINT64_MAX) tokenId, tokensOwed0, tokensOwed1, poolPath, fee0, fee1 := CollectFee(1, true) - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.tokensOwed0.ToString(), "0") uassert.Equal(t, position.tokensOwed1.ToString(), "0") } @@ -150,7 +150,7 @@ func TestUnclaimedFee02(t *testing.T) { foo.Approve(a2u(consts.POOL_ADDR), consts.UINT64_MAX) tokenId, tokensOwed0, tokensOwed1, poolPath, fee0, fee1 := CollectFee(2, true) - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.tokensOwed0.ToString(), "0") uassert.Equal(t, position.tokensOwed1.ToString(), "0") } diff --git a/position/tests/__TEST_position_same_user_same_pool_same_range_diff_position_swap_fee_test.gnoA b/position/tests/__TEST_position_same_user_same_pool_same_range_diff_position_swap_fee_test.gnoA index 1b2754ea9..27b864fc6 100644 --- a/position/tests/__TEST_position_same_user_same_pool_same_range_diff_position_swap_fee_test.gnoA +++ b/position/tests/__TEST_position_same_user_same_pool_same_range_diff_position_swap_fee_test.gnoA @@ -47,7 +47,7 @@ func TestMintPosition01InRange(t *testing.T) { uassert.Equal(t, amount0, "18394892") uassert.Equal(t, amount1, "50000000") - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -86,7 +86,7 @@ func TestMintPosition02InRange(t *testing.T) { uassert.Equal(t, amount0, "18394892") uassert.Equal(t, amount1, "50000000") - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -132,7 +132,7 @@ func TestUnclaimedFee01(t *testing.T) { foo.Approve(a2u(consts.POOL_ADDR), consts.UINT64_MAX) tokenId, afterFee0, afterFee1, poolPath, fee0, fee1 := CollectFee(1, false) - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.tokensOwed0.ToString(), "0") uassert.Equal(t, position.tokensOwed1.ToString(), "0") uassert.Equal(t, fee0, "308") @@ -152,7 +152,7 @@ func TestUnclaimedFee02(t *testing.T) { foo.Approve(a2u(consts.POOL_ADDR), consts.UINT64_MAX) tokenId, afterFee0, afterFee1, poolPath, fee0, fee1 := CollectFee(2, false) - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.tokensOwed0.ToString(), "0") uassert.Equal(t, position.tokensOwed1.ToString(), "0") uassert.Equal(t, fee0, "308") diff --git a/position/tests/__TEST_position_tokens_owed_left_grc20_pair_more_action_test.gnoA b/position/tests/__TEST_position_tokens_owed_left_grc20_pair_more_action_test.gnoA index 7acd33951..e0edf9179 100644 --- a/position/tests/__TEST_position_tokens_owed_left_grc20_pair_more_action_test.gnoA +++ b/position/tests/__TEST_position_tokens_owed_left_grc20_pair_more_action_test.gnoA @@ -50,7 +50,7 @@ func TestMintPosition01InRange(t *testing.T) { uassert.Equal(t, amount0, "18394892") uassert.Equal(t, amount1, "50000000") - position := positions[uint64(1)] + position := MustGetPosition(uint64(1)) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -85,7 +85,7 @@ func TestSwap1(t *testing.T) { uassert.Equal(t, amount1, "-3332779") // pool.swap really doesn't update the position - position := positions[uint64(1)] + position := MustGetPosition(uint64(1)) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -130,7 +130,7 @@ func TestMintPosition02InRange(t *testing.T) { uassert.Equal(t, amount0, "2767688") uassert.Equal(t, amount1, "50000000") - position := positions[uint64(2)] + position := MustGetPosition(uint64(2)) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -164,7 +164,7 @@ func TestSwap2(t *testing.T) { uassert.Equal(t, amount1, "-3305175") // pool.swap really doesn't update the position - position := positions[uint64(1)] + position := MustGetPosition(uint64(1)) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -199,7 +199,7 @@ func TestSwap3(t *testing.T) { // pool.swap really doesn't update the position { - position := positions[uint64(1)] + position := MustGetPosition(uint64(1)) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -214,7 +214,7 @@ func TestSwap3(t *testing.T) { } { - position := positions[uint64(2)] + position := MustGetPosition(uint64(2)) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") @@ -290,7 +290,7 @@ func TestDecreaseLiquidityPosition02(t *testing.T) { uassert.Equal(t, unclaimedFee0.ToString(), "0") uassert.Equal(t, unclaimedFee1.ToString(), "0") - position := positions[_lpTokenId] + position := MustGetPosition(_lpTokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:500") diff --git a/position/tests/__TEST_position_tokens_owed_left_pair_more_action_exact_test.gnoA b/position/tests/__TEST_position_tokens_owed_left_pair_more_action_exact_test.gnoA index 292dd3552..ad3f7d395 100644 --- a/position/tests/__TEST_position_tokens_owed_left_pair_more_action_exact_test.gnoA +++ b/position/tests/__TEST_position_tokens_owed_left_pair_more_action_exact_test.gnoA @@ -52,7 +52,7 @@ func TestMintPosition01(t *testing.T) { uassert.Equal(t, amount0, "20000000") uassert.Equal(t, amount1, "20000000") - position := positions[uint64(1)] + position := MustGetPosition(uint64(1)) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:3000") @@ -89,7 +89,7 @@ func TestSwap1(t *testing.T) { std.TestSkipHeights(10) // swap really doesn't update the position - position := positions[uint64(1)] + position := MustGetPosition(uint64(1)) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:3000") @@ -129,7 +129,7 @@ func TestMintPosition02(t *testing.T) { uassert.Equal(t, amount0, "8455058") uassert.Equal(t, amount1, "4991781") - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:3000") @@ -169,7 +169,7 @@ func TestMintPosition03(t *testing.T) { uassert.Equal(t, amount0, "40000000") uassert.Equal(t, amount1, "23615594") - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:3000") @@ -232,7 +232,7 @@ func TestMintPosition04(t *testing.T) { uassert.Equal(t, amount0, "5098207") uassert.Equal(t, amount1, "2881761") - position := positions[tokenId] + position := MustGetPosition(tokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:3000") @@ -329,7 +329,7 @@ func TestDecreaseLiquidityPosition02(t *testing.T) { uassert.Equal(t, unclaimedFee0.ToString(), "0") uassert.Equal(t, unclaimedFee1.ToString(), "0") - position := positions[_lpTokenId] + position := MustGetPosition(_lpTokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:3000") @@ -376,7 +376,7 @@ func TestDecreaseLiquidityPosition02All(t *testing.T) { uassert.Equal(t, unclaimedFee0.ToString(), "0") uassert.Equal(t, unclaimedFee1.ToString(), "0") - position := positions[_lpTokenId] + position := MustGetPosition(_lpTokenId) uassert.Equal(t, position.nonce.ToString(), "0") uassert.Equal(t, position.operator, consts.ZERO_ADDRESS) uassert.Equal(t, position.poolKey, "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:3000") diff --git a/position/type.gno b/position/type.gno index 68bc00529..b8dcff995 100644 --- a/position/type.gno +++ b/position/type.gno @@ -6,6 +6,8 @@ import ( u256 "gno.land/p/gnoswap/uint256" ) +// Position represents a liquidity position in a pool. +// Each position tracks the amount of liquidity, fee growth, and tokens owed to the position owner. type Position struct { nonce *u256.Uint // nonce for permits @@ -50,6 +52,24 @@ type MintParams struct { caller std.Address // address to call the function } +// newMintParams creates `MintParams` from processed input data. +func newMintParams(input ProcessedMintInput, mintInput MintInput) MintParams { + return MintParams{ + token0: input.tokenPair.token0, + token1: input.tokenPair.token1, + fee: mintInput.fee, + tickLower: mintInput.tickLower, + tickUpper: mintInput.tickUpper, + amount0Desired: input.amount0Desired, + amount1Desired: input.amount1Desired, + amount0Min: input.amount0Min, + amount1Min: input.amount1Min, + deadline: mintInput.deadline, + mintTo: mintInput.mintTo, + caller: mintInput.caller, + } +} + type AddLiquidityParams struct { poolKey string // poolPath of the pool which has the position tickLower int32 // lower end of the tick range for the position diff --git a/position/utils.gno b/position/utils.gno index 2121fb4fb..94da2987e 100644 --- a/position/utils.gno +++ b/position/utils.gno @@ -2,12 +2,18 @@ package position import ( "std" + "strconv" "time" + "gno.land/p/demo/grc/grc721" "gno.land/p/demo/ufmt" pusers "gno.land/p/demo/users" + "gno.land/r/gnoswap/v1/common" "gno.land/r/gnoswap/v1/consts" + "gno.land/r/gnoswap/v1/gnft" + + u256 "gno.land/p/gnoswap/uint256" ) // a2u converts std.Address to pusers.AddressOrName. @@ -69,6 +75,46 @@ func checkDeadline(deadline int64) { } } +func checkPositionHasClear(position Position) { + if !position.isClear() { + panic(newErrorWithDetail( + errNotClear, + ufmt.Sprintf( + "position(%d) isn't clear(liquidity:%s, tokensOwed0:%s, tokensOwed1:%s)", + position.poolKey, position.liquidity.ToString(), position.tokensOwed0.ToString(), position.tokensOwed1.ToString(), + ), + )) + } +} + +func assertTokenOwnership(tokenId uint64) { + assertTokenExists(tokenId) + + // MUST BE OWNER TO INCREASE LIQUIDITY + // can not be approved address ≈ staked position can't be modified + owner := gnft.OwnerOf(tokenIdFrom(tokenId)) + caller := std.PrevRealm().Addr() + assertCallerIsOwner(tokenId, owner, caller) +} + +func assertTokenExists(tokenId uint64) { + if !exists(tokenId) { + panic(newErrorWithDetail( + errDataNotFound, + ufmt.Sprintf("tokenId(%d) doesn't exist", tokenId), + )) + } +} + +func assertCallerIsOwner(tokenId uint64, owner, caller std.Address) { + if owner != caller { + panic(newErrorWithDetail( + errNoPermission, + ufmt.Sprintf("only owner(%s) can increase liquidity for tokenId(%d), but called from %s", owner, tokenId, caller), + )) + } +} + // assertOnlyUserOrStaker panics if the caller is not a user or staker. func assertOnlyUserOrStaker(caller std.Realm) { if !caller.IsUser() { @@ -96,6 +142,15 @@ func assertOnlyValidAddress(addr std.Address) { } } +func assertWrapNativeToken(ugnotSent uint64, prevRealm std.Address) { + if err := wrap(ugnotSent, prevRealm); err != nil { + panic(newErrorWithDetail( + errWrapUnwrap, + ufmt.Sprintf("wrap error: %s", err.Error()), + )) + } +} + // assertOnlyValidAddress panics if the address is invalid or previous address is not // different from the other address. func assertOnlyValidAddressWith(prevAddr, otherAddr std.Address) { @@ -109,3 +164,142 @@ func assertOnlyValidAddressWith(prevAddr, otherAddr std.Address) { )) } } + +func verifyTokenIdAndOwnership(tokenId uint64) { + assertTokenExists(tokenId) + + owner := gnft.OwnerOf(tokenIdFrom(tokenId)) + caller := std.PrevRealm().Addr() + assertCallerIsOwner(tokenId, owner, caller) +} + +func verifyBurnedAmounts(burnedAmount0, burnedAmount1, amount0Min, amount1Min *u256.Uint) { + if !(burnedAmount0.Gte(amount0Min) && burnedAmount1.Gte(amount1Min)) { + panic(newErrorWithDetail( + errSlippage, + ufmt.Sprintf("burnedAmount0(%s) >= amount0Min(%s) && burnedAmount1(%s) >= amount1Min(%s)", burnedAmount0.ToString(), amount0Min.ToString(), burnedAmount1.ToString(), amount1Min.ToString()), + )) + } +} + +// region: helper + +func GetOrigPkgAddr() std.Address { + return consts.POSITION_ADDR +} + +// tokenIdFrom converts tokenId to grc721.TokenID type +// NOTE: input parameter tokenId can be string, int, uint64, or grc721.TokenID +// if tokenId is nil or not supported, it will panic +// if tokenId is not found, it will panic +// input: tokenId interface{} +// output: grc721.TokenID +func tokenIdFrom(tokenId interface{}) grc721.TokenID { + if tokenId == nil { + panic(newErrorWithDetail(errInvalidInput, "tokenId is nil")) + } + + switch tokenId.(type) { + case string: + return grc721.TokenID(tokenId.(string)) + case int: + return grc721.TokenID(strconv.Itoa(tokenId.(int))) + case uint64: + return grc721.TokenID(strconv.Itoa(int(tokenId.(uint64)))) + case grc721.TokenID: + return tokenId.(grc721.TokenID) + default: + panic(newErrorWithDetail(errInvalidInput, "unsupported tokenId type")) + } +} + +// exists checks whether tokenId exists +// If tokenId doesn't exist, return false, otherwise return true +// input: tokenId uint64 +// output: bool +func exists(tokenId uint64) bool { + return gnft.Exists(tokenIdFrom(tokenId)) +} + +// isOwner checks whether the caller is the owner of the tokenId +// If the caller is the owner of the tokenId, return true, otherwise return false +// input: tokenId uint64, addr std.Address +// output: bool +func isOwner(tokenId uint64, addr std.Address) bool { + owner := gnft.OwnerOf(tokenIdFrom(tokenId)) + if owner == addr { + return true + } + return false +} + +// isOperator checks whether the caller is the approved operator of the tokenId +// If the caller is the approved operator of the tokenId, return true, otherwise return false +// input: tokenId uint64, addr std.Address +// output: bool +func isOperator(tokenId uint64, addr std.Address) bool { + operator, ok := gnft.GetApproved(tokenIdFrom(tokenId)) + if ok && operator == addr { + return true + } + return false +} + +// isStaked checks whether tokenId is staked +// If tokenId is staked, owner of tokenId is staker contract +// If tokenId is staked, return true, otherwise return false +// input: tokenId grc721.TokenID +// output: bool +func isStaked(tokenId grc721.TokenID) bool { + exist := gnft.Exists(tokenId) + if exist { + owner := gnft.OwnerOf(tokenId) + if owner == consts.STAKER_ADDR { + return true + } + } + return false +} + +// isOwnerOrOperator checks whether the caller is the owner or approved operator of the tokenId +// If the caller is the owner or approved operator of the tokenId, return true, otherwise return false +// input: addr std.Address, tokenId uint64 +// output: bool +func isOwnerOrOperator(addr std.Address, tokenId uint64) bool { + assertOnlyValidAddress(addr) + if !exists(tokenId) { + return false + } + if isOwner(tokenId, addr) || isOperator(tokenId, addr) { + return true + } + if isStaked(tokenIdFrom(tokenId)) { + position, exist := GetPosition(tokenId) + if exist && addr == position.operator { + return true + } + } + return false +} + +// splitOf divides poolKey into pToken0, pToken1, and pFee +// If poolKey is invalid, it will panic +// +// input: poolKey string +// output: +// - token0Path string +// - token1Path string +// - fee uint32 +func splitOf(poolKey string) (string, string, uint32) { + res, err := common.Split(poolKey, ":", 3) + if err != nil { + panic(newErrorWithDetail(errInvalidInput, ufmt.Sprintf("invalid poolKey(%s)", poolKey))) + } + pToken0, pToken1, pFeeStr := res[0], res[1], res[2] + + pFee, err := strconv.Atoi(pFeeStr) + if err != nil { + panic(newErrorWithDetail(errInvalidInput, ufmt.Sprintf("invalid fee(%s)", pFeeStr))) + } + return pToken0, pToken1, uint32(pFee) +} diff --git a/position/utils_test.gno b/position/utils_test.gno index 26051a271..75b1a7301 100644 --- a/position/utils_test.gno +++ b/position/utils_test.gno @@ -4,6 +4,7 @@ import ( "std" "testing" + "gno.land/p/demo/grc/grc721" "gno.land/p/demo/uassert" pusers "gno.land/p/demo/users" "gno.land/r/demo/users" @@ -11,10 +12,21 @@ import ( "gno.land/r/gnoswap/v1/consts" ) +func assertPanic(t *testing.T, expectedMsg string, fn func()) { + t.Helper() + defer func() { + r := recover() + if r == nil { + t.Errorf("expected panic but got none") + } else if r != expectedMsg { + t.Errorf("expected panic %v, got %v", expectedMsg, r) + } + }() + fn() +} + func TestA2u(t *testing.T) { - var ( - addr = std.Address("g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8c") - ) + addr := std.Address("g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8c") tests := []struct { name string @@ -37,9 +49,7 @@ func TestA2u(t *testing.T) { } func TestDerivePkgAddr(t *testing.T) { - var ( - pkgPath = "gno.land/r/gnoswap/v1/position" - ) + pkgPath := "gno.land/r/gnoswap/v1/position" tests := []struct { name string input string @@ -192,6 +202,12 @@ func TestCheckDeadline(t *testing.T) { now: 1234567890, expected: "[GNOSWAP-POSITION-007] transaction expired || transaction too old, now(1234567890) > deadline(1234567790)", }, + { + name: "Success - deadline equals now", + deadline: 1234567890, + now: 1234567890, + expected: "", + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -288,7 +304,7 @@ func TestAssertOnlyValidAddress(t *testing.T) { name: "Failure - invalid address", addr: "g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8", // invalid length expected: false, - errorMsg: "[GNOSWAP-POSITION-011] invalid address || (g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8)", + errorMsg: "[GNOSWAP-POSITION-012] invalid address || (g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8)", }, } for _, tc := range tests { @@ -325,7 +341,7 @@ func TestAssertOnlyValidAddressWith(t *testing.T) { addr: "g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8", other: "g17290cwvmrapvp869xfnhhawa8sm9edpufzat7d", expected: false, - errorMsg: "[GNOSWAP-POSITION-011] invalid address || (g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8)", + errorMsg: "[GNOSWAP-POSITION-012] invalid address || (g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8)", }, } for _, tc := range tests { @@ -342,3 +358,444 @@ func TestAssertOnlyValidAddressWith(t *testing.T) { }) } } + +// region: gno_helper + +func TestGetNextId(t *testing.T) { + tests := []struct { + name string + newMint bool + expected uint64 + }{ + { + name: "Success - initial nextId", + newMint: false, + expected: 1, + }, + { + name: "Success - after mint", + newMint: true, + expected: 2, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.newMint { + MakeMintPositionWithoutFee(t) + } + got := getNextId() + uassert.Equal(t, tc.expected, got) + }) + } +} + +func TestTokenIdFrom(t *testing.T) { + tests := []struct { + name string + input interface{} + expected string + shouldPanic bool + }{ + { + name: "Panic - nil", + input: nil, + expected: "[GNOSWAP-POSITION-005] invalid input data || tokenId is nil", + shouldPanic: true, + }, + { + name: "Panic - unsupported type", + input: float64(1), + expected: "[GNOSWAP-POSITION-005] invalid input data || unsupported tokenId type", + shouldPanic: true, + }, + { + name: "Success - string", + input: "1", + expected: "1", + shouldPanic: false, + }, + { + name: "Success - int", + input: int(1), + expected: "1", + shouldPanic: false, + }, + { + name: "Success - uint64", + input: uint64(1), + expected: "1", + shouldPanic: false, + }, + { + name: "Success - grc721.TokenID", + input: grc721.TokenID("1"), + expected: "1", + shouldPanic: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + defer func() { + r := recover() + if r == nil { + if tc.shouldPanic { + t.Errorf(">>> %s: expected panic but got none", tc.name) + return + } + } else { + switch r.(type) { + case string: + if r.(string) != tc.expected { + t.Errorf(">>> %s: got panic %v, want %v", tc.name, r, tc.expected) + } + case error: + if r.(error).Error() != tc.expected { + t.Errorf(">>> %s: got panic %v, want %v", tc.name, r.(error).Error(), tc.expected) + } + default: + t.Errorf(">>> %s: got panic %v, want %v", tc.name, r, tc.expected) + } + } + }() + + if !tc.shouldPanic { + got := tokenIdFrom(tc.input) + uassert.Equal(t, tc.expected, string(got)) + } else { + tokenIdFrom(tc.input) + } + }) + } +} + +func TestExists(t *testing.T) { + tests := []struct { + name string + tokenId uint64 + expected bool + }{ + { + name: "Fail - not exists", + tokenId: 2, + expected: false, + }, + { + name: "Success - exists", + tokenId: 1, + expected: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := exists(tc.tokenId) + uassert.Equal(t, tc.expected, got) + }) + } +} + +func TestIsOwner(t *testing.T) { + tests := []struct { + name string + tokenId uint64 + addr std.Address + expected bool + }{ + { + name: "Fail - is not owner", + tokenId: 1, + addr: users.Resolve(alice), + expected: false, + }, + { + name: "Success - is owner", + tokenId: 1, + addr: users.Resolve(admin), + expected: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + MakeMintPositionWithoutFee(t) + got := isOwner(tc.tokenId, tc.addr) + uassert.Equal(t, tc.expected, got) + }) + } +} + +func TestIsOperator(t *testing.T) { + MakeMintPositionWithoutFee(t) + tests := []struct { + name string + tokenId uint64 + addr pusers.AddressOrName + expected bool + }{ + { + name: "Fail - is not operator", + tokenId: 1, + addr: alice, + expected: false, + }, + { + name: "Success - is operator", + tokenId: 1, + addr: bob, + expected: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.expected { + LPTokenApprove(t, admin, tc.addr, tc.tokenId) + } + got := isOperator(tc.tokenId, users.Resolve(tc.addr)) + uassert.Equal(t, tc.expected, got) + }) + } +} + +func TestIsStaked(t *testing.T) { + MakeMintPositionWithoutFee(t) + tests := []struct { + name string + owner pusers.AddressOrName + operator pusers.AddressOrName + tokenId uint64 + expected bool + }{ + { + name: "Fail - is not staked", + owner: bob, + operator: alice, + tokenId: 1, + expected: false, + }, + { + name: "Fail - is not exist tokenId", + owner: admin, + operator: bob, + tokenId: 100, + expected: false, + }, + { + name: "Success - is staked", + owner: admin, + operator: admin, + tokenId: 1, + expected: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.expected && tc.owner == tc.operator { + LPTokenStake(t, tc.owner, tc.tokenId) + } + got := isStaked(tokenIdFrom(tc.tokenId)) + uassert.Equal(t, tc.expected, got) + if tc.expected && tc.owner == tc.operator { + LPTokenUnStake(t, tc.owner, tc.tokenId, false) + } + }) + } +} + +func TestIsOwnerOrOperator(t *testing.T) { + MakeMintPositionWithoutFee(t) + tests := []struct { + name string + owner pusers.AddressOrName + operator pusers.AddressOrName + tokenId uint64 + expected bool + }{ + { + name: "Fail - is not owner or operator", + owner: admin, + operator: alice, + tokenId: 1, + expected: false, + }, + { + name: "Success - is operator", + owner: admin, + operator: bob, + tokenId: 1, + expected: true, + }, + { + name: "Success - is owner", + owner: admin, + operator: admin, + tokenId: 1, + expected: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.expected && tc.owner != tc.operator { + LPTokenApprove(t, tc.owner, tc.operator, tc.tokenId) + } + var got bool + if tc.owner == tc.operator { + got = isOwnerOrOperator(users.Resolve(tc.owner), tc.tokenId) + } else { + got = isOwnerOrOperator(users.Resolve(tc.operator), tc.tokenId) + } + uassert.Equal(t, tc.expected, got) + }) + } +} + +func TestIsOwnerOrOperatorWithStake(t *testing.T) { + MakeMintPositionWithoutFee(t) + tests := []struct { + name string + owner pusers.AddressOrName + operator pusers.AddressOrName + tokenId uint64 + isStake bool + expected bool + }{ + { + name: "Fail - is not token staked", + owner: admin, + operator: alice, + tokenId: 1, + isStake: false, + expected: false, + }, + { + name: "Success - is token staked (position operator)", + owner: admin, + operator: admin, + tokenId: 1, + isStake: true, + expected: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.isStake { + LPTokenStake(t, tc.owner, tc.tokenId) + } + got := isOwnerOrOperator(users.Resolve(tc.operator), tc.tokenId) + uassert.Equal(t, tc.expected, got) + }) + } +} + +func TestPoolKeyDivide(t *testing.T) { + tests := []struct { + name string + poolKey string + expectedPath0 string + expectedPath1 string + expectedFee uint32 + expectedError string + shouldPanic bool + }{ + { + name: "Fail - invalid poolKey", + poolKey: "gno.land/r/onbloc", + expectedError: "[GNOSWAP-POSITION-005] invalid input data || invalid poolKey(gno.land/r/onbloc)", + shouldPanic: true, + }, + { + name: "Success - split poolKey", + poolKey: "gno.land/r/gnoswap/v1/gns:gno.land/r/demo/wugnot:500", + expectedPath0: gnsPath, + expectedPath1: wugnotPath, + expectedFee: fee500, + shouldPanic: false, + }, + { + name: "Fail -empty poolKey", + poolKey: "", + expectedError: "[GNOSWAP-POSITION-005] invalid input data || invalid poolKey()", + shouldPanic: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + defer func() { + r := recover() + if r == nil { + if tc.shouldPanic { + t.Errorf(">>> %s: expected panic but got none", tc.name) + return + } + } else { + switch r.(type) { + case string: + if r.(string) != tc.expectedError { + t.Errorf(">>> %s: got panic %v, want %v", tc.name, r, tc.expectedError) + } + case error: + if r.(error).Error() != tc.expectedError { + t.Errorf(">>> %s: got panic %v, want %v", tc.name, r.(error).Error(), tc.expectedError) + } + default: + t.Errorf(">>> %s: got panic %v, want %v", tc.name, r, tc.expectedError) + } + } + }() + + if !tc.shouldPanic { + gotToken0, gotToken1, gotFee := splitOf(tc.poolKey) + uassert.Equal(t, tc.expectedPath0, gotToken0) + uassert.Equal(t, tc.expectedPath1, gotToken1) + uassert.Equal(t, tc.expectedFee, gotFee) + } else { + splitOf(tc.poolKey) + } + }) + } +} + +func TestSplitOf_Improved(t *testing.T) { + tests := []struct { + name string + poolKey string + expectedPath0 string + expectedPath1 string + expectedFee uint32 + expectedError string + shouldPanic bool + }{ + { + name: "Fail - empty poolKey", + poolKey: "", + expectedError: "[GNOSWAP-POSITION-005] invalid input data || invalid poolKey()", + shouldPanic: true, + }, + { + name: "Fail - invalid delimiter", + poolKey: "gno.land/r/gnoswap:v1/gns:gno.land/r/demo/wugnot-500", + expectedError: "[GNOSWAP-POSITION-005] invalid input data || invalid fee(gno.land/r/demo/wugnot-500)", + shouldPanic: true, + }, + { + name: "Success - valid poolKey", + poolKey: "gno.land/r/gnoswap/v1/gns:gno.land/r/demo/wugnot:500", + expectedPath0: "gno.land/r/gnoswap/v1/gns", + expectedPath1: "gno.land/r/demo/wugnot", + expectedFee: 500, + shouldPanic: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.shouldPanic { + assertPanic(t, tc.expectedError, func() { + splitOf(tc.poolKey) + }) + } else { + gotToken0, gotToken1, gotFee := splitOf(tc.poolKey) + uassert.Equal(t, tc.expectedPath0, gotToken0) + uassert.Equal(t, tc.expectedPath1, gotToken1) + uassert.Equal(t, tc.expectedFee, gotFee) + } + }) + } +} diff --git a/position/wrap_unwrap.gno b/position/wrap_unwrap.gno deleted file mode 100644 index e1c97f9d0..000000000 --- a/position/wrap_unwrap.gno +++ /dev/null @@ -1,49 +0,0 @@ -package position - -import ( - "std" - - "gno.land/r/demo/wugnot" - - "gno.land/p/demo/ufmt" - "gno.land/r/gnoswap/v1/consts" -) - -func wrap(ugnotAmount uint64, to std.Address) { - if ugnotAmount == 0 { - return - } - - if ugnotAmount < consts.UGNOT_MIN_DEPOSIT_TO_WRAP { - panic(addDetailToError( - errWugnotMinimum, - ufmt.Sprintf("wrap.gno__wrap() || amount(%d) < minimum(%d)", ugnotAmount, consts.UGNOT_MIN_DEPOSIT_TO_WRAP), - )) - } - - // WRAP IT - wugnotAddr := std.DerivePkgAddr(consts.WRAPPED_WUGNOT) - banker := std.GetBanker(std.BankerTypeRealmSend) - - banker.SendCoins(consts.POSITION_ADDR, wugnotAddr, std.Coins{{"ugnot", int64(ugnotAmount)}}) - wugnot.Deposit() // POSITION HAS WUGNOT - - // SEND WUGNOT: POSITION -> USER - wugnot.Transfer(a2u(to), ugnotAmount) -} - -func unwrap(wugnotAmount uint64, to std.Address) { - if wugnotAmount == 0 { - return - } - - // SEND WUGNOT: USER -> POSITION - wugnot.TransferFrom(a2u(to), a2u(consts.POSITION_ADDR), wugnotAmount) - - // UNWRAP IT - wugnot.Withdraw(wugnotAmount) - - // SEND GNOT: POSITION -> USER - banker := std.GetBanker(std.BankerTypeRealmSend) - banker.SendCoins(consts.POSITION_ADDR, to, std.Coins{{"ugnot", int64(wugnotAmount)}}) -} diff --git a/staker/staker.gno b/staker/staker.gno index fe95cec3e..afdd3067b 100644 --- a/staker/staker.gno +++ b/staker/staker.gno @@ -670,7 +670,7 @@ func getTokenPairBalanceFromPosition(tokenId uint64) (string, string) { poolKey := pn.PositionGetPositionPoolKey(tokenId) pool := pl.GetPoolFromPoolPath(poolKey) - currentX96 := pool.PoolGetSlot0SqrtPriceX96() + currentX96 := pool.Slot0SqrtPriceX96() lowerX96 := common.TickMathGetSqrtRatioAtTick(pn.PositionGetPositionTickLower(tokenId)) upperX96 := common.TickMathGetSqrtRatioAtTick(pn.PositionGetPositionTickUpper(tokenId))