From 4fc6109bf36aae76f34c8d86e21dc73333e47e48 Mon Sep 17 00:00:00 2001 From: Tian Date: Tue, 17 Sep 2024 15:17:48 -0400 Subject: [PATCH] add megavault withdrawal fee estimation (#2242) --- protocol/lib/big_math.go | 13 + protocol/lib/big_math_test.go | 50 ++ protocol/lib/vault/utils.go | 52 ++ protocol/lib/vault/utils_test.go | 116 +++++ protocol/mocks/PerpetualsKeeper.go | 28 ++ protocol/x/vault/keeper/orders.go | 56 +-- protocol/x/vault/keeper/vault.go | 88 ++++ protocol/x/vault/keeper/withdraw.go | 119 +++++ protocol/x/vault/keeper/withdraw_test.go | 524 +++++++++++++++++++++ protocol/x/vault/types/errors.go | 6 +- protocol/x/vault/types/expected_keepers.go | 4 + 11 files changed, 1010 insertions(+), 46 deletions(-) create mode 100644 protocol/lib/vault/utils.go create mode 100644 protocol/lib/vault/utils_test.go create mode 100644 protocol/x/vault/keeper/withdraw.go create mode 100644 protocol/x/vault/keeper/withdraw_test.go diff --git a/protocol/lib/big_math.go b/protocol/lib/big_math.go index 705623172c..3515ea6392 100644 --- a/protocol/lib/big_math.go +++ b/protocol/lib/big_math.go @@ -88,6 +88,19 @@ func BigMin(a, b *big.Int) *big.Int { return result } +// BigRatMin takes two `big.Rat` as parameters and returns the smaller one. +func BigRatMin(a, b *big.Rat) *big.Rat { + result := new(big.Rat) + // If `a` is greater than `b`, return `b` since it is smaller. + // Else, return `a` since it is smaller than or equal to `b`. + if a.Cmp(b) > 0 { + result.Set(b) + } else { + result.Set(a) + } + return result +} + // BigMax takes two `big.Int` as parameters and returns the larger one. func BigMax(a, b *big.Int) *big.Int { result := new(big.Int) diff --git a/protocol/lib/big_math_test.go b/protocol/lib/big_math_test.go index e2ffe46a30..d62f015a2e 100644 --- a/protocol/lib/big_math_test.go +++ b/protocol/lib/big_math_test.go @@ -304,6 +304,56 @@ func TestBigMin(t *testing.T) { } } +func TestBigRatMin(t *testing.T) { + tests := map[string]struct { + a *big.Rat + b *big.Rat + expected *big.Rat + }{ + "a is smaller than b": { + a: big.NewRat(5, 2), + b: big.NewRat(6, 2), + expected: big.NewRat(5, 2), + }, + "b is smaller than a": { + a: big.NewRat(7, 1), + b: big.NewRat(4, 1), + expected: big.NewRat(4, 1), + }, + "a is equal to b": { + a: big.NewRat(8, 7), + b: big.NewRat(8, 7), + expected: big.NewRat(8, 7), + }, + "a and b are negative, a is less than b": { + a: big.NewRat(-8, 3), + b: big.NewRat(-7, 3), + expected: big.NewRat(-8, 3), + }, + "a and b are negative, b is less than a": { + a: big.NewRat(-9, 5), + b: big.NewRat(-10, 5), + expected: big.NewRat(-10, 5), + }, + "a is positive, b is negative, and abs(a) is less than abs(b)": { + a: big.NewRat(4, 3), + b: big.NewRat(-7, 2), + expected: big.NewRat(-7, 2), + }, + "a is positive, b is negative, and abs(a) is greater than abs(b)": { + a: big.NewRat(7, 2), + b: big.NewRat(-4, 3), + expected: big.NewRat(-4, 3), + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result := lib.BigRatMin(tc.a, tc.b) + require.Equal(t, tc.expected, result) + }) + } +} + func TestBigMax(t *testing.T) { tests := map[string]struct { a *big.Int diff --git a/protocol/lib/vault/utils.go b/protocol/lib/vault/utils.go new file mode 100644 index 0000000000..c5562bc809 --- /dev/null +++ b/protocol/lib/vault/utils.go @@ -0,0 +1,52 @@ +package vault + +import ( + "math/big" + + "github.com/dydxprotocol/v4-chain/protocol/lib" + pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" + "github.com/dydxprotocol/v4-chain/protocol/x/vault/types" +) + +// SkewAntiderivative returns the antiderivative of skew given a vault's skew +// factor and leverage. +// skew_antiderivative = skew_factor * leverage^2 + skew_factor^2 * leverage^3 / 3 +func SkewAntiderivative( + skewFactorPpm uint32, + leverage *big.Rat, +) *big.Rat { + bigSkewFactorPpm := new(big.Rat).SetUint64(uint64(skewFactorPpm)) + bigOneMillion := lib.BigRatOneMillion() + + // a = skew_factor * leverage^2. + a := new(big.Rat).Mul(leverage, leverage) + a.Mul(a, bigSkewFactorPpm) + + // b = skew_factor^2 * leverage^3 / 3. + b := new(big.Rat).Set(a) + b.Mul(b, leverage) + b.Mul(b, bigSkewFactorPpm) + b.Quo(b, big.NewRat(3, 1)) + + // normalize `a` whose unit currently is ppm. + a.Quo(a, bigOneMillion) + // normalize `b` whose unit currently is ppm * ppm. + b.Quo(b, bigOneMillion) + b.Quo(b, bigOneMillion) + + // return a + b. + return a.Add(a, b) +} + +// SpreadPpm returns the spread that a vault should quote at given its +// quoting params and corresponding market param. +// spread_ppm = max(spread_min_ppm, spread_buffer_ppm + min_price_change_ppm) +func SpreadPpm( + quotingParams *types.QuotingParams, + marketParam *pricestypes.MarketParam, +) uint32 { + return lib.Max( + quotingParams.SpreadMinPpm, + quotingParams.SpreadBufferPpm+marketParam.MinPriceChangePpm, + ) +} diff --git a/protocol/lib/vault/utils_test.go b/protocol/lib/vault/utils_test.go new file mode 100644 index 0000000000..699717138a --- /dev/null +++ b/protocol/lib/vault/utils_test.go @@ -0,0 +1,116 @@ +package vault_test + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/dydxprotocol/v4-chain/protocol/lib/vault" + pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" + "github.com/dydxprotocol/v4-chain/protocol/x/vault/types" +) + +func TestSkewAntiderivativePpm(t *testing.T) { + tests := map[string]struct { + skewFactorPpm uint32 + leverage *big.Rat + expected *big.Rat + }{ + "Zero skew factor and leverage": { + skewFactorPpm: 0, + leverage: big.NewRat(0, 1), + expected: big.NewRat(0, 1), + }, + "Non-zero skew factor, zero leverage": { + skewFactorPpm: 1_000_000, + leverage: big.NewRat(0, 1), + expected: big.NewRat(0, 1), + }, + "Zero skew factor, non-zero leverage": { + skewFactorPpm: 0, + leverage: big.NewRat(1_000_000, 1), + expected: big.NewRat(0, 1), + }, + "Small skew factor and small positive leverage": { + skewFactorPpm: 500_000, // 0.5 + leverage: big.NewRat(4, 5), // 0.8 + // 0.5 * 0.8^2 + 0.5^2 * 0.8^3 / 3 = 136/375 + expected: big.NewRat(136, 375), + }, + "Small skew factor and small negative leverage": { + skewFactorPpm: 500_000, // 0.5 + leverage: big.NewRat(-4, 5), // -0.8 + // 0.5 * (-0.8)^2 + 0.5^2 * (-0.8)^3 / 3 = 104/375 + expected: big.NewRat(104, 375), + }, + "Large skew factor and large positive leverage": { + skewFactorPpm: 5_000_000, // 5 + leverage: big.NewRat(87, 10), // 8.7 + // 5 * (8.7)^2 + 5^2 * (8.7)^3 / 3 = 234639/40 + expected: big.NewRat(234_639, 40), + }, + "Large skew factor and large negative leverage": { + skewFactorPpm: 5_000_000, // 5 + leverage: big.NewRat(-87, 10), // -8.7 + // 5 * (-8.7)^2 + 5^2 * (-8.7)^3 / 3 = -204363/40 + expected: big.NewRat(-204_363, 40), + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + actual := vault.SkewAntiderivative(tc.skewFactorPpm, tc.leverage) + require.Equal(t, tc.expected, actual) + }) + } +} + +func TestSpreadPpm(t *testing.T) { + tests := map[string]struct { + quotingParams *types.QuotingParams + marketParam *pricestypes.MarketParam + expected uint32 + }{ + "SpreadMinPpm > SpreadBufferPpm + MinPriceChangePpm": { + quotingParams: &types.QuotingParams{ + SpreadMinPpm: 1000, + SpreadBufferPpm: 200, + }, + marketParam: &pricestypes.MarketParam{ + MinPriceChangePpm: 500, + }, + expected: 1000, + }, + "SpreadMinPpm < SpreadBufferPpm + MinPriceChangePpm": { + quotingParams: &types.QuotingParams{ + SpreadMinPpm: 1000, + SpreadBufferPpm: 600, + }, + marketParam: &pricestypes.MarketParam{ + MinPriceChangePpm: 500, + }, + expected: 1100, + }, + "SpreadMinPpm = SpreadBufferPpm + MinPriceChangePpm": { + quotingParams: &types.QuotingParams{ + SpreadMinPpm: 1000, + SpreadBufferPpm: 400, + }, + marketParam: &pricestypes.MarketParam{ + MinPriceChangePpm: 600, + }, + expected: 1000, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + require.Equal( + t, + tc.expected, + vault.SpreadPpm(tc.quotingParams, tc.marketParam), + ) + }) + } +} diff --git a/protocol/mocks/PerpetualsKeeper.go b/protocol/mocks/PerpetualsKeeper.go index ed2237ba87..fb83519c53 100644 --- a/protocol/mocks/PerpetualsKeeper.go +++ b/protocol/mocks/PerpetualsKeeper.go @@ -122,6 +122,34 @@ func (_m *PerpetualsKeeper) GetAllPerpetuals(ctx types.Context) []perpetualstype return r0 } +// GetLiquidityTier provides a mock function with given fields: ctx, id +func (_m *PerpetualsKeeper) GetLiquidityTier(ctx types.Context, id uint32) (perpetualstypes.LiquidityTier, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for GetLiquidityTier") + } + + var r0 perpetualstypes.LiquidityTier + var r1 error + if rf, ok := ret.Get(0).(func(types.Context, uint32) (perpetualstypes.LiquidityTier, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(types.Context, uint32) perpetualstypes.LiquidityTier); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(perpetualstypes.LiquidityTier) + } + + if rf, ok := ret.Get(1).(func(types.Context, uint32) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetNetCollateral provides a mock function with given fields: ctx, id, bigQuantums func (_m *PerpetualsKeeper) GetNetCollateral(ctx types.Context, id uint32, bigQuantums *big.Int) (*big.Int, error) { ret := _m.Called(ctx, id, bigQuantums) diff --git a/protocol/x/vault/keeper/orders.go b/protocol/x/vault/keeper/orders.go index 2ed4c48bff..fccb96f6e7 100644 --- a/protocol/x/vault/keeper/orders.go +++ b/protocol/x/vault/keeper/orders.go @@ -1,6 +1,7 @@ package keeper import ( + "errors" "fmt" "math" "math/big" @@ -11,6 +12,7 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/lib" "github.com/dydxprotocol/v4-chain/protocol/lib/log" "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" + "github.com/dydxprotocol/v4-chain/protocol/lib/vault" clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" "github.com/dydxprotocol/v4-chain/protocol/x/vault/types" ) @@ -154,31 +156,11 @@ func (k Keeper) GetVaultClobOrders( vaultId types.VaultId, ) (orders []*clobtypes.Order, err error) { // Get clob pair, perpetual, market parameter, and market price that correspond to this vault. - clobPair, exists := k.clobKeeper.GetClobPair(ctx, clobtypes.ClobPairId(vaultId.Number)) - if !exists || clobPair.Status == clobtypes.ClobPair_STATUS_FINAL_SETTLEMENT { + clobPair, perpetual, marketParam, marketPrice, err := k.GetVaultClobPerpAndMarket(ctx, vaultId) + if errors.Is(err, types.ErrClobPairNotFound) || clobPair.Status == clobtypes.ClobPair_STATUS_FINAL_SETTLEMENT { return []*clobtypes.Order{}, nil - } - perpId := clobPair.Metadata.(*clobtypes.ClobPair_PerpetualClobMetadata).PerpetualClobMetadata.PerpetualId - perpetual, err := k.perpetualsKeeper.GetPerpetual(ctx, perpId) - if err != nil { - return orders, errorsmod.Wrap( - err, - fmt.Sprintf("VaultId: %v", vaultId), - ) - } - marketParam, exists := k.pricesKeeper.GetMarketParam(ctx, perpetual.Params.MarketId) - if !exists { - return orders, errorsmod.Wrap( - types.ErrMarketParamNotFound, - fmt.Sprintf("VaultId: %v", vaultId), - ) - } - marketPrice, err := k.pricesKeeper.GetMarketPrice(ctx, perpetual.Params.MarketId) - if err != nil { - return orders, errorsmod.Wrap( - err, - fmt.Sprintf("VaultId: %v", vaultId), - ) + } else if err != nil { + return orders, err } else if marketPrice.Price == 0 { // Market price can be zero upon market initialization or due to invalid exchange config. return orders, errorsmod.Wrap( @@ -187,26 +169,13 @@ func (k Keeper) GetVaultClobOrders( ) } - // Calculate leverage = open notional / equity. - equity, err := k.GetVaultEquity(ctx, vaultId) + // Get vault leverage and equity. + leverage, equity, err := k.GetVaultLeverageAndEquity(ctx, vaultId, perpetual, marketPrice) if err != nil { return orders, err } - if equity.Sign() <= 0 { - return orders, errorsmod.Wrap( - types.ErrNonPositiveEquity, - fmt.Sprintf("VaultId: %v", vaultId), - ) - } - inventory := k.GetVaultInventoryInPerpetual(ctx, vaultId, perpId) - openNotional := lib.BaseToQuoteQuantums( - inventory, - perpetual.Params.AtomicResolution, - marketPrice.GetPrice(), - marketPrice.GetExponent(), - ) - leveragePpm := new(big.Int).Mul(openNotional, lib.BigIntOneMillion()) - leveragePpm.Quo(leveragePpm, equity) + leveragePpm := new(big.Int).Mul(leverage.Num(), lib.BigIntOneMillion()) + leveragePpm = lib.BigDivCeil(leveragePpm, leverage.Denom()) // Get vault parameters. quotingParams, exists := k.GetVaultQuotingParams(ctx, vaultId) @@ -247,10 +216,7 @@ func (k Keeper) GetVaultClobOrders( } // Calculate spread. - spreadPpm := lib.BigU(lib.Max( - quotingParams.SpreadMinPpm, - quotingParams.SpreadBufferPpm+marketParam.MinPriceChangePpm, - )) + spreadPpm := lib.BigU(vault.SpreadPpm("ingParams, &marketParam)) // Get oracle price in subticks. oracleSubticks := clobtypes.PriceToSubticks( marketPrice, diff --git a/protocol/x/vault/keeper/vault.go b/protocol/x/vault/keeper/vault.go index db92ea56a8..7f68377f4f 100644 --- a/protocol/x/vault/keeper/vault.go +++ b/protocol/x/vault/keeper/vault.go @@ -1,12 +1,17 @@ package keeper import ( + "fmt" "math/big" errorsmod "cosmossdk.io/errors" "cosmossdk.io/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/dydxprotocol/v4-chain/protocol/lib" "github.com/dydxprotocol/v4-chain/protocol/lib/log" + clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" + perptypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" + pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" "github.com/dydxprotocol/v4-chain/protocol/x/vault/types" ) @@ -56,6 +61,44 @@ func (k Keeper) GetVaultEquity( return k.GetSubaccountEquity(ctx, *vaultId.ToSubaccountId()) } +// GetVaultLeverageAndEquity returns a vault's leverage and equity. +// - leverage = open notional / equity. +func (k Keeper) GetVaultLeverageAndEquity( + ctx sdk.Context, + vaultId types.VaultId, + perpetual perptypes.Perpetual, + marketPrice pricestypes.MarketPrice, +) ( + leverage *big.Rat, + equity *big.Int, + err error, +) { + equity, err = k.GetVaultEquity(ctx, vaultId) + if err != nil { + return nil, nil, err + } + if equity.Sign() <= 0 { + return nil, equity, errorsmod.Wrap( + types.ErrNonPositiveEquity, + fmt.Sprintf("VaultId: %v", vaultId), + ) + } + + inventory := k.GetVaultInventoryInPerpetual(ctx, vaultId, perpetual.GetId()) + openNotional := lib.BaseToQuoteQuantums( + inventory, + perpetual.Params.AtomicResolution, + marketPrice.GetPrice(), + marketPrice.GetExponent(), + ) + leverage = new(big.Rat).SetFrac( + openNotional, + equity, + ) + + return leverage, equity, nil +} + // GetSubaccountEquity returns the equity of a subaccount (in quote quantums). func (k Keeper) GetSubaccountEquity( ctx sdk.Context, @@ -92,6 +135,51 @@ func (k Keeper) GetVaultInventoryInPerpetual( return inventory } +// GetVaultClobPerpAndMarket returns the clob pair, perpetual, market param, and market price +// that correspond to a vault. +func (k Keeper) GetVaultClobPerpAndMarket( + ctx sdk.Context, + vaultId types.VaultId, +) ( + clobPair clobtypes.ClobPair, + perpetual perptypes.Perpetual, + marketParam pricestypes.MarketParam, + marketPrice pricestypes.MarketPrice, + err error, +) { + clobPair, exists := k.clobKeeper.GetClobPair(ctx, clobtypes.ClobPairId(vaultId.Number)) + if !exists { + return clobPair, perpetual, marketParam, marketPrice, errorsmod.Wrap( + types.ErrClobPairNotFound, + fmt.Sprintf("VaultId: %v", vaultId), + ) + } + perpId := clobPair.Metadata.(*clobtypes.ClobPair_PerpetualClobMetadata).PerpetualClobMetadata.PerpetualId + perpetual, err = k.perpetualsKeeper.GetPerpetual(ctx, perpId) + if err != nil { + return clobPair, perpetual, marketParam, marketPrice, errorsmod.Wrap( + err, + fmt.Sprintf("VaultId: %v", vaultId), + ) + } + marketParam, exists = k.pricesKeeper.GetMarketParam(ctx, perpetual.Params.MarketId) + if !exists { + return clobPair, perpetual, marketParam, marketPrice, errorsmod.Wrap( + types.ErrMarketParamNotFound, + fmt.Sprintf("VaultId: %v", vaultId), + ) + } + marketPrice, err = k.pricesKeeper.GetMarketPrice(ctx, perpetual.Params.MarketId) + if err != nil { + return clobPair, perpetual, marketParam, marketPrice, errorsmod.Wrap( + err, + fmt.Sprintf("VaultId: %v", vaultId), + ) + } + + return clobPair, perpetual, marketParam, marketPrice, nil +} + // DecommissionVaults decommissions all deactivated vaults that have non-positive equities. func (k Keeper) DecommissionNonPositiveEquityVaults( ctx sdk.Context, diff --git a/protocol/x/vault/keeper/withdraw.go b/protocol/x/vault/keeper/withdraw.go new file mode 100644 index 0000000000..ac5ab5f1cd --- /dev/null +++ b/protocol/x/vault/keeper/withdraw.go @@ -0,0 +1,119 @@ +package keeper + +import ( + "math/big" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/dydxprotocol/v4-chain/protocol/lib" + "github.com/dydxprotocol/v4-chain/protocol/lib/vault" + "github.com/dydxprotocol/v4-chain/protocol/x/vault/types" +) + +// GetVaultWithdrawalSlippagePpm returns the slippage that should be incurred from the specified +// vault on withdrawing `sharesToWithdraw` shares. +// For example, if `sharesToWithdraw = 100` and `0.2` is returned, it means that withdrawing +// 100 shares has a 20% slippage for the given `vaultId`. +// +// Slippage is calculated as `min(simple_slippage, estimated_slippage)` where: +// - simple_slippage = leverage * initial_margin +// - estimated_slippage = spread * (1 + average_skew) * leverage +// - average_skew = integral / (posterior_leverage - leverage) +// - integral = skew_antiderivative(skew_factor, posterior_leverage) - +// skew_antiderivative(skew_factor, leverage) +// - posterior_leverage = leverage / (1 - withdrawal_portion) +// = leverage / (1 - shares_to_withdraw / total_shares) +// = leverage * total_shares / (total_shares - shares_to_withdraw) +// +// To simplify above formula, let l = leverage, n = total_shares, m = shares_to_withdraw +// +// estimated_slippage +// = spread * (1 + integral / (posterior_leverage - leverage)) * leverage +// = spread * (1 + integral * (n - m) / (ln - l(n - m))) * l +// = spread * (1 + integral * (n - m) / lm) * l +// = spread * (l + integral * (n - m) / m) +func (k Keeper) GetVaultWithdrawalSlippage( + ctx sdk.Context, + vaultId types.VaultId, + sharesToWithdraw *big.Int, +) (*big.Rat, error) { + totalShares := k.GetTotalShares(ctx).NumShares.BigInt() + if sharesToWithdraw.Sign() <= 0 || sharesToWithdraw.Cmp(totalShares) > 0 { + return nil, errorsmod.Wrapf( + types.ErrInvalidSharesToWithdraw, + "sharesToWithdraw: %s, totalShares: %s", + sharesToWithdraw, + totalShares, + ) + } + + quotingParams, exists := k.GetVaultQuotingParams(ctx, vaultId) + if !exists { + return nil, types.ErrVaultParamsNotFound + } + + _, perpetual, marketParam, marketPrice, err := k.GetVaultClobPerpAndMarket(ctx, vaultId) + if err != nil { + return nil, err + } + + // Get vault leverage. + leverage, _, err := k.GetVaultLeverageAndEquity(ctx, vaultId, perpetual, marketPrice) + if err != nil { + return nil, err + } + // No leverage, no slippage. + if leverage.Sign() == 0 { + return lib.BigRat0(), nil + } + + // Use absolute value of leverage. + leverage.Abs(leverage) + + // Calculate simple_slippage = leverage * initial_margin. + lt, err := k.perpetualsKeeper.GetLiquidityTier(ctx, perpetual.Params.LiquidityTier) + if err != nil { + return nil, err + } + simpleSlippage := lib.BigRatMulPpm(leverage, lt.InitialMarginPpm) + + // Return simple slippage if withdrawing 100%. + if sharesToWithdraw.Cmp(totalShares) == 0 { + return simpleSlippage, nil + } + + // Calculate estimated_slippage. + // 1. leverage_after_withdrawal + // = leverage / (1 - withdrawal_portion) + // = leverage * total_shares / (total_shares - shares_to_withdraw) + remainingShares := new(big.Int).Sub(totalShares, sharesToWithdraw) + posteriorLeverage := new(big.Rat).Mul( + leverage, + new(big.Rat).SetFrac(totalShares, remainingShares), + ) + + // 2. integral = skew_antiderivative(skew_factor, posterior_leverage) - skew_antiderivative(skew_factor, leverage) + integral := vault.SkewAntiderivative(quotingParams.SkewFactorPpm, posteriorLeverage) + integral.Sub(integral, vault.SkewAntiderivative(quotingParams.SkewFactorPpm, leverage)) + + // 3. estimated_slippage + // = spread * (l + integral * (n - m) / m) + estimatedSlippage := new(big.Rat).Mul( + integral, + new(big.Rat).SetFrac(remainingShares, sharesToWithdraw), + ) + estimatedSlippage.Add( + estimatedSlippage, + leverage, + ) + estimatedSlippage = lib.BigRatMulPpm( + estimatedSlippage, + vault.SpreadPpm("ingParams, &marketParam), + ) + + // Return min(simple_slippage, estimated_slippage). + return lib.BigRatMin( + simpleSlippage, + estimatedSlippage, + ), nil +} diff --git a/protocol/x/vault/keeper/withdraw_test.go b/protocol/x/vault/keeper/withdraw_test.go new file mode 100644 index 0000000000..f41e69fb26 --- /dev/null +++ b/protocol/x/vault/keeper/withdraw_test.go @@ -0,0 +1,524 @@ +package keeper_test + +import ( + "math/big" + "testing" + + "github.com/cometbft/cometbft/types" + "github.com/dydxprotocol/v4-chain/protocol/dtypes" + testapp "github.com/dydxprotocol/v4-chain/protocol/testutil/app" + "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" + testutil "github.com/dydxprotocol/v4-chain/protocol/testutil/util" + assettypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types" + clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" + perptypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" + pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" + satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" + vaulttypes "github.com/dydxprotocol/v4-chain/protocol/x/vault/types" + "github.com/stretchr/testify/require" +) + +func TestGetVaultWithdrawalSlippage(t *testing.T) { + testVaultId := constants.Vault_Clob1 + testClobPair := constants.ClobPair_Eth + testPerpetual := constants.EthUsd_20PercentInitial_10PercentMaintenance + testMarketParam := constants.TestMarketParams[1] + testMarketPrice := constants.TestMarketPrices[1] + + tests := map[string]struct { + /* --- Setup --- */ + // skew. + skewFactorPpm uint32 + // spread. + spreadMinPpm uint32 + spreadBufferPpm uint32 + minPriceChangePpm uint32 + // leverage. + assetQuoteQuantums *big.Int + positionBaseQuantums *big.Int + // total shares. + totalShares *big.Int + // function input. + vaultId vaulttypes.VaultId + sharesToWithdraw *big.Int + /* --- Expectations --- */ + expectedSlippage *big.Rat + expectedErr string + }{ + "Success: leverage 0, skew 2, spread 0.003, withdraw 10%": { + skewFactorPpm: 2_000_000, + spreadMinPpm: 2_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_500, + assetQuoteQuantums: big.NewInt(1_000_000_000), // 1,000 USDC + positionBaseQuantums: big.NewInt(0), + vaultId: testVaultId, + sharesToWithdraw: big.NewInt(10), + totalShares: big.NewInt(100), + // no slippage when leverage is 0. + expectedSlippage: big.NewRat(0, 1), + }, + "Success: leverage 0.00003, skew 3, spread 0.005, withdraw 9_999_999 out of 10_000_000 shares": { + skewFactorPpm: 3_000_000, + spreadMinPpm: 5_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_500, + assetQuoteQuantums: big.NewInt(999_970_000), + positionBaseQuantums: big.NewInt(10_000), // 1 ETH + vaultId: testVaultId, + sharesToWithdraw: big.NewInt(9_999_999), + totalShares: big.NewInt(10_000_000), + // open_notional = 10_000 * 10^-9 * 3_000 * 10^6 = 30_000 + // leverage = 30_000 / (999_700_000 + 3_000_000) = 0.00003 + // posterior_leverage = 0.00003 * 10_000_000 / (10_000_000 - 9_999_999) = 300 + // integral + // = skew_antiderivative(skew_factor, posterior_leverage) - skew_antiderivative(skew_factor, leverage) + // = 3 * 300^2 + 3^2 * 300^3 / 3 - (3 * 1.5^2 + 3^2 * 1.5^3 / 3) + // ~= 81_270_000 + // estimated_slippage + // = spread * (leverage + integral * (total_shares - shares_to_withdraw) / shares_to_withdraw) + // = 0.005 * (0.00003 + 81_270_000 * 1 / 9_999_999) + // ~= 0.0406352 + // slippage = min(0.0406352, leverage * imf) + // = min(0.0406352, 0.00003 * 0.2) = 0.000006 + expectedSlippage: big.NewRat(6, 1_000_000), + }, + "Success: leverage 0.000003, skew 3, spread 0.005, withdraw 9_999_999 out of 10_000_000 shares": { + skewFactorPpm: 3_000_000, + spreadMinPpm: 5_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_500, + assetQuoteQuantums: big.NewInt(999_997_000), + positionBaseQuantums: big.NewInt(1_000), // 1 ETH + vaultId: testVaultId, + sharesToWithdraw: big.NewInt(9_999_999), + totalShares: big.NewInt(10_000_000), + // open_notional = 1_000 * 10^-9 * 3_000 * 10^6 = 3_000 + // leverage = 3_000 / (999_997_000 + 3_000) = 0.000003 + // posterior_leverage = 0.000003 * 10_000_000 / (10_000_000 - 9_999_999) = 30 + // integral + // = skew_antiderivative(skew_factor, posterior_leverage) - skew_antiderivative(skew_factor, leverage) + // = 3 * 30^2 + 3^2 * 30^3 / 3 - (3 * 1.5^2 + 3^2 * 1.5^3 / 3) + // ~= 83_700 + // estimated_slippage + // = spread * (leverage + integral * (total_shares - shares_to_withdraw) / shares_to_withdraw) + // = 0.005 * (0.000003 + 83_700 * 1 / 9_999_999) + // ~= 0.000041865 + // slippage = min(0.000041865, leverage * imf) + // = min(0.000041865, 0.000003 * 0.2) + // = 0.0000006 + expectedSlippage: big.NewRat(3, 5_000_000), + }, + "Success: leverage 0.5, skew 2, spread 0.003, withdraw 10%": { + skewFactorPpm: 2_000_000, + spreadMinPpm: 2_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_500, + assetQuoteQuantums: big.NewInt(3_000_000_000), // 3,000 USDC + positionBaseQuantums: big.NewInt(1_000_000_000), // 1 ETH + vaultId: testVaultId, + sharesToWithdraw: big.NewInt(100_000), + totalShares: big.NewInt(1_000_000), + // open_notional = 1_000_000_000 * 10^-9 * 3_000 * 10^6 = 3_000_000_000 + // leverage = 3_000_000_000 / (3_000_000_000 + 3_000_000_000) = 0.5 + // posterior_leverage = 0.5 * 1_000_000 / (1_000_000 - 100_000) = 5 / 9 + // integral + // = skew_antiderivative(skew_factor, posterior_leverage) - skew_antiderivative(skew_factor, leverage) + // = 2 * (5/9)^2 + 2^2 * (5/9)^3 / 3 - (2 * 0.5^2 + 2^2 * 0.5^3 / 3) + // = 392/2187 + // estimated_slippage + // = spread * (leverage + integral * (total_shares - shares_to_withdraw) / shares_to_withdraw) + // = 0.003 * (0.5 + 392/2187 * 900_000 / 100_000) + // = 1027 / 162000 + // slippage = min(0.010565935, leverage * imf) + // = min(1027 / 162000, 0.5 * 0.2) = 1027 / 162000 + expectedSlippage: big.NewRat(1_027, 162_000), + }, + "Success: leverage 1.5, skew 2, spread 0.003, withdraw 0.0001%": { + skewFactorPpm: 2_000_000, + spreadMinPpm: 2_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_500, + assetQuoteQuantums: big.NewInt(-1_000_000_000), // -1,000 USDC + positionBaseQuantums: big.NewInt(1_000_000_000), // 1 ETH + vaultId: testVaultId, + sharesToWithdraw: big.NewInt(1), + totalShares: big.NewInt(1_000_000), + // open_notional = 1_000_000_000 * 10^-9 * 3_000 * 10^6 = 3_000_000_000 + // leverage = 3_000_000_000 / (-1_000_000_000 + 3_000_000_000) = 1.5 + // posterior_leverage = 1.5 * 1_000_000 / (1_000_000 - 1) = 500_000/333_333 + // integral + // = skew_antiderivative(skew_factor, posterior_leverage) - skew_antiderivative(skew_factor, leverage) + // = 2 * (500_000/333_333)^2 + 2^2 * (500_000/333_333)^3 / 3 - (2 * 1.5^2 + 2^2 * 1.5^3 / 3) + // = 2499997000001 / 111110777778111111 + // estimated_slippage + // = spread * (leverage + integral * (total_shares - shares_to_withdraw) / shares_to_withdraw) + // = 0.003 * (1.5 + 2499997000001 / 111110777778111111 * 999_999 / 1) + // = 5333326666669 / 74073925926000 + // slippage = min(0.100499, leverage * imf) + // = min(5333326666669 / 74073925926000, 1.5 * 0.2) = 5333326666669 / 74073925926000 + // ~= 0.072000054 + expectedSlippage: big.NewRat(5333326666669, 74073925926000), + }, + "Success: leverage 1.5, skew 3, spread 0.003, withdraw 10%": { + skewFactorPpm: 3_000_000, + spreadMinPpm: 2_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_500, + assetQuoteQuantums: big.NewInt(-1_000_000_000), // -1,000 USDC + positionBaseQuantums: big.NewInt(1_000_000_000), // 1 ETH + vaultId: testVaultId, + sharesToWithdraw: big.NewInt(100_000), + totalShares: big.NewInt(1_000_000), + // open_notional = 1_000_000_000 * 10^-9 * 3_000 * 10^6 = 3_000_000_000 + // leverage = 3_000_000_000 / (-1_000_000_000 + 3_000_000_000) = 1.5 + // posterior_leverage = 1.5 * 1_000_000 / (1_000_000 - 100_000) = 5/3 + // integral + // = skew_antiderivative(skew_factor, posterior_leverage) - skew_antiderivative(skew_factor, leverage) + // = 3 * (5/3)^2 + 3^2 * (5/3)^3 / 3 - (3 * 1.5^2 + 3^2 * 1.5^3 / 3) + // = 385/72 + // estimated_slippage + // = spread * (leverage + integral * (total_shares - shares_to_withdraw) / shares_to_withdraw) + // = 0.003 * (1.5 + 385/72 * 900_000 / 100_000) + // = 1191/8000 + // slippage = min(1191/8000, leverage * imf) + // = min(1191/8000, 1.5 * 0.2) + // ~= 0.148875 + expectedSlippage: big.NewRat(1_191, 8_000), + }, + "Success: leverage 1.5, skew 3, spread 0.003, withdraw 50%": { + skewFactorPpm: 3_000_000, + spreadMinPpm: 2_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_500, + assetQuoteQuantums: big.NewInt(-1_000_000_000), // -1,000 USDC + positionBaseQuantums: big.NewInt(1_000_000_000), // 1 ETH + vaultId: testVaultId, + sharesToWithdraw: big.NewInt(500_000), + totalShares: big.NewInt(1_000_000), + // open_notional = 1_000_000_000 * 10^-9 * 3_000 * 10^6 = 3_000_000_000 + // leverage = 3_000_000_000 / (-1_000_000_000 + 3_000_000_000) = 1.5 + // posterior_leverage = 1.5 * 1_000_000 / (1_000_000 - 500_000) = 3 + // integral + // = skew_antiderivative(skew_factor, posterior_leverage) - skew_antiderivative(skew_factor, leverage) + // = 3 * 3^2 + 3^2 * 3^3 / 3 - (3 * 1.5^2 + 3^2 * 1.5^3 / 3) + // = 729/8 + // estimated_slippage + // = spread * (leverage + integral * (total_shares - shares_to_withdraw) / shares_to_withdraw) + // = 0.003 * (1.5 + 729/8 * (1_000_000 - 500_000) / 500_000) + // = 2223/8000 + // slippage = min(441/800, leverage * imf) + // = min(2223/8000, 1.5 * 0.2) = 2223/8000 + // = 0.277875 + expectedSlippage: big.NewRat(2_223, 8_000), + }, + "Success: leverage -1.5, skew 3, spread 0.003, withdraw 50%, slippage is same as when leverage is 1.5": { + skewFactorPpm: 3_000_000, + spreadMinPpm: 2_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_500, + assetQuoteQuantums: big.NewInt(5_000_000_000), // 5,000 USDC + positionBaseQuantums: big.NewInt(-1_000_000_000), // -1 ETH + vaultId: testVaultId, + sharesToWithdraw: big.NewInt(1_111), + totalShares: big.NewInt(2_222), + // open_notional = -1_000_000_000 * 10^-9 * 3_000 * 10^6 = -3_000_000_000 + // |leverage| = |-3_000_000_000 / (5_000_000_000 + -3_000_000_000)| = |-1.5| = 1.5 + // posterior_leverage = 1.5 * 2_222 / (2_222 - 1_111) = 3 + // integral + // = skew_antiderivative(skew_factor, posterior_leverage) - skew_antiderivative(skew_factor, leverage) + // = 3 * 3^2 + 3^2 * 3^3 / 3 - (3 * 1.5^2 + 3^2 * 1.5^3 / 3) + // = 729/8 + // estimated_slippage + // = spread * (leverage + integral * (total_shares - shares_to_withdraw) / shares_to_withdraw) + // = 0.003 * (1.5 + 729/8 * (2_222 - 1_111) / 1_111) + // = 2223/8000 + // slippage = min(441/800, leverage * imf) + // = min(2223/8000, 1.5 * 0.2) = 2223/8000 + // = 0.277875 + expectedSlippage: big.NewRat(2_223, 8_000), + }, + "Success: leverage 1.5, skew 3, spread 0.005, withdraw 50%": { + skewFactorPpm: 3_000_000, + spreadMinPpm: 5_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_500, + assetQuoteQuantums: big.NewInt(-1_000_000_000), // -1,000 USDC + positionBaseQuantums: big.NewInt(1_000_000_000), // 1 ETH + vaultId: testVaultId, + sharesToWithdraw: big.NewInt(2_345_678), + totalShares: big.NewInt(4_691_356), + // open_notional = 1_000_000_000 * 10^-9 * 3_000 * 10^6 = 3_000_000_000 + // leverage = 3_000_000_000 / (-1_000_000_000 + 3_000_000_000) = 1.5 + // posterior_leverage = 1.5 * 4_691_356 / (4_691_356 - 2_345_678) = 3 + // integral + // = skew_antiderivative(skew_factor, posterior_leverage) - skew_antiderivative(skew_factor, leverage) + // = 3 * 3^2 + 3^2 * 3^3 / 3 - (3 * 1.5^2 + 3^2 * 1.5^3 / 3) + // = 729/8 + // estimated_slippage + // = spread * (leverage + integral * (total_shares - shares_to_withdraw) / shares_to_withdraw) + // = 0.005 * (1.5 + 729/8 * (4_691_356 - 2_345_678) / 2_345_678) + // = 741/1600 + // slippage = min(741/1600, leverage * imf) + // = min(741/1600, 1.5 * 0.2) = 0.3 + expectedSlippage: big.NewRat(3, 10), + }, + "Success: leverage 1.5, skew 3, spread 0.005, withdraw 100%": { + skewFactorPpm: 3_000_000, + spreadMinPpm: 5_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_500, + assetQuoteQuantums: big.NewInt(-1_000_000_000), // -1,000 USDC + positionBaseQuantums: big.NewInt(1_000_000_000), // 1 ETH + vaultId: testVaultId, + sharesToWithdraw: big.NewInt(2_345_678), + totalShares: big.NewInt(2_345_678), + // open_notional = 1_000_000_000 * 10^-9 * 3_000 * 10^6 = 3_000_000_000 + // leverage = 3_000_000_000 / (-1_000_000_000 + 3_000_000_000) = 1.5 + // slippage = leverage * imf = 1.5 * 0.2 = 0.3 + expectedSlippage: big.NewRat(3, 10), + }, + "Success: leverage 3, skew 2, spread 0.003, withdraw 1 out of 10 million shares": { + skewFactorPpm: 2_000_000, + spreadMinPpm: 2_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_500, + assetQuoteQuantums: big.NewInt(-2_000_000_000), // -2,000 USDC + positionBaseQuantums: big.NewInt(1_000_000_000), // 1 ETH + vaultId: testVaultId, + sharesToWithdraw: big.NewInt(1), + totalShares: big.NewInt(10_000_000), + // open_notional = 1_000_000_000 * 10^-9 * 3_000 * 10^6 = 3_000_000_000 + // leverage = 3_000_000_000 / (-2_000_000_000 + 3_000_000_000) = 3 + // posterior_leverage = 3 * 10_000_000 / (10_000_000 - 1) = 3.0000003 + // integral + // = skew_antiderivative(skew_factor, posterior_leverage) - skew_antiderivative(skew_factor, leverage) + // = 2 * 3.0000003^2 + 2^2 * 3.0000003^3 / 3 - (2 * 3^2 + 2^2 * 3^3 / 3) + // = 144_000_013/10_000_000_000_000 + // estimated_slippage + // = spread * (leverage + integral * (total_shares - shares_to_withdraw) / shares_to_withdraw) + // = 0.003 * (3 + 144000013/10000000000000 * (10_000_000 - 1) / 1) + // = 1633333146666673 / 3703702962963000 + // slippage = min(1633333146666673 / 3703702962963000, leverage * imf) + // = min(1633333146666673 / 3703702962963000, 3 * 0.2) = 1633333146666673 / 3703702962963000 + // = 0.4410000378 + expectedSlippage: big.NewRat(1_633_333_146_666_673, 3_703_702_962_963_000), + }, + "Success: leverage 3, skew 2, spread 0.003, withdraw 1234 out of 12345 shares": { + skewFactorPpm: 2_000_000, + spreadMinPpm: 3_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_000, + assetQuoteQuantums: big.NewInt(-2_000_000_000), // -2,000 USDC + positionBaseQuantums: big.NewInt(1_000_000_000), // 1 ETH + vaultId: testVaultId, + sharesToWithdraw: big.NewInt(1_234), + totalShares: big.NewInt(12_345), + // open_notional = 1_000_000_000 * 10^-9 * 3_000 * 10^6 = 3_000_000_000 + // leverage = 3_000_000_000 / (-2_000_000_000 + 3_000_000_000) = 3 + // posterior_leverage = 3 * 12345 / (12345 - 1234) = 37035/11111 + // integral + // = skew_antiderivative(skew_factor, posterior_leverage) - skew_antiderivative(skew_factor, leverage) + // = 2 * (37035/11111)^2 + 2^2 * (37035/11111)^3 / 3 - (2 * 3^2 + 2^2 * 3^3 / 3) + // ~= 17.59627186 + // estimated_slippage + // = spread * (leverage + integral * (total_shares - shares_to_withdraw) / shares_to_withdraw) + // = 0.003 * (3 + 17.59627186 * (12345 - 1234) / 1234) + // = 59_790_561_381 / 123_454_321_000 + // slippage = min(59_790_561_381 / 123_454_321_000, leverage * imf) + // = min(59_790_561_381 / 123_454_321_000, 3 * 0.2) + // = 59_790_561_381 / 123_454_321_000 + // ~= 0.484313 + expectedSlippage: big.NewRat(59_790_561_381, 123_454_321_000), + }, + "Success: leverage 3, skew 2, spread 0.003, withdraw 50%": { + skewFactorPpm: 2_000_000, + spreadMinPpm: 2_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_500, + assetQuoteQuantums: big.NewInt(-2_000_000_000), // -2,000 USDC + positionBaseQuantums: big.NewInt(1_000_000_000), // 1 ETH + vaultId: testVaultId, + sharesToWithdraw: big.NewInt(222_222), + totalShares: big.NewInt(444_444), + // open_notional = 1_000_000_000 * 10^-9 * 3_000 * 10^6 = 3_000_000_000 + // leverage = 3_000_000_000 / (-2_000_000_000 + 3_000_000_000) = 3 + // posterior_leverage = 3 * 444_444 / (444_444 - 222_222) = 6 + // integral + // = skew_antiderivative(skew_factor, posterior_leverage) - skew_antiderivative(skew_factor, leverage) + // = 2 * 6^2 + 2^2 * 6^3 / 3 - (2 * 3^2 + 2^2 * 3^3 / 3) + // = 360 - 54 + // = 306 + // estimated_slippage + // = spread * (leverage + integral * (total_shares - shares_to_withdraw) / shares_to_withdraw) + // = 0.003 * (3 + 306 * (444_444 - 222_222) / 222_222) + // = 927/1000 + // slippage = min(927/1000, leverage * imf) + // = min(927/1000, 3 * 0.2) + // = 0.6 + expectedSlippage: big.NewRat(3, 5), + }, + "Success: leverage 3, skew 2, spread 0.003, withdraw 99.9999%": { + skewFactorPpm: 2_000_000, + spreadMinPpm: 2_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_500, + assetQuoteQuantums: big.NewInt(-2_000_000_000), // -2,000 USDC + positionBaseQuantums: big.NewInt(1_000_000_000), // 1 ETH + vaultId: testVaultId, + sharesToWithdraw: big.NewInt(999_999), + totalShares: big.NewInt(1_000_000), + // open_notional = 1_000_000_000 * 10^-9 * 3_000 * 10^6 = 3_000_000_000 + // leverage = 3_000_000_000 / (-2_000_000_000 + 3_000_000_000) = 3 + // posterior_leverage = 3 * 1_000_000 / (1_000_000 - 999_999) = 3_000_000 + // integral + // = skew_antiderivative(skew_factor, posterior_leverage) - skew_antiderivative(skew_factor, leverage) + // = 2 * 3_000_000^2 + 2^2 * 3_000_000^3 / 3 - (2 * 3^2 + 2^2 * 3^3 / 3) + // = 36000018e12 + // estimated_slippage + // = spread * (leverage + integral * (total_shares - shares_to_withdraw) / shares_to_withdraw) + // = 0.003 * (3 + 36000018e12 * (1_000_000 - 999_999) / 999_999) + // = large number + // slippage = min(large number, leverage * imf) + // = min(large number, 3 * 0.2) + // = 0.6 + expectedSlippage: big.NewRat(3, 5), + }, + "Error: vault not found": { + skewFactorPpm: 2_000_000, + spreadMinPpm: 2_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_500, + assetQuoteQuantums: big.NewInt(-2_000_000_000), // -2,000 USDC + positionBaseQuantums: big.NewInt(1_000_000_000), // 1 ETH + vaultId: constants.Vault_Clob0, // non-existent vault + sharesToWithdraw: big.NewInt(10), + totalShares: big.NewInt(100), + expectedErr: vaulttypes.ErrVaultParamsNotFound.Error(), + }, + "Error: negative shares to withdraw": { + skewFactorPpm: 2_000_000, + spreadMinPpm: 2_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_500, + assetQuoteQuantums: big.NewInt(-2_000_000_000), // -2,000 USDC + positionBaseQuantums: big.NewInt(1_000_000_000), // 1 ETH + vaultId: testVaultId, + sharesToWithdraw: big.NewInt(-1), + totalShares: big.NewInt(100), + expectedErr: vaulttypes.ErrInvalidSharesToWithdraw.Error(), + }, + "Error: zero shares to withdraw": { + skewFactorPpm: 2_000_000, + spreadMinPpm: 2_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_500, + assetQuoteQuantums: big.NewInt(-2_000_000_000), // -2,000 USDC + positionBaseQuantums: big.NewInt(1_000_000_000), // 1 ETH + vaultId: testVaultId, + sharesToWithdraw: big.NewInt(0), + totalShares: big.NewInt(100), + expectedErr: vaulttypes.ErrInvalidSharesToWithdraw.Error(), + }, + "Error: shares to withdraw greater than total shares": { + skewFactorPpm: 2_000_000, + spreadMinPpm: 2_000, + spreadBufferPpm: 1_500, + minPriceChangePpm: 1_500, + assetQuoteQuantums: big.NewInt(-2_000_000_000), // -2,000 USDC + positionBaseQuantums: big.NewInt(1_000_000_000), // 1 ETH + vaultId: testVaultId, + sharesToWithdraw: big.NewInt(1_000_001), + totalShares: big.NewInt(1_000_000), + expectedErr: vaulttypes.ErrInvalidSharesToWithdraw.Error(), + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + tApp := testapp.NewTestAppBuilder(t).WithGenesisDocFn(func() (genesis types.GenesisDoc) { + genesis = testapp.DefaultGenesis() + // Set up vault's quoting params. + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *vaulttypes.GenesisState) { + quotingParams := vaulttypes.DefaultQuotingParams() + quotingParams.SkewFactorPpm = tc.skewFactorPpm + quotingParams.SpreadMinPpm = tc.spreadMinPpm + quotingParams.SpreadBufferPpm = tc.spreadBufferPpm + genesisState.Vaults = []vaulttypes.Vault{ + { + VaultId: testVaultId, + VaultParams: vaulttypes.VaultParams{ + Status: vaulttypes.VaultStatus_VAULT_STATUS_QUOTING, + QuotingParams: "ingParams, + }, + }, + } + genesisState.TotalShares = vaulttypes.BigIntToNumShares(tc.totalShares) + }, + ) + // Set up markets. + testMarketParam.MinPriceChangePpm = tc.minPriceChangePpm + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *pricestypes.GenesisState) { + genesisState.MarketParams = []pricestypes.MarketParam{testMarketParam} + genesisState.MarketPrices = []pricestypes.MarketPrice{testMarketPrice} + }, + ) + // Set up perpetuals. + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *perptypes.GenesisState) { + genesisState.LiquidityTiers = constants.LiquidityTiers + genesisState.Perpetuals = []perptypes.Perpetual{testPerpetual} + }, + ) + // Set up clob pairs. + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *clobtypes.GenesisState) { + genesisState.ClobPairs = []clobtypes.ClobPair{testClobPair} + }, + ) + // Set up vault asset and perpetual positions. + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *satypes.GenesisState) { + genesisState.Subaccounts = []satypes.Subaccount{ + { + Id: tc.vaultId.ToSubaccountId(), + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: assettypes.AssetUsdc.Id, + Quantums: dtypes.NewIntFromBigInt(tc.assetQuoteQuantums), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + testutil.CreateSinglePerpetualPosition( + testPerpetual.Params.Id, + tc.positionBaseQuantums, + big.NewInt(0), + big.NewInt(0), + ), + }, + }, + } + }, + ) + return genesis + }).Build() + ctx := tApp.InitChain() + k := tApp.App.VaultKeeper + + slippage, err := k.GetVaultWithdrawalSlippage(ctx, tc.vaultId, tc.sharesToWithdraw) + + if tc.expectedErr == "" { + require.NoError(t, err) + require.Equal(t, tc.expectedSlippage, slippage) + } else { + require.ErrorContains(t, err, tc.expectedErr) + } + }) + } +} diff --git a/protocol/x/vault/types/errors.go b/protocol/x/vault/types/errors.go index 45f0f5cee7..73d60253a3 100644 --- a/protocol/x/vault/types/errors.go +++ b/protocol/x/vault/types/errors.go @@ -10,7 +10,6 @@ var ( 1, "Shares are negative", ) - // Deprecated since v6.x ErrClobPairNotFound = errorsmod.Register( ModuleName, 2, @@ -126,4 +125,9 @@ var ( 24, "Empty operator address", ) + ErrInvalidSharesToWithdraw = errorsmod.Register( + ModuleName, + 25, + "Shares to withdraw must be positive and less than or equal to total shares", + ) ) diff --git a/protocol/x/vault/types/expected_keepers.go b/protocol/x/vault/types/expected_keepers.go index 904c9ede3f..4bc05f7f0f 100644 --- a/protocol/x/vault/types/expected_keepers.go +++ b/protocol/x/vault/types/expected_keepers.go @@ -46,6 +46,10 @@ type PerpetualsKeeper interface { ctx sdk.Context, id uint32, ) (val perptypes.Perpetual, err error) + GetLiquidityTier( + ctx sdk.Context, + id uint32, + ) (val perptypes.LiquidityTier, err error) } type PricesKeeper interface {