diff --git a/pool/_TEST_math_logic_test.gnoa b/pool/_TEST_math_logic_test.gnoa new file mode 100644 index 00000000..7ea61415 --- /dev/null +++ b/pool/_TEST_math_logic_test.gnoa @@ -0,0 +1,136 @@ +package pool + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + + pos "gno.land/r/position" +) + +var ( + gsa = testutils.TestAddress("gsa") + lp01 = testutils.TestAddress("lp01") + + pToken0 = "foo" + pToken1 = "bar" + pFee uint16 = 500 + sqrtPrice bigint = 130621891405341611593710811006 + + tickLower int32 = 9000 + tickUpper int32 = 11000 + liquidityAmount bigint = 123456789 + currentTick int32 = 10000 +) + +func init() { + std.TestSetOrigCaller(gsa) + InitManual() + CreatePool(pToken0, pToken1, pFee, sqrtPrice) +} + +func TestGetSqrtRatioFromTick(t *testing.T) { + sqrtX96 := GetSqrtRatioFromTick(currentTick) + shouldEQ(t, sqrtX96, sqrtPrice) +} + +func TestGetTickFromSqrtRatio(t *testing.T) { + tick := GetTickFromSqrtRatio(sqrtPrice) + shouldEQ(t, tick, 9999) // currentTick - 1 +} + +func TestDrySwap_ZeroForOneTrue_AmountSpecified_Positive_16000(t *testing.T) { + std.TestSetOrigCaller(lp01) + + // no mint == no liquidity => swap will fail + shouldPanic(t, func() { DrySwap(pToken0, pToken1, pFee, "_", true, 16000, MIN_PRICE) }) + + // not enough mint == swap will fail + pos.Mint(pToken0, pToken1, pFee, tickLower, tickUpper, 1000, 1000, 0, 0, 9999999999) + shouldPanic(t, func() { DrySwap(pToken0, pToken1, pFee, "_", true, 16000, MIN_PRICE) }) + + pos.Mint(pToken0, pToken1, pFee, tickLower, tickUpper, 100000, 100000, 0, 0, 9999999999) + + // zeroForOne true + // amountSpecified 16000 + input, output := DrySwap( + pToken0, // pToken0 + pToken1, // pToken1 + pFee, // pFee + "_", // recipient + true, // zeroForOne + 16000, // amountSpecified + MIN_PRICE, // sqrtPriceLimitX96 + ) + shouldEQ(t, input, bigint(16000)) + shouldEQ(t, output, bigint(-42574)) +} + +func TestDrySwap_ZeroForOneTrue_AmountSpecified_Negative_16000(t *testing.T) { + // zeroForOne true + // amountSpecified -16000 + + input, output := DrySwap( + pToken0, // pToken0 + pToken1, // pToken1 + pFee, // pFee + "_", // recipient + true, // zeroForOne + -16000, // amountSpecified + MIN_SQRT_RATIO+1, // sqrtPriceLimitX96 + ) + + shouldEQ(t, input, bigint(5934)) + shouldEQ(t, output, bigint(-15999)) +} + +func TestDrySwap_ZeroForOneFalse_AmountSpecified_Positive_16000(t *testing.T) { + // zeroForOne false + // amountSpecified 16000 + + input, output := DrySwap( + pToken0, // pToken0 + pToken1, // pToken1 + pFee, // pFee + "_", // recipient + false, // zeroForOne + 16000, // amountSpecified + MAX_SQRT_RATIO-1, // sqrtPriceLimitX96 + ) + shouldEQ(t, input, bigint(-42574)) + shouldEQ(t, output, bigint(16000)) +} + +func TestDrySwap_ZeroForOneFalse_AmountSpecified_Negative_16000(t *testing.T) { + // zeroForOne false + // amountSpecified -16000 + + input, output := DrySwap( + pToken0, // pToken0 + pToken1, // pToken1 + pFee, // pFee + "_", // recipient + false, // zeroForOne + -16000, // amountSpecified + MAX_SQRT_RATIO-1, // sqrtPriceLimitX96 + ) + shouldEQ(t, input, bigint(-15999)) + shouldEQ(t, output, bigint(5934)) +} + +/* HELPER */ +func shouldEQ(t *testing.T, got, expected interface{}) { + if got != expected { + t.Errorf("got %v, expected %v", got, expected) + } +} + +func shouldPanic(t *testing.T, f func()) { + defer func() { + if r := recover(); r == nil { + t.Errorf("expected panic") + } + }() + f() +} diff --git a/pool/pool_multi_lp_fee_api_test.gno b/pool/_TEST_pool_multi_lp_fee_api_test.gnoa similarity index 100% rename from pool/pool_multi_lp_fee_api_test.gno rename to pool/_TEST_pool_multi_lp_fee_api_test.gnoa diff --git a/pool/pool_multi_lp_fee_test.gnoa b/pool/_TEST_pool_multi_lp_fee_test.gnoa similarity index 100% rename from pool/pool_multi_lp_fee_test.gnoa rename to pool/_TEST_pool_multi_lp_fee_test.gnoa diff --git a/pool/pool_multi_lp_test.gnoa b/pool/_TEST_pool_multi_lp_test.gnoa similarity index 100% rename from pool/pool_multi_lp_test.gnoa rename to pool/_TEST_pool_multi_lp_test.gnoa diff --git a/pool/_TEST_pool_router_test.gno b/pool/_TEST_pool_router_test.gno new file mode 100644 index 00000000..d2f561ab --- /dev/null +++ b/pool/_TEST_pool_router_test.gno @@ -0,0 +1,160 @@ +package pool + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/r/demo/users" + + bar "gno.land/r/bar" + foo "gno.land/r/foo" + + pos "gno.land/r/position" +) + +var ( + gsa = testutils.TestAddress("gsa") // Gnoswap Admin + lp01 = testutils.TestAddress("lp01") // Liquidity Provider 01 + + poolAddr = std.DerivePkgAddr("gno.land/r/pool") +) + +// 1. Init +func TestInitManual(t *testing.T) { + std.TestSetOrigCaller(gsa) + InitManual() + std.TestSkipHeights(1) +} + +func TestCreatePool(t *testing.T) { + CreatePool("foo", "bar", uint16(500), 5602223755577321903022134995689) // 85_176 == x4999.904 + std.TestSkipHeights(1) + // fee = 500 + // tickSpacing = 10 + + // sqrtPriceX96 = 130621891405341611593710811006 + // tick = 10_000 + // ratio = 1.648% + + CreatePool("foo", "bar", uint16(3000), 255973311431586396528129062412) // 23456 == 3.23% + std.TestSkipHeights(1) + // fee = 3000 + // tickSpacing = 60 + + // sqrtPriceX96 = 255973311431586396528129062412 + // tick = 23_456 + // ratio = 3.23% +} + +func TestPositionMint500(t *testing.T) { + std.TestSetOrigCaller(lp01) + + _, _, m0, m1 := pos.Mint( + "foo", // token0 + "bar", // token1 + uint16(500), // fee 500 ~= tickSpacing 10 + 84220, // tickLower // x4544 + 86130, // tickUpper // x5500 + 30000000, // amount0Desired + 30000000, // amount1Desired + 0, // amount0Min + 0, // amount1Min + 9999999999, // deadline + ) + + shouldEQ(t, m0, bigint(5987)) // x5010.8565224653 + shouldEQ(t, m1, bigint(29999998)) + + shouldEQ(t, bigint(fooBalance(poolAddr)), m0) + shouldEQ(t, bigint(barBalance(poolAddr)), m1) +} + +func TestPositionMint3000(t *testing.T) { + std.TestSetOrigCaller(lp01) + + _, _, m0, m1 := pos.Mint( + "foo", // token0 + "bar", // token1 + uint16(3000), // fee 3000 ~= tickSpacing 60 + 22800, // tickLower + 24000, // tickUpper + 1000, // amount0Desired + 1000, // amount1Desired + 0, // amount0Min + 0, // amount1Min + 9999999999, // deadline + ) + shouldEQ(t, m0, bigint(79)) + shouldEQ(t, m1, bigint(999)) +} + +func TestFindBestPoolTruePositive(t *testing.T) { + bestPath := FindBestPool( + "foo", // tokenA + "bar", // tokenB + true, // zeroForOne + 500, // amountSpecified + ) + shouldEQ(t, bestPath, "bar_foo_500") +} + +func TestFindBestPoolTrueNegative(t *testing.T) { + bestPath := FindBestPool( + "foo", // tokenA + "bar", // tokenB + true, // zeroForOne + -5000, // amountSpecified + ) + shouldEQ(t, bestPath, "bar_foo_500") +} + +func TestFindBestPoolFalsePositive(t *testing.T) { + bestPath := FindBestPool( + "foo", // tokenA + "bar", // tokenB + false, // zeroForOne + 50, // amountSpecified + ) + shouldEQ(t, bestPath, "bar_foo_3000") +} + +func TestFindBestPoolFalseNegative(t *testing.T) { + bestPath := FindBestPool( + "foo", // tokenA + "bar", // tokenB + false, // zeroForOne + -1234, // amountSpecified + ) + shouldEQ(t, bestPath, "bar_foo_500") +} + +func TestFindBestPoolWrong(t *testing.T) { + shouldPanic(t, func() { FindBestPool("foo", "bar", true, 0) }) +} + +/* HELPER */ +func shouldEQ(t *testing.T, got, expected interface{}) { + if got != expected { + t.Errorf("got %v, expected %v", got, expected) + } +} + +func shouldPanic(t *testing.T, f func()) { + defer func() { + if r := recover(); r == nil { + t.Errorf("expected panic") + } + }() + f() +} + +func fooBalance(addr std.Address) uint64 { + user := users.AddressOrName(addr) + return foo.BalanceOf(user) +} + +func barBalance(addr std.Address) uint64 { + user := users.AddressOrName(addr) + return bar.BalanceOf(user) +} diff --git a/pool/pool_same_token_pair_diff_fee_test.gnoa b/pool/_TEST_pool_same_token_pair_diff_fee_test.gnoa similarity index 100% rename from pool/pool_same_token_pair_diff_fee_test.gnoa rename to pool/_TEST_pool_same_token_pair_diff_fee_test.gnoa diff --git a/pool/pool_single_lp_test.gnoa b/pool/_TEST_pool_single_lp_test.gnoa similarity index 100% rename from pool/pool_single_lp_test.gnoa rename to pool/_TEST_pool_single_lp_test.gnoa diff --git a/pool/math_logic_test.gnoa b/pool/math_logic_test.gnoa deleted file mode 100644 index dfc74fd4..00000000 --- a/pool/math_logic_test.gnoa +++ /dev/null @@ -1,129 +0,0 @@ -package pool - -import ( - "std" - "testing" - - "gno.land/p/demo/testutils" -) - -var ( - gsa = testutils.TestAddress("gsa") - - pToken0 = "foo" - pToken1 = "bar" - pFee = uint16(500) - - tickLower = int32(9000) - tickUpper = int32(11000) - - liquidityAmount = bigint(123456789) - - currentTick = int32(10000) - - sqrtPrice bigint = 130621891405341611593710811006 -) - -func init() { - std.TestSetOrigCaller(gsa) - InitManual() - CreatePool("foo", "bar", pFee, sqrtPrice) -} - -func TestGetAmountsFromLiquidity(t *testing.T) { - amount0, amount1 := getAmountsFromLiquidity(pToken0, pToken1, pFee, tickLower, tickUpper, liquidityAmount, currentTick) - shouldEQ(t, amount0, bigint(3651869)) - shouldEQ(t, amount1, bigint(9926315)) -} - -func TestGetSqrtRatioFromTick(t *testing.T) { - sqrtX96 := getSqrtRatioFromTick(currentTick) // tick 10000 - shouldEQ(t, sqrtX96, sqrtPrice) // 130621891405341611593710811006 -} - -func TestGetTickFromSqrtRatio(t *testing.T) { - tick := getTickFromSqrtRatio(sqrtPrice) - shouldEQ(t, tick, 9999) // currentTick - 1 -} - -func TestSwapAmountZ_ZeroForOneTrue_AmountSpecified_Positive_16000(t *testing.T) { - // zeroForOne true - // amountSpecified 16000 - - input, output := swapAmount( - pToken0, // pToken0 - pToken1, // pToken1 - pFee, // pFee - "_", // recipient - true, // zeroForOne - 16000, // amountSpecified - MIN_SQRT_RATIO+1, // sqrtPriceLimitX96 - ) - shouldEQ(t, input, bigint(16000)) - shouldEQ(t, output, bigint(-43459)) -} - -func TestSwapAmountZ_ZeroForOneTrue_AmountSpecified_Negative_16000(t *testing.T) { - // zeroForOne true - // amountSpecified -16000 - - input, output := swapAmount( - pToken0, // pToken0 - pToken1, // pToken1 - pFee, // pFee - "_", // recipient - true, // zeroForOne - -16000, // amountSpecified - MIN_SQRT_RATIO+1, // sqrtPriceLimitX96 - ) - - shouldEQ(t, input, bigint(5888)) - shouldEQ(t, output, bigint(-15999)) -} - -func TestSwapAmountZ_ZeroForOneFalse_AmountSpecified_Positive_16000(t *testing.T) { - // zeroForOne false - // amountSpecified 16000 - - input, output := swapAmount( - pToken0, // pToken0 - pToken1, // pToken1 - pFee, // pFee - "_", // recipient - false, // zeroForOne - 16000, // amountSpecified - MAX_SQRT_RATIO-1, // sqrtPriceLimitX96 - ) - shouldEQ(t, input, bigint(-43459)) - shouldEQ(t, output, bigint(16000)) -} - -func TestSwapAmountZ_ZeroForOneFalse_AmountSpecified_Negative_16000(t *testing.T) { - // zeroForOne false - // amountSpecified -16000 - - input, output := swapAmount( - pToken0, // pToken0 - pToken1, // pToken1 - pFee, // pFee - "_", // recipient - false, // zeroForOne - -16000, // amountSpecified - MAX_SQRT_RATIO-1, // sqrtPriceLimitX96 - ) - shouldEQ(t, input, bigint(-15999)) - shouldEQ(t, output, bigint(5888)) -} - -/* HELPER */ -func shouldEQ(t *testing.T, got, expected interface{}) { - if got != expected { - t.Errorf("got %v, expected %v", got, expected) - } -} - -func shouldNEQ(t *testing.T, got, expected interface{}) { - if got == expected { - t.Errorf("got %v, didn't expected %v", got, expected) - } -} diff --git a/pool/pool_router.gno b/pool/pool_router.gno new file mode 100644 index 00000000..4096523b --- /dev/null +++ b/pool/pool_router.gno @@ -0,0 +1,124 @@ +package pool + +import ( + "strconv" + "strings" + + "gno.land/p/demo/ufmt" +) + +func FindBestPool( + tokenA string, + tokenB string, + zeroForOne bool, + amountSpecified bigint, +) string { + if tokenA == tokenB { + panic("token pair cannot be the same") + } + + if tokenA > tokenB { + tokenA, tokenB = tokenB, tokenA + } + + partialPath := tokenA + "_" + tokenB + "_" + foundPool := []string{} + + for poolPath, _ := range pools { + if strings.HasPrefix(poolPath, partialPath) { + foundPool = append(foundPool, poolPath) + } + } + + // check if amount is enough + firstSelectedPool := []string{} + if false { + continue + } else if zeroForOne == true && amountSpecified > 0 { + for _, singlePool := range foundPool { + p := GetPoolFromPoolKey(singlePool) + poolSqrtX96 := p.slot0.sqrtPriceX96 + poolPrice := poolSqrtX96 * poolSqrtX96 / Q96 / Q96 + + pool := GetPoolFromPoolKey(singlePool) + if pool.balances.token1 > amountSpecified*poolPrice { // must be bigger (can't be equal due to fee) + firstSelectedPool = append(firstSelectedPool, singlePool) + } + } + + } else if zeroForOne == true && amountSpecified < 0 { + for _, singlePool := range foundPool { + pool := GetPoolFromPoolKey(singlePool) + if pool.balances.token1 > (-1 * amountSpecified) { // must be bigger (can't be equal due to fee) + firstSelectedPool = append(firstSelectedPool, singlePool) + } + } + + } else if zeroForOne == false && amountSpecified > 0 { + for _, singlePool := range foundPool { + p := GetPoolFromPoolKey(singlePool) + poolSqrtX96 := p.slot0.sqrtPriceX96 + poolPrice := poolSqrtX96 * poolSqrtX96 / Q96 / Q96 + + pool := GetPoolFromPoolKey(singlePool) + if pool.balances.token0 > amountSpecified/poolPrice { // must be bigger (can't be equal due to fee) + firstSelectedPool = append(firstSelectedPool, singlePool) + } + } + + } else if zeroForOne == false && amountSpecified < 0 { + for _, singlePool := range foundPool { + pool := GetPoolFromPoolKey(singlePool) + if pool.balances.token0 > (-1 * amountSpecified) { // must be bigger (can't be equal due to fee) + firstSelectedPool = append(firstSelectedPool, singlePool) + } + } + } else { + panic(ufmt.Sprintf("[POOL] pool_router.gno__FindBestPool() || unknown swap condition, zeroForOne: %t, amountSpecified: %d", zeroForOne, amountSpecified)) + } + + // check tick and return + var poolWithTick map[int32]string = make(map[int32]string) + + minTick := int32(887272) + maxTick := int32(-887272) + for _, singlePool := range firstSelectedPool { + // save tick with poolPath + pool := GetPoolFromPoolKey(singlePool) + poolTick := pool.slot0.tick + + poolWithTick[poolTick] = singlePool + + // find min + if poolTick < minTick { + minTick = poolTick + } + + // find max + if poolTick > maxTick { + maxTick = poolTick + } + } + + if zeroForOne == true { // if token0 is being sold to buy token1, then we want to find the pool with the largest tick (more token1 can be bought) + return poolWithTick[maxTick] + + } else { // if token1 is being sold to buy token0, then we want to find the pool with the smallest tick (more token0 can be bought) + return poolWithTick[minTick] + } +} + +func poolPathDivide(poolPath string) (string, string, uint16) { + poolPathSplit := strings.Split(poolPath, "_") + + if len(poolPathSplit) != 3 { + panic(ufmt.Sprintf("[POOL] pool_router.gno__poolPathDivide() || len(poolPathSplit) != 3, poolPath: %s", poolPath)) + } + + feeInt, err := strconv.ParseInt(poolPathSplit[2], 10, 16) + if err != nil { + panic(ufmt.Sprintf("[POOL] pool_router.gno__poolPathDivide() || cannot convert fee(%s) to uint16", poolPathSplit[2])) + } + + return poolPathSplit[0], poolPathSplit[1], uint16(feeInt) +}