From 5ab79876910de258272745a247b1aa5db611b81b Mon Sep 17 00:00:00 2001 From: n3wbie Date: Thu, 22 Feb 2024 15:16:13 +0900 Subject: [PATCH] GSW-792 feat: do not burn nft even entire liquidity is removed --- position/_RPC_api.gno | 13 +- position/_TEST_INIT_basic_test.gno | 6 +- position/_TEST_position_test.gno | 201 +++++++++++++---------------- position/gno.mod | 21 +-- position/nft_helper.gno | 10 +- position/position.gno | 12 +- position/type.gno | 2 + 7 files changed, 121 insertions(+), 144 deletions(-) diff --git a/position/_RPC_api.gno b/position/_RPC_api.gno index 6ddcf0d9..3f2cd277 100644 --- a/position/_RPC_api.gno +++ b/position/_RPC_api.gno @@ -13,6 +13,7 @@ import ( type RpcPosition struct { LpTokenId uint64 `json:"lpTokenId"` + Burned bool `json:"burned"` Operator string `json:"operator"` PoolKey string `json:"poolKey"` TickLower int32 `json:"tickLower"` @@ -68,6 +69,8 @@ func rpcMakePosition(lpTokenId uint64) RpcPosition { panic(ufmt.Sprintf("[POSITION] getter_api.gno__rpcMakePosition() || position not found for lpTokenId(%d)", lpTokenId)) } + burned := isBurned(lpTokenId) + pool := pl.GetPoolFromPoolPath(position.poolKey) currentX96 := pool.PoolGetSlot0SqrtPriceX96() lowerX96 := common.TickMathGetSqrtRatioAtTick(position.tickLower) @@ -80,10 +83,14 @@ func rpcMakePosition(lpTokenId uint64) RpcPosition { position.liquidity, ) - unclaimedFee0, unclaimedFee1 := unclaimedFee(lpTokenId) + var unclaimedFee0, unclaimedFee1 bigint // if position is burned, unclaimedFee0 and unclaimedFee1 will be 0 + if !burned { + unclaimedFee0, unclaimedFee1 := unclaimedFee(lpTokenId) + } return RpcPosition{ LpTokenId: lpTokenId, + Burned: burned, Operator: position.operator.String(), PoolKey: position.poolKey, TickLower: position.tickLower, @@ -150,3 +157,7 @@ func unclaimedFee(tokenId uint64) (bigint, bigint) { return unclaimedFee0, unclaimedFee1 } + +func isBurned(tokenId uint64) bool { + return positions[tokenId].burned +} diff --git a/position/_TEST_INIT_basic_test.gno b/position/_TEST_INIT_basic_test.gno index 68f1bf34..a27786a0 100644 --- a/position/_TEST_INIT_basic_test.gno +++ b/position/_TEST_INIT_basic_test.gno @@ -12,7 +12,7 @@ import ( "gno.land/r/demo/wugnot" - nft "gno.land/r/demo/gnft" + "gno.land/r/demo/gnft" ) var ( @@ -28,6 +28,8 @@ var ( fee100 = uint16(100) fee500 = uint16(500) fee3000 = uint16(3000) + + max_timeout = bigint(9999999999) ) func init() { @@ -97,7 +99,7 @@ func ugnotBalanceOf(addr std.Address) uint64 { } func isOwner(t *testing.T, tokenId uint64, addr std.Address) bool { - owner := nft.OwnerOf(tid(tokenId)) + owner := gnft.OwnerOf(tid(tokenId)) if owner == addr { return true diff --git a/position/_TEST_position_test.gno b/position/_TEST_position_test.gno index aedd9a2d..d5046e88 100644 --- a/position/_TEST_position_test.gno +++ b/position/_TEST_position_test.gno @@ -1,18 +1,15 @@ package position import ( + "encoding/gjson" "std" "testing" - "encoding/gjson" + "gno.land/p/demo/common" pl "gno.land/r/demo/pool" -) -var ( - test_tickLower = int32(9000) - test_tickUpper = int32(11000) - test_liquidityExpect = bigint(1000) + "gno.land/r/demo/gnft" ) // 1. Init & Create Pool @@ -21,119 +18,76 @@ func TestPoolInitCreatePool(t *testing.T) { pl.InitManual() std.TestSetOrigCaller(test1) - pl.CreatePool(fooPath, barPath, fee500, 130621891405341611593710811006) - - // fee - // 500 = 0.05% // USv3 default - // 3000 = 0.3% // USv3 default - // 10000 = 1% // USv3 default + pl.CreatePool(barPath, fooPath, fee500, common.TickMathGetSqrtRatioAtTick(10000)) // x2.71814592682522526700950038502924144268035888671875 // sqrtPrice - // 130621891405341611593710811006 // tick = 10000 - shouldPanic(t, func() { pl.CreatePool(fooPath, barPath, fee500, 130621891405341611593710811006) }) + shouldPanic(t, func() { pl.CreatePool(fooPath, barPath, fee500, common.TickMathGetSqrtRatioAtTick(10000)) }) } -// 2. Mint LP and Get GNFT -func TestMint(t *testing.T) { - // test1 mints - // will get tid 1 nft - { - std.TestSetOrigCaller(test1) - - tTokenId, tLiquidity, tAmount0, tAmount1 := Mint( - fooPath, - barPath, - fee500, - test_tickLower, - test_tickUpper, - bigint(50000000), - bigint(50000000), - bigint(1), - bigint(1), - bigint(1234567890), - ) - - isOwner(t, tTokenId, test1) - shouldEQ(t, tTokenId, 1) - shouldEQ(t, getNextId(), 2) - } - - // test1 mints - // will get tid 2 nft - { - std.TestSetOrigCaller(test1) - - tTokenId, tLiquidity, tAmount0, tAmount1 := Mint( - fooPath, - barPath, - fee500, - test_tickLower, - test_tickUpper, - bigint(2000), - bigint(2000), - bigint(1), - bigint(1), - bigint(1234567890), - ) - - isOwner(t, tTokenId, test1) - shouldEQ(t, tTokenId, 2) - shouldEQ(t, getNextId(), 3) - } - - // test1 mints out of range => token1 will be 0 - // will get tid 3 nft - { - std.TestSetOrigCaller(test1) - - tTokenId, tLiquidity, tAmount0, tAmount1 := Mint( - fooPath, - barPath, - fee500, - int32(1000), - int32(2000), - bigint(1000), - bigint(1000), - bigint(0), - bigint(0), - bigint(1234567890), - ) - shouldEQ(t, tAmount0, bigint(1000)) - shouldEQ(t, tAmount1, bigint(0)) - - isOwner(t, tTokenId, test1) - shouldEQ(t, tTokenId, 3) - shouldEQ(t, getNextId(), 4) - } - - // test1 mints out of range => token0 will be 0 - // will get tid 4 nft - { - std.TestSetOrigCaller(test1) - - tTokenId, tLiquidity, tAmount0, tAmount1 := Mint( - fooPath, - barPath, - fee500, - int32(-34000), - int32(-32000), - bigint(1000), - bigint(1000), - bigint(0), - bigint(0), - bigint(1234567890), - ) - shouldEQ(t, tAmount0, bigint(1000)) - shouldEQ(t, tAmount1, bigint(0)) - - isOwner(t, tTokenId, test1) - shouldEQ(t, tTokenId, 4) - shouldEQ(t, getNextId(), 5) - } +func TestMintPosition01InRange(t *testing.T) { + std.TestSetOrigCaller(test1) + + tokenId, liquidity, amount0, amount1 := Mint( + barPath, + fooPath, + fee500, + 8000, + 12000, + bigint(50000000), + bigint(50000000), + bigint(0), + bigint(0), + max_timeout, + ) + shouldEQ(t, tokenId, 1) + shouldEQ(t, getNextId(), 2) + shouldEQ(t, amount0, bigint(18394891)) + shouldEQ(t, amount1, bigint(50000000)) } -func TestCollectFee(t *testing.T) { - // collect fee from tid 1 ( no swap, no fee) +func TestMintPosition02LowerRange(t *testing.T) { + std.TestSetOrigCaller(test1) + + tokenId, liquidity, amount0, amount1 := Mint( + barPath, + fooPath, + fee500, + 5000, + 8000, + bigint(50000000), + bigint(50000000), + bigint(0), + bigint(0), + max_timeout, + ) + shouldEQ(t, tokenId, 2) + shouldEQ(t, getNextId(), 3) + shouldEQ(t, amount0, bigint(0)) + shouldEQ(t, amount1, bigint(50000000)) +} + +func TestMintPosition03UpperRange(t *testing.T) { + std.TestSetOrigCaller(test1) + + tokenId, liquidity, amount0, amount1 := Mint( + barPath, + fooPath, + fee500, + 12000, + 14000, + bigint(50000000), + bigint(50000000), + bigint(0), + bigint(0), + max_timeout, + ) + shouldEQ(t, tokenId, 3) + shouldEQ(t, getNextId(), 4) + shouldEQ(t, amount0, bigint(50000000)) + shouldEQ(t, amount1, bigint(0)) +} + +func TestCollectFeeBeforeSwap(t *testing.T) { tokenId, fee0, fee1, poolPath := CollectFee(1) shouldEQ(t, tokenId, uint64(1)) shouldEQ(t, fee0, bigint(0)) @@ -141,8 +95,27 @@ func TestCollectFee(t *testing.T) { shouldEQ(t, poolPath, "gno.land/r/demo/bar:gno.land/r/demo/foo:500") } +func TestBurnUpperPosition(t *testing.T) { + std.TestSetOrigCaller(test1) + + ownerOfPosition := gnft.OwnerOf(tid(3)) + shouldEQ(t, ownerOfPosition, GetOrigCaller()) + + tokenId, liquidity, amount0, amount1, poolPath := Burn(3) + shouldEQ(t, tokenId, uint64(3)) + shouldEQ(t, amount0, bigint(50000000)) + shouldEQ(t, amount1, bigint(0)) + + ownerOfPosition = gnft.OwnerOf(tid(3)) + shouldEQ(t, ownerOfPosition, GetOrigCaller()) +} + func TestApiGetPositions(t *testing.T) { gpss := ApiGetPositions() jsonStr := gjson.Parse(gpss) - shouldEQ(t, len(jsonStr.Get("response").Array()), 4) + jsonArr := jsonStr.Get("response").Array() + shouldEQ(t, len(jsonArr), 3) + + shouldEQ(t, jsonArr[0].Get("burned").Bool(), false) + shouldEQ(t, jsonArr[2].Get("burned").Bool(), true) } diff --git a/position/gno.mod b/position/gno.mod index 76442d99..108623e0 100644 --- a/position/gno.mod +++ b/position/gno.mod @@ -1,20 +1 @@ -module gno.land/r/demo/position - -require ( - gno.land/p/demo/common v0.0.0-latest - gno.land/p/demo/grc/grc721 v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/r/demo/bar v0.0.0-latest - gno.land/r/demo/baz v0.0.0-latest - gno.land/r/demo/consts v0.0.0-latest - gno.land/r/demo/foo v0.0.0-latest - gno.land/r/demo/fred v0.0.0-latest - gno.land/r/demo/gnft v0.0.0-latest - gno.land/r/demo/gns v0.0.0-latest - gno.land/r/demo/obl v0.0.0-latest - gno.land/r/demo/pool v0.0.0-latest - gno.land/r/demo/qux v0.0.0-latest - gno.land/r/demo/thud v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest - gno.land/r/demo/wugnot v0.0.0-latest -) +module gno.land/r/demo/position \ No newline at end of file diff --git a/position/nft_helper.gno b/position/nft_helper.gno index af4eabe7..5406e70b 100644 --- a/position/nft_helper.gno +++ b/position/nft_helper.gno @@ -6,11 +6,11 @@ import ( "gno.land/p/demo/ufmt" "gno.land/r/demo/consts" - nft "gno.land/r/demo/gnft" + "gno.land/r/demo/gnft" ) func nftBalanceOf(addr std.Address) bigint { - return bigint(nft.BalanceOf(a2u(addr))) + return bigint(gnft.BalanceOf(a2u(addr))) } func exists(tokenId uint64) bool { @@ -24,7 +24,7 @@ func exists(tokenId uint64) bool { // exists method in grc721 is private // we don't have much choice but to use ownerOf - owner := nft.OwnerOf(tid(tokenId)) + owner := gnft.OwnerOf(tid(tokenId)) if owner == consts.ZERO_ADDRESS { panic(ufmt.Sprintf("[POSITION] nft_helper.gno__exists() || tokenId(%s) doesn't exist__ZeroAddressOwner", tokenId)) return false @@ -43,7 +43,7 @@ func isApprovedOrOwner(addr std.Address, tokenId uint64) bool { } // check owner first - owner := nft.OwnerOf(tid) + owner := gnft.OwnerOf(tid) if addr == owner { return true } @@ -57,7 +57,7 @@ func isApprovedOrOwner(addr std.Address, tokenId uint64) bool { } // if not owner, check whether approved in actual grc721 contract - operator, ok := nft.GetApproved(tid) + operator, ok := gnft.GetApproved(tid) if ok && addr == operator { return true } diff --git a/position/position.gno b/position/position.gno index 7be2c1ef..4909b263 100644 --- a/position/position.gno +++ b/position/position.gno @@ -180,8 +180,8 @@ func Burn(tokenId uint64) (uint64, bigint, bigint, bigint, string) { // tokenId, positions[tokenId] = position - // MUST BE BURN NFT - burnNFT(tokenId) + // burnNFT(tokenId) // DO NOT BURN NFT + burnPosition(tokenId) // JUST UPDATE FLAG return tokenId, positionLiquidity, collectAmount0, collectAmount1, position.poolKey } @@ -197,6 +197,14 @@ func burnNFT(tokenId uint64) { gnft.Burn(tid(tokenId)) } +func burnPosition(tokenId uint64) { + position := positions[tokenId] + require(position.isClear(), ufmt.Sprintf("[POSITION] position.gno__burnPosition() || position(tokenId:%d) isn't clear(liquidity:%d, tokensOwed0:%d, tokensOwed1:%d)", tokenId, position.liquidity, position.tokensOwed0, position.tokensOwed1)) + + position.burned = true + positions[tokenId] = position +} + func isAuthorizedForToken(tokenId uint64) { require(isApprovedOrOwner(PrevRealmAddr(), tokenId), ufmt.Sprintf("[POSITION] position.gno__isAuthorizedForToken() || caller(%s) is not approved or owner of tokenId(%d)", PrevRealmAddr(), tokenId)) } diff --git a/position/type.gno b/position/type.gno index 90feafc8..f7329a4c 100644 --- a/position/type.gno +++ b/position/type.gno @@ -21,6 +21,8 @@ type Position struct { tokensOwed0 bigint tokensOwed1 bigint + + burned bool } type MintParams struct { // r3v4_xxx: private?