From 57781bd299ef26ab457bc8716a9675c8fa9a5828 Mon Sep 17 00:00:00 2001 From: 0xTopaz Date: Tue, 10 Dec 2024 23:54:42 +0900 Subject: [PATCH] GSW-1839 refactor: integrated helper and test code - integrated helper with nft helper - add test helper code - add test code for helper - change file filename --- ..._receiver.gno.gno => _GET_no_receiver.gno} | 2 +- position/_RPC_api.gno | 12 +- position/_helper_test.gno | 101 +++-- position/errors.gno | 32 +- position/helper.gno | 113 ++++- position/helper_test.gno | 397 ++++++++++++++++++ position/liquidity_management.gno | 2 +- position/nft_helper.gno | 70 --- position/position.gno | 22 +- position/utils.gno | 27 +- 10 files changed, 625 insertions(+), 153 deletions(-) rename position/{_GET_no_receiver.gno.gno => _GET_no_receiver.gno} (97%) create mode 100644 position/helper_test.gno delete mode 100644 position/nft_helper.gno diff --git a/position/_GET_no_receiver.gno.gno b/position/_GET_no_receiver.gno similarity index 97% rename from position/_GET_no_receiver.gno.gno rename to position/_GET_no_receiver.gno index 7a29d05f..ffd26f73 100644 --- a/position/_GET_no_receiver.gno.gno +++ b/position/_GET_no_receiver.gno @@ -68,5 +68,5 @@ func PositionIsInRange(tokenId uint64) bool { } func PositionGetPositionOwner(tokenId uint64) std.Address { - return gnft.OwnerOf(tid(tokenId)) + return gnft.OwnerOf(tokenIdFrom(tokenId)) } diff --git a/position/_RPC_api.gno b/position/_RPC_api.gno index 5abf9a1b..932e8a7c 100644 --- a/position/_RPC_api.gno +++ b/position/_RPC_api.gno @@ -80,7 +80,7 @@ func ApiGetPositions() string { _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(tid(position.LpTokenId)).String()), + "owner": json.StringNode("owner", gnft.OwnerOf(tokenIdFrom(position.LpTokenId)).String()), "operator": json.StringNode("operator", position.Operator), "poolKey": json.StringNode("poolKey", position.PoolKey), "tickLower": json.NumberNode("tickLower", float64(position.TickLower)), @@ -140,7 +140,7 @@ func ApiGetPosition(lpTokenId uint64) string { _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(tid(position.LpTokenId)).String()), + "owner": json.StringNode("owner", gnft.OwnerOf(tokenIdFrom(position.LpTokenId)).String()), "operator": json.StringNode("operator", position.Operator), "poolKey": json.StringNode("poolKey", position.PoolKey), "tickLower": json.NumberNode("tickLower", float64(position.TickLower)), @@ -203,7 +203,7 @@ func ApiGetPositionsByPoolPath(poolPath string) string { _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(tid(position.LpTokenId)).String()), + "owner": json.StringNode("owner", gnft.OwnerOf(tokenIdFrom(position.LpTokenId)).String()), "operator": json.StringNode("operator", position.Operator), "poolKey": json.StringNode("poolKey", position.PoolKey), "tickLower": json.NumberNode("tickLower", float64(position.TickLower)), @@ -238,7 +238,7 @@ func ApiGetPositionsByAddress(address std.Address) string { rpcPositions := []RpcPosition{} for lpTokenId, position := range positions { - if !(position.operator == address || gnft.OwnerOf(tid(lpTokenId)) == address) { + if !(position.operator == address || gnft.OwnerOf(tokenIdFrom(lpTokenId)) == address) { continue } @@ -266,7 +266,7 @@ func ApiGetPositionsByAddress(address std.Address) string { _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(tid(position.LpTokenId)).String()), + "owner": json.StringNode("owner", gnft.OwnerOf(tokenIdFrom(position.LpTokenId)).String()), "operator": json.StringNode("operator", position.Operator), "poolKey": json.StringNode("poolKey", position.PoolKey), "tickLower": json.NumberNode("tickLower", float64(position.TickLower)), @@ -410,7 +410,7 @@ func rpcMakePosition(lpTokenId uint64) RpcPosition { return RpcPosition{ LpTokenId: lpTokenId, Burned: burned, - Owner: gnft.OwnerOf(tid(lpTokenId)).String(), + Owner: gnft.OwnerOf(tokenIdFrom(lpTokenId)).String(), Operator: position.operator.String(), PoolKey: position.poolKey, TickLower: position.tickLower, diff --git a/position/_helper_test.gno b/position/_helper_test.gno index be52f83b..f998f2ab 100644 --- a/position/_helper_test.gno +++ b/position/_helper_test.gno @@ -14,6 +14,7 @@ import ( "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/onbloc/bar" "gno.land/r/onbloc/baz" "gno.land/r/onbloc/foo" @@ -37,6 +38,10 @@ const ( fee3000 uint32 = 3000 maxApprove uint64 = 18446744073709551615 max_timeout int64 = 9999999999 + + TIER_1 uint64 = 1 + TIER_2 uint64 = 2 + TIER_3 uint64 = 3 ) const ( @@ -165,6 +170,7 @@ func init() { 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) adminRealm = std.NewUserRealm(users.Resolve(admin)) @@ -182,10 +188,7 @@ func InitialisePoolTest(t *testing.T) { std.TestSetOrigCaller(users.Resolve(admin)) TokenApprove(t, gnsPath, admin, pool, maxApprove) - poolPath := pl.GetPoolPath(wugnotPath, gnsPath, fee3000) - if !pl.DoesPoolPathExist(poolPath) { - pl.CreatePool(wugnotPath, gnsPath, fee3000, "79228162514264337593543950336") - } + CreatePool(t, wugnotPath, gnsPath, fee3000, "79228162514264337593543950336", users.Resolve(admin)) //2. create position std.TestSetOrigCaller(users.Resolve(alice)) @@ -300,6 +303,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, @@ -332,6 +351,54 @@ func MintPosition(t *testing.T, caller) } +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))) @@ -487,7 +554,7 @@ func burnAllNFT(t *testing.T) { std.TestSetRealm(std.NewCodeRealm(consts.POSITION_PATH)) for i := uint64(1); i <= gnft.TotalSupply(); i++ { - gnft.Burn(tid(i)) + gnft.Burn(tokenIdFrom(i)) } } @@ -495,29 +562,7 @@ func TestBeforeResetObject(t *testing.T) { // 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) - pl.CreatePool(barPath, fooPath, fee500, common.TickMathGetSqrtRatioAtTick(0).ToString()) - - // mint position - bar.Approve(a2u(consts.POOL_ADDR), consts.UINT64_MAX) - foo.Approve(a2u(consts.POOL_ADDR), consts.UINT64_MAX) - - tokenId, liquidity, amount0, amount1 := Mint( - barPath, - fooPath, - fee500, - -887270, - 887270, - "50000", - "50000", - "0", - "0", - max_timeout, - users.Resolve(admin), - users.Resolve(admin), - ) - + tokenId, liquidity, amount0, amount1 := MakeMintPositionWithoutFee(t) uassert.Equal(t, tokenId, uint64(1), "tokenId should be 1") uassert.Equal(t, liquidity, "50000", "liquidity should be 50000") uassert.Equal(t, amount0, "50000", "amount0 should be 50000") diff --git a/position/errors.gno b/position/errors.gno index 9cb698fa..60ef43ce 100644 --- a/position/errors.gno +++ b/position/errors.gno @@ -7,19 +7,31 @@ 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") + 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") ) +// 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") +// +// input: err error, detail string +// output: "err.Error() || detail" +func newErrorWithDetail(err error, detail string) string { + return ufmt.Errorf("%s || %s", err.Error(), detail).Error() +} diff --git a/position/helper.gno b/position/helper.gno index 84871762..532e0fdc 100644 --- a/position/helper.gno +++ b/position/helper.gno @@ -1,21 +1,30 @@ 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 } -func tid(tokenId interface{}) grc721.TokenID { +// 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(addDetailToError( - errDataNotFound, - "helper.gno__tid() || tokenId is nil", - )) + panic(newErrorWithDetail(errInvalidInput, "tokenId is nil")) } switch tokenId.(type) { @@ -28,9 +37,95 @@ func tid(tokenId interface{}) grc721.TokenID { case grc721.TokenID: return tokenId.(grc721.TokenID) default: - panic(addDetailToError( - errInvalidInput, - "helper.gno__tid() || unsupported tokenId type", - )) + 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 new file mode 100644 index 00000000..35be8cb5 --- /dev/null +++ b/position/helper_test.gno @@ -0,0 +1,397 @@ +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 417b6a4a..ff2d29e7 100644 --- a/position/liquidity_management.gno +++ b/position/liquidity_management.gno @@ -28,7 +28,7 @@ func addLiquidity(params AddLiquidityParams) (*u256.Uint, *u256.Uint, *u256.Uint params.amount1Desired, ) - pToken0, pToken1, pFee := poolKeyDivide(params.poolKey) + pToken0, pToken1, pFee := splitOf(params.poolKey) amount0, amount1 := pl.Mint( pToken0, pToken1, diff --git a/position/nft_helper.gno b/position/nft_helper.gno deleted file mode 100644 index ff8d0a76..00000000 --- a/position/nft_helper.gno +++ /dev/null @@ -1,70 +0,0 @@ -package position - -import ( - "std" - - "gno.land/p/demo/ufmt" - "gno.land/r/gnoswap/v1/consts" - - "gno.land/r/gnoswap/v1/gnft" -) - -func exists(tokenId uint64) bool { - // non exist tokenId will panic - // use defer to catch the panic - defer func() { - if err := recover(); err != nil { - panic(addDetailToError( - errDataNotFound, - ufmt.Sprintf("nft_helper.gno__exists() || tokenId(%d) doesn't exist", tokenId), - )) - } - }() - - // exists method in grc721 is private - // we don't have much choice but to use ownerOf - owner := gnft.OwnerOf(tid(tokenId)) - if owner == consts.ZERO_ADDRESS { - panic(addDetailToError( - errDataNotFound, - ufmt.Sprintf("nft_helper.gno__exists() || tokenId(%d) doesn't exist__ZeroAddressOwner", tokenId), - )) - return false - } - - return true -} - -func isApprovedOrOwner(addr std.Address, tokenId uint64) bool { - tid := tid(tokenId) - - // check whether token exists - if !exists(tokenId) { - panic(addDetailToError( - errDataNotFound, - ufmt.Sprintf("nft_helper.gno__isApprovedOrOwner() || tokenId(%d) doesn't exist", tokenId), - )) - } - - // check owner first - owner := gnft.OwnerOf(tid) - if addr == owner { - return true - } - - // if not owner, check whether approved in position contract - position, exist := positions[tokenId] - if exist { - if addr == position.operator { - return true - } - } - - // if not owner, check whether approved in actual grc721 contract - operator, ok := gnft.GetApproved(tid) - if ok && addr == operator { - return true - } - - return false -} diff --git a/position/position.gno b/position/position.gno index 7e5e37a2..d94fc7b1 100644 --- a/position/position.gno +++ b/position/position.gno @@ -185,7 +185,7 @@ func mint(params MintParams) (uint64, *u256.Uint, *u256.Uint, *u256.Uint) { ) tokenId := nextId - gnft.Mint(a2u(params.mintTo), tid(tokenId)) // owner, tokenId + gnft.Mint(a2u(params.mintTo), tokenIdFrom(tokenId)) // owner, tokenId nextId++ positionKey := positionKeyCompute(GetOrigPkgAddr(), params.tickLower, params.tickUpper) @@ -240,7 +240,7 @@ func IncreaseLiquidity( // wrap if target pool has wugnot position := positions[tokenId] - pToken0, pToken1, _ := poolKeyDivide(position.poolKey) + pToken0, pToken1, _ := splitOf(position.poolKey) isToken0Wugnot := pToken0 == consts.WRAPPED_WUGNOT isToken1Wugnot := pToken1 == consts.WRAPPED_WUGNOT @@ -293,7 +293,7 @@ func increaseLiquidity(params IncreaseLiquidityParams) (uint64, *u256.Uint, *u25 // MUST BE OWNER TO INCREASE LIQUIDITY // can not be approved address ≈ staked position can't be modified - owner := gnft.OwnerOf(tid(params.tokenId)) + owner := gnft.OwnerOf(tokenIdFrom(params.tokenId)) caller := std.PrevRealm().Addr() if owner != caller { panic(addDetailToError( @@ -434,7 +434,7 @@ func decreaseLiquidity(params DecreaseLiquidityParams) (uint64, *u256.Uint, *u25 liquidityToRemove := calculateLiquidityToRemove(positionLiquidity, params.liquidityRatio) - pToken0, pToken1, pFee := poolKeyDivide(position.poolKey) + pToken0, pToken1, pFee := splitOf(position.poolKey) pool := pl.GetPoolFromPoolPath(position.poolKey) // BURN HERE @@ -537,7 +537,7 @@ func Reposition( // MUST BE OWNER TO REPOSITION // can not be approved address > staked position can't be modified - owner := gnft.OwnerOf(tid(tokenId)) + owner := gnft.OwnerOf(tokenIdFrom(tokenId)) caller := std.PrevRealm().Addr() if owner != caller { panic(addDetailToError( @@ -558,7 +558,7 @@ func Reposition( )) } - token0, token1, _ := poolKeyDivide(position.poolKey) + token0, token1, _ := splitOf(position.poolKey) // check if gnot pool token0IsNative := false token1IsNative := false @@ -663,7 +663,7 @@ func CollectFee(tokenId uint64, unwrapResult bool) (uint64, string, string, stri )) } - token0, token1, fee := poolKeyDivide(position.poolKey) + token0, token1, fee := splitOf(position.poolKey) pl.Burn( token0, @@ -726,7 +726,7 @@ func CollectFee(tokenId uint64, unwrapResult bool) (uint64, string, string, stri withoutFee0, withoutFee1 := pl.HandleWithdrawalFee(tokenId, token0, amount0, token1, amount1, position.poolKey, std.PrevRealm().Addr()) // UNWRAP - pToken0, pToken1, _ := poolKeyDivide(position.poolKey) + pToken0, pToken1, _ := splitOf(position.poolKey) if (pToken0 == consts.WUGNOT_PATH || pToken1 == consts.WUGNOT_PATH) && unwrapResult { userNewWugnot := wugnot.BalanceOf(a2u(std.PrevRealm().Addr())) unwrapAmount := userNewWugnot - userWugnot @@ -785,7 +785,7 @@ func burnNFT(tokenId uint64) { )) } delete(positions, tokenId) - gnft.Burn(tid(tokenId)) + gnft.Burn(tokenIdFrom(tokenId)) } func burnPosition(tokenId uint64) { @@ -802,7 +802,7 @@ func burnPosition(tokenId uint64) { } func isAuthorizedForToken(tokenId uint64) { - if !(isApprovedOrOwner(std.PrevRealm().Addr(), tokenId)) { + 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), @@ -818,7 +818,7 @@ func verifyTokenIdAndOwnership(tokenId uint64) { )) } - owner := gnft.OwnerOf(tid(tokenId)) + owner := gnft.OwnerOf(tokenIdFrom(tokenId)) caller := std.PrevRealm().Addr() if owner != caller { panic(addDetailToError( diff --git a/position/utils.gno b/position/utils.gno index 81b64477..dedba866 100644 --- a/position/utils.gno +++ b/position/utils.gno @@ -2,12 +2,10 @@ package position import ( "std" - "strconv" "time" "gno.land/p/demo/ufmt" pusers "gno.land/p/demo/users" - "gno.land/r/gnoswap/v1/common" ) func checkDeadline(deadline int64) { @@ -24,21 +22,6 @@ func a2u(addr std.Address) pusers.AddressOrName { return pusers.AddressOrName(addr) } -func poolKeyDivide(poolKey string) (string, string, uint32) { - res, err := common.Split(poolKey, ":", 3) - if err != nil { - panic(addDetailToError( - errInvalidInput, - ufmt.Sprintf("utils.gno__poolKeyDivide() || invalid poolKey(%s)", poolKey), - )) - } - - pToken0, pToken1, pFeeStr := res[0], res[1], res[2] - - pFee, _ := strconv.Atoi(pFeeStr) - return pToken0, pToken1, uint32(pFee) -} - func prevRealm() string { return std.PrevRealm().PkgPath() } @@ -51,3 +34,13 @@ func getPrev() (string, string) { prev := std.PrevRealm() return prev.Addr().String(), prev.PkgPath() } + +// assertOnlyValidAddress panics if the address is invalid. +func assertOnlyValidAddress(addr std.Address) { + if !addr.IsValid() { + panic(newErrorWithDetail( + errInvalidAddress, + ufmt.Sprintf("(%s)", addr), + )) + } +}