Skip to content

Commit

Permalink
Add tests and msgServer
Browse files Browse the repository at this point in the history
  • Loading branch information
jcompagni10 committed Apr 12, 2024
1 parent 6b94c06 commit 1bf8f2f
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 117 deletions.
2 changes: 1 addition & 1 deletion proto/neutron/dex/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ message MsgPlaceLimitOrder {
];
string price_in_to_out = 11 [
(gogoproto.moretags) = "yaml:\"price_in_to_out\"",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.customtype) = "github.com/neutron-org/neutron/v3/utils/math.PrecDec",
(gogoproto.nullable) = true,
(gogoproto.jsontag) = "price_in_to_out"
];
Expand Down
6 changes: 3 additions & 3 deletions utils/math/prec_dec.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

// NOTE: This file is nearly direct copy from cosmossdk.io/math/dec.go @v1.01
// The Precesion has been changed from 18 to 26
// The Precesion has been changed from 18 to 27

// NOTE: never use new(Dec) or else we will panic unmarshalling into the
// nil embedded big.Int
Expand All @@ -23,11 +23,11 @@ type PrecDec struct {

const (
// number of decimal places
Precision = 26
Precision = 27

// bits required to represent the above precision
// Ceiling[Log2[10^Precision - 1]]
PrecDecimalPrecisionBits = 87
PrecDecimalPrecisionBits = 90

// decimalTruncateBits is the minimum number of bits removed
// by a truncate operation. It is equal to
Expand Down
35 changes: 35 additions & 0 deletions x/dex/keeper/integration_placelimitorder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
sdkmath "cosmossdk.io/math"
abci "github.com/cometbft/cometbft/abci/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
math_utils "github.com/neutron-org/neutron/v3/utils/math"

"github.com/neutron-org/neutron/v3/x/dex/types"
)
Expand Down Expand Up @@ -301,6 +302,40 @@ func (s *DexTestSuite) TestLimitOrderPartialFillDepositCancel() {
s.assertDexBalances(0, 0)
}

func (s *DexTestSuite) TestPlaceLimitOrderWithPrice0To1() {
s.fundAliceBalances(10, 0)
s.fundBobBalances(0, 100)

// GIVEN
// Alice place LO at price ~10.0
trancheKey0 := s.limitSellsWithPrice(s.alice, "TokenA", math_utils.NewPrecDec(10), 10)

// WHEN bob swaps through all of Alice's LO
s.bobLimitSells("TokenB", -23078, 100, types.LimitOrderType_IMMEDIATE_OR_CANCEL)
s.aliceWithdrawsLimitSell(trancheKey0)

// THEN alice gets out ~100 TOKENB and bob pays ~100 TOKENA
s.assertAliceBalancesInt(sdkmath.ZeroInt(), sdkmath.NewInt(99_999_967))
s.assertBobBalancesInt(sdkmath.NewInt(10000000), sdkmath.NewInt(23))
}

func (s *DexTestSuite) TestPlaceLimitOrderWithPrice1To0() {
s.fundAliceBalances(0, 100)
s.fundBobBalances(25, 0)
var price = math_utils.MustNewPrecDecFromStr("0.25")
// GIVEN
// Alice place LO at price ~.25
trancheKey0 := s.limitSellsWithPrice(s.alice, "TokenB", price, 100)

// WHEN bob swaps through all of Alice's LO
s.limitSellsWithPrice(s.bob, "TokenA", price, 25)
s.aliceWithdrawsLimitSell(trancheKey0)

// THEN alice gets out ~25 TOKENA and bob pays ~25 TOKENB
s.assertAliceBalancesInt(sdkmath.NewInt(25000000), sdkmath.ZeroInt())
s.assertBobBalancesInt(sdkmath.ZeroInt(), sdkmath.NewInt(25000000))
}

// Fill Or Kill limit orders ///////////////////////////////////////////////////////////
func (s *DexTestSuite) TestPlaceLimitOrderFoKNoLiq() {
s.fundAliceBalances(10, 0)
Expand Down
9 changes: 8 additions & 1 deletion x/dex/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,19 @@ func (k MsgServer) PlaceLimitOrder(
if err != nil {
return &types.MsgPlaceLimitOrderResponse{}, err
}
tickIndex := msg.TickIndexInToOut
if msg.PriceInToOut != nil {
tickIndex, err = types.CalcTickIndexFromPrice(*msg.PriceInToOut)
if err != nil {
return &types.MsgPlaceLimitOrderResponse{}, err
}
}
trancheKey, coinIn, _, coinOutSwap, err := k.PlaceLimitOrderCore(
goCtx,
msg.TokenIn,
msg.TokenOut,
msg.AmountIn,
msg.TickIndexInToOut,
tickIndex,
msg.OrderType,
msg.ExpirationTime,
msg.MaxAmountOut,
Expand Down
23 changes: 23 additions & 0 deletions x/dex/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,29 @@ func (s *DexTestSuite) limitSellsWithMaxOut(
return msg.TrancheKey
}

func (s *DexTestSuite) limitSellsWithPrice(
account sdk.AccAddress,
tokenIn string,
price math_utils.PrecDec,
amountIn int,
) string {
tokenIn, tokenOut := dexkeeper.GetInOutTokens(tokenIn, "TokenA", "TokenB")

msg, err := s.msgServer.PlaceLimitOrder(s.GoCtx, &types.MsgPlaceLimitOrder{
Creator: account.String(),
Receiver: account.String(),
TokenIn: tokenIn,
TokenOut: tokenOut,
PriceInToOut: &price,
AmountIn: sdkmath.NewInt(int64(amountIn)).Mul(denomMultiple),
OrderType: types.LimitOrderType_GOOD_TIL_CANCELLED,
})

s.Assert().NoError(err)

return msg.TrancheKey
}

func (s *DexTestSuite) limitSellsInt(
account sdk.AccAddress,
tokenIn string,
Expand Down
11 changes: 11 additions & 0 deletions x/dex/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,15 @@ var (
1154,
"Swap amount too small; creates unfair spread for liquidity providers",
)

ErrCalcTickFromPrice = sdkerrors.Register(
ModuleName,
1155,
"Cannot convert price to int64 tick value",
)
ErrPriceOutsideRange = sdkerrors.Register(
ModuleName,
1156,
"Invalid price; 0.00000000000000000000000050 < PRICE > 2020125331305056766451886.728",
)
)
36 changes: 35 additions & 1 deletion x/dex/types/price.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package types

import (
sdkerrors "cosmossdk.io/errors"

math_utils "github.com/neutron-org/neutron/v3/utils/math"
"github.com/neutron-org/neutron/v3/x/dex/utils"
)

// NOTE: 559_680 is the highest possible tick at which price can be calculated with a < 1% error
// when using 26 digit decimal precision (via prec_dec).
// The error rate for very negative ticks approaches zero, so there is no concern there
const MaxTickExp uint64 = 559_680
const (
MaxTickExp uint64 = 559_680
MinPrice string = "0.000000000000000000000000495"
MaxPrice string = "2020125331305056766452345.127500016657360222036663651"
)

// Calculates the price for a swap from token 0 to token 1 given a relative tick
// tickIndex refers to the index of a specified tick such that x * 1.0001 ^(-1 * t) = y
Expand All @@ -24,6 +30,29 @@ func CalcPrice(relativeTickIndex int64) (math_utils.PrecDec, error) {
return math_utils.OnePrecDec().Quo(utils.BasePrice().Power(uint64(relativeTickIndex))), nil
}

func CalcTickIndexFromPrice(price math_utils.PrecDec) (int64, error) {
if IsPriceOutOfRange(price) {
return 0, ErrPriceOutsideRange
}

if price.LT(math_utils.OnePrecDec()) {
//Log precision is bad on small numbers so we invert first
tick, err := utils.Log(math_utils.OnePrecDec().Quo(price), utils.BasePrice())
if err != nil {
return 0, sdkerrors.Wrap(ErrCalcTickFromPrice, err.Error())
}

return tick.RoundInt64(), nil
}

tick, err := utils.Log(price, utils.BasePrice())
if err != nil {
return 0, sdkerrors.Wrap(ErrCalcTickFromPrice, err.Error())
}

return tick.RoundInt64() * -1, nil
}

func MustCalcPrice(relativeTickIndex int64) math_utils.PrecDec {
price, err := CalcPrice(relativeTickIndex)
if err != nil {
Expand All @@ -36,6 +65,11 @@ func IsTickOutOfRange(tickIndex int64) bool {
return utils.Abs(tickIndex) > MaxTickExp
}

func IsPriceOutOfRange(price math_utils.PrecDec) bool {
return price.GT(math_utils.MustNewPrecDecFromStr(MaxPrice)) ||
price.LT(math_utils.MustNewPrecDecFromStr(MinPrice))
}

func ValidateTickFee(tick int64, fee uint64) error {
// Ensure |tick| + fee <= MaxTickExp
// NOTE: Ugly arithmetic is to ensure that we don't overflow uint64
Expand Down
80 changes: 71 additions & 9 deletions x/dex/types/price_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,73 @@
package types_test

// This will continue to fail until we upgrade away from sdk.Dec
// func TestPriceMath(t *testing.T) {
// tick := 352437
// amount := sdk.MustNewDecFromStr("1000000000000000000000")
// basePrice := utils.BasePrice()
// expected := amount.Quo(basePrice.Power(uint64(tick))).TruncateInt()
// result := types.MustCalcPrice(int64(tick)).Mul(amount).TruncateInt()
// assert.Equal(t, expected.Int64(), result.Int64())
// }
import (
"errors"
"testing"

"github.com/stretchr/testify/require"

"github.com/neutron-org/neutron/v3/x/dex/types"
)

func TestCalcTickIndexFromPrice(t *testing.T) {
for _, tc := range []struct {
desc string
tick int64
}{
{
desc: "0",
tick: 0,
},
{
desc: "10",
tick: 10,
},
{
desc: "-10",
tick: -10,
},
{
desc: "100000",
tick: 100000,
},
{
desc: "-100000",
tick: -100000,
},
{
desc: "-100000",
tick: -100000,
},
{
desc: "-100000",
tick: -100000,
},
{
desc: "MaxTickExp",
tick: int64(types.MaxTickExp),
},
{
desc: "MinTickExp",
tick: int64(types.MaxTickExp) * -1,
},
{
desc: "GT MaxTickExp",
tick: int64(types.MaxTickExp) + 1,
},
{
desc: "LT TickExp",
tick: -1*int64(types.MaxTickExp) - 1,
},
} {
t.Run(tc.desc, func(t *testing.T) {
price, err1 := types.CalcPrice(tc.tick)
val, err2 := types.CalcTickIndexFromPrice(price)
if errors.Is(err1, types.ErrTickOutsideRange) {
require.ErrorIs(t, err2, types.ErrPriceOutsideRange)
} else {
require.NoError(t, err2)
require.Equal(t, tc.tick, val)
}
})
}
}
Loading

0 comments on commit 1bf8f2f

Please sign in to comment.