diff --git a/router/router.gno b/router/router.gno index 644c6444..02a50672 100644 --- a/router/router.gno +++ b/router/router.gno @@ -19,6 +19,10 @@ import ( sr "gno.land/r/gnoswap/v1/staker" ) +const ( + POOL_SEPARATOR = "*POOL*" +) + // SwapRoute swaps the input token to the output token and returns the result amount // If swapType is EXACT_IN, it returns the amount of output token ≈ amount of user to receive // If swapType is EXACT_OUT, it returns the amount of input token ≈ amount of user to pay @@ -27,27 +31,15 @@ import ( func SwapRoute( inputToken string, outputToken string, - _amountSpecified string, // int256 + _amountSpecified string, swapType string, - strRouteArr string, // []string - quoteArr string, // []int - _tokenAmountLimit string, // uint256 -) (string, string) { // tokneIn, tokenOut + strRouteArr string, + quoteArr string, + _tokenAmountLimit string, +) (string, string) { common.IsHalted() - - if swapType != "EXACT_IN" && swapType != "EXACT_OUT" { - panic(addDetailToError( - errInvalidSwapType, - ufmt.Sprintf("router.gno__SwapRoute() || unknown swapType(%s)", swapType), - )) - } - - if common.GetLimitCaller() && std.PrevRealm().PkgPath() != "" { - panic(addDetailToError( - errNoPermission, - "router.gno__SwapRoute() || only user can call this function", - )) - } + assertNotASwapType(swapType) + assertDirectCallOnly() en.MintAndDistributeGns() if consts.EMISSION_REFACTORED { @@ -64,7 +56,7 @@ func SwapRoute( validateInput(amountSpecified, swapType, routes, quotes) - if swapType == "EXACT_OUT" { + if swapType == ExactOut { amountSpecified = i256.Zero().Neg(amountSpecified) } @@ -73,7 +65,7 @@ func SwapRoute( if inputToken == consts.GNOT || outputToken == consts.GNOT { userBeforeWugnotBalance = wugnot.BalanceOf(a2u(std.PrevRealm().Addr())) - if swapType == "EXACT_IN" && inputToken == consts.GNOT { + if swapType == ExactIn && inputToken == consts.GNOT { sent := std.GetOrigSend() ugnotSentByUser := uint64(sent.AmountOf("ugnot")) i256AmountSpecified := i256.MustFromDecimal(_amountSpecified) @@ -82,7 +74,7 @@ func SwapRoute( if ugnotSentByUser != u64AmountSpecified { panic(addDetailToError( errInvalidInput, - ufmt.Sprintf("router.gno__SwapRoute() || ugnot sent by user(%d) is not equal to amountSpecified(%d)", ugnotSentByUser, u64AmountSpecified), + ufmt.Sprintf("ugnot sent by user(%d) is not equal to amountSpecified(%d)", ugnotSentByUser, u64AmountSpecified), )) } @@ -131,21 +123,21 @@ func validateInput(amountSpecified *i256.Int, swapType string, routes, quotes [] if amountSpecified.IsZero() || amountSpecified.IsNeg() { panic(addDetailToError( errInvalidInput, - ufmt.Sprintf("router.gno__validateInput() || invalid amountSpecified(%s), must be positive", amountSpecified.ToString()), + ufmt.Sprintf("invalid amountSpecified(%s), must be positive", amountSpecified.ToString()), )) } if len(routes) < 1 || len(routes) > 7 { panic(addDetailToError( errInvalidInput, - ufmt.Sprintf("router.gno__validateInput() || route length(%d) must be 1~7", len(routes)), + ufmt.Sprintf("route length(%d) must be 1~7", len(routes)), )) } if len(routes) != len(quotes) { panic(addDetailToError( errInvalidInput, - ufmt.Sprintf("router.gno__validateInput() || mismatch between routes(%d) and quotes(%d) length", len(routes), len(quotes)), + ufmt.Sprintf("mismatch between routes(%d) and quotes(%d) length", len(routes), len(quotes)), )) } @@ -158,7 +150,7 @@ func validateInput(amountSpecified *i256.Int, swapType string, routes, quotes [] if quotesSum != 100 { panic(addDetailToError( errInvalidInput, - ufmt.Sprintf("router.gno__validateInput() || quote sum(%d) must be 100", quotesSum), + ufmt.Sprintf("quote sum(%d) must be 100", quotesSum), )) } } @@ -168,15 +160,10 @@ func processRoutes(routes, quotes []string, amountSpecified *i256.Int, swapType resultAmountOut := u256.Zero() for i, route := range routes { - numHops := strings.Count(route, "*POOL*") + 1 + numHops := strings.Count(route, POOL_SEPARATOR) + 1 quote, _ := strconv.Atoi(quotes[i]) - if numHops < 1 || numHops > 3 { - panic(addDetailToError( - errInvalidInput, - ufmt.Sprintf("router.gno__processRoutes() || number of hops(%d) must be 1~3", numHops), - )) - } + assertHopsInRange(numHops) toSwap := i256.Zero().Mul(amountSpecified, i256.NewInt(int64(quote))) toSwap = toSwap.Div(toSwap, i256.NewInt(100)) @@ -211,10 +198,10 @@ func handleSingleSwap(route string, amountSpecified *i256.Int, isDry bool) (*u25 } func finalizeSwap(inputToken, outputToken string, resultAmountIn, resultAmountOut *u256.Uint, swapType string, tokenAmountLimit *u256.Uint, userBeforeWugnotBalance, userWrappedWugnot uint64, amountSpecified *u256.Uint) (string, string) { - if swapType == "EXACT_OUT" && resultAmountOut.Lt(amountSpecified) { + if swapType == ExactOut && resultAmountOut.Lt(amountSpecified) { panic(addDetailToError( errSlippage, - ufmt.Sprintf("router.gno__finalizeSwap() || too few received for user (expected minimum: %s, actual: %s, swapType: %s)", amountSpecified.ToString(), resultAmountOut.ToString(), swapType), + ufmt.Sprintf("too few received for user (expected minimum: %s, actual: %s, swapType: %s)", amountSpecified.ToString(), resultAmountOut.ToString(), swapType), )) } @@ -229,7 +216,7 @@ func finalizeSwap(inputToken, outputToken string, resultAmountIn, resultAmountOu // used existing wugnot panic(addDetailToError( errSlippage, - ufmt.Sprintf("router.gno__finalizeSwap() || too much wugnot spent (wrapped: %d, spend: %d)", userWrappedWugnot, spend), + ufmt.Sprintf("too much wugnot spent (wrapped: %d, spend: %d)", userWrappedWugnot, spend), )) } @@ -242,18 +229,20 @@ func finalizeSwap(inputToken, outputToken string, resultAmountIn, resultAmountOu unwrap(userRecvWugnot) } - if swapType == "EXACT_IN" { + // TODO (@notJoon): Is it possible for an invalid SwapType to get this point? + // TODO(@notJoon): remove not operatior and extract as function. + if swapType == ExactIn { if !tokenAmountLimit.Lte(afterFee) { panic(addDetailToError( errSlippage, - ufmt.Sprintf("router.gno__finalizeSwap() || too few received for user (expected minimum: %s, actual: %s, swapType: %s)", tokenAmountLimit.ToString(), afterFee.ToString(), swapType), + ufmt.Sprintf("too few received for user (expected minimum: %s, actual: %s, swapType: %s)", tokenAmountLimit.ToString(), afterFee.ToString(), swapType), )) } } else { if !resultAmountIn.Lte(tokenAmountLimit) { panic(addDetailToError( errSlippage, - ufmt.Sprintf("router.gno__finalizeSwap() || too much spent for user (expected maximum: %s, actual: %s, swapType: %s)", tokenAmountLimit.ToString(), resultAmountIn.ToString(), swapType), + ufmt.Sprintf("too much spent for user (expected maximum: %s, actual: %s, swapType: %s)", tokenAmountLimit.ToString(), resultAmountIn.ToString(), swapType), )) } } @@ -264,7 +253,7 @@ func finalizeSwap(inputToken, outputToken string, resultAmountIn, resultAmountOu func handleMultiSwap(swapType string, route string, numHops int, amountSpecified *i256.Int, isDry bool) (*u256.Uint, *u256.Uint) { switch swapType { - case "EXACT_IN": + case ExactIn: input, output, fee := getDataForMultiPath(route, 0) // first data swapParams := SwapParams{ tokenIn: input, @@ -279,7 +268,7 @@ func handleMultiSwap(swapType string, route string, numHops int, amountSpecified } return multiSwap(swapParams, 0, numHops, route) // iterate here - case "EXACT_OUT": + case ExactOut: input, output, fee := getDataForMultiPath(route, numHops-1) // last data swapParams := SwapParams{ tokenIn: input, @@ -297,7 +286,7 @@ func handleMultiSwap(swapType string, route string, numHops int, amountSpecified default: panic(addDetailToError( errInvalidSwapType, - ufmt.Sprintf("router.gno__handleMultiSwap() || unknown swapType(%s)", swapType), + ufmt.Sprintf("unknown swapType(%s)", swapType), )) } } diff --git a/router/type.gno b/router/type.gno index d78e39b8..d7f7c656 100644 --- a/router/type.gno +++ b/router/type.gno @@ -6,12 +6,9 @@ import ( i256 "gno.land/p/gnoswap/int256" ) -// SWAP TYPE -type SwapType string - const ( - ExactIn SwapType = "EXACT_IN" - ExactOut SwapType = "EXACT_OUT" + ExactIn string = "EXACT_IN" + ExactOut string = "EXACT_OUT" ) // SINGLE SWAP diff --git a/router/utils.gno b/router/utils.gno index fb41b81f..7b60711f 100644 --- a/router/utils.gno +++ b/router/utils.gno @@ -1,6 +1,7 @@ package router import ( + "bytes" "std" "strconv" "strings" @@ -10,12 +11,39 @@ import ( "gno.land/r/gnoswap/v1/common" ) +func assertNotASwapType(swapType string) { + switch swapType { + case ExactIn, ExactOut: + return + default: + panic(addDetailToError( + errInvalidSwapType, + ufmt.Sprintf("unknown swapType: expected ExactIn or ExactOut, got %s", swapType), + )) + } +} + +func assertDirectCallOnly() { + if common.GetLimitCaller() && std.PrevRealm().PkgPath() != "" { + panic(addDetailToError(errNoPermission, "only user can call this function")) + } +} + +func assertHopsInRange(hops int) { + if hops < 1 || hops > 3 { + panic(addDetailToError( + errInvalidInput, + ufmt.Sprintf("number of hops(%d) must be 1~3", hops), + )) + } +} + func poolPathWithFeeDivide(poolPath string) (string, string, int) { poolPathSplit, err := common.Split(poolPath, ":", 3) if err != nil { panic(addDetailToError( errInvalidPoolPath, - ufmt.Sprintf("utils.gno__poolPathWithFeeDivide() || invalid poolPath(%s)", poolPath), + ufmt.Sprintf("invalid poolPath(%s)", poolPath), )) } @@ -32,7 +60,7 @@ func getDataForSinglePath(poolPath string) (string, string, uint32) { if err != nil { panic(addDetailToError( errInvalidPoolPath, - ufmt.Sprintf("utils.gno__getDataForSinglePath() || len(poolPathSplit) != 3, poolPath: %s", poolPath), + ufmt.Sprintf("len(poolPathSplit) != 3, poolPath: %s", poolPath), )) } @@ -44,11 +72,12 @@ func getDataForSinglePath(poolPath string) (string, string, uint32) { } func getDataForMultiPath(possiblePath string, poolIdx int) (string, string, uint32) { - pools := strings.Split(possiblePath, "*POOL*") + pools := strings.Split(possiblePath, POOL_SEPARATOR) var token0, token1 string var fee uint32 + // TODO (@notJoon): remove hard-coded numbers switch poolIdx { case 0: token0, token1, fee = getDataForSinglePath(pools[0]) @@ -63,6 +92,8 @@ func getDataForMultiPath(possiblePath string, poolIdx int) (string, string, uint return token0, token1, fee } +// TODO (@notJoon): need to check how large the array is, how frequently the searches are, +// and whether the data already sorted. func isStringInStringArr(arr []string, str string) bool { for _, a := range arr { if a == str { @@ -104,3 +135,53 @@ func getPrev() (string, string) { prev := std.PrevRealm() return prev.Addr().String(), prev.PkgPath() } + +// splitSingleChar splits a string by a single character separator. +// +// This function is optimized for splitting strings with a single-byte separator. +// Unlike `strings.Split`, it: +// 1. Performs direct byte comparison instead of substring matching +// 2. Avoids additional string allocations by using slicing +// 3. Makes only one allocation for the result slice +// +// The main differences from `strings.Split` are: +// - Only works with single-byte separators +// - More memory efficient as it doesn't need to handle multi-byte separators +// - Faster for small to medium strings due to simpler byte comparison +// +// Performance: +// - Up to 5x faster than `strings.Split` for small strings (in Go) +// - For gno (run test with `-print-runtime-metrics` option): +// // | Function | Cycles | Allocations +// // |-----------------|------------------|--------------| +// // | strings.Split | 1.1M | 808.1K | +// // | splitSingleChar | 1.0M | 730.4K | +// - Uses zero allocations except for the initial result slice +// - Most effective for strings under 1KB with simple single-byte delimiters +// (* This test result was measured without the `uassert` package) +// +// Parameters: +// +// s (string): source string to split +// sep (byte): single byte separator to split on +// +// Returns: +// +// []string: slice containing the split string parts +func splitSingleChar(s string, sep byte) []string { + l := len(s) + if l == 0 { + return []string{""} + } + + result := make([]string, 0, bytes.Count([]byte(s), []byte{sep})+1) + start := 0 + for i := 0; i < l; i++ { + if s[i] == sep { + result = append(result, s[start:i]) + start = i + 1 + } + } + result = append(result, s[start:]) + return result +} diff --git a/router/utils_test.gno b/router/utils_test.gno new file mode 100644 index 00000000..d82f5128 --- /dev/null +++ b/router/utils_test.gno @@ -0,0 +1,66 @@ +package router + +import ( + "strings" + "testing" + + "gno.land/p/demo/uassert" +) + +func TestSplitSingleChar(t *testing.T) { + testCases := []struct { + name string + input string + sep byte + expected []string + }{ + { + name: "plain split", + input: "a,b,c", + sep: ',', + expected: []string{"a", "b", "c"}, + }, + { + name: "empty string", + input: "", + sep: ',', + expected: []string{""}, + }, + { + name: "no separator", + input: "abc", + sep: ',', + expected: []string{"abc"}, + }, + { + name: "consecutive separators", + input: "a,,b,,c", + sep: ',', + expected: []string{"a", "", "b", "", "c"}, + }, + { + name: "separator at the beginning and end", + input: ",a,b,c,", + sep: ',', + expected: []string{"", "a", "b", "c", ""}, + }, + { + name: "space separator", + input: "a b c", + sep: ' ', + expected: []string{"a", "b", "c"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := splitSingleChar(tc.input, tc.sep) + + uassert.Equal(t, len(result), len(tc.expected)) + + for i := 0; i < len(tc.expected); i++ { + uassert.Equal(t, result[i], tc.expected[i]) + } + }) + } +} diff --git a/staker/staker_external_incentive.gno b/staker/staker_external_incentive.gno index 99d6077b..8214861f 100644 --- a/staker/staker_external_incentive.gno +++ b/staker/staker_external_incentive.gno @@ -342,7 +342,7 @@ func isMidnight(startTime time.Time) bool { func getTokenPairBalanceFromPosition(poolPath string, tokenId uint64) (string, string) { pool := pl.GetPoolFromPoolPath(poolPath) - currentX96 := pool.PoolGetSlot0SqrtPriceX96() + currentX96 := pool.Slot0SqrtPriceX96() lowerX96 := common.TickMathGetSqrtRatioAtTick(pn.PositionGetPositionTickLower(tokenId)) upperX96 := common.TickMathGetSqrtRatioAtTick(pn.PositionGetPositionTickUpper(tokenId))