From 1506ca8e4c937d90a1ff2f305e3396477d1741f0 Mon Sep 17 00:00:00 2001 From: Julian Compagni Portis Date: Wed, 3 Jul 2024 17:46:18 -0400 Subject: [PATCH 1/5] split dex message execution and bank / events opertations this makes it much easier to support simulation queries for all messages --- x/dex/keeper/core.go | 526 ++++++++++++++++++++-------------- x/dex/keeper/multihop_swap.go | 7 + 2 files changed, 320 insertions(+), 213 deletions(-) diff --git a/x/dex/keeper/core.go b/x/dex/keeper/core.go index 04e0d29fa..33753bf94 100644 --- a/x/dex/keeper/core.go +++ b/x/dex/keeper/core.go @@ -34,8 +34,50 @@ func (k Keeper) DepositCore( ) (amounts0Deposit, amounts1Deposit []math.Int, sharesIssued sdk.Coins, failedDeposits []*types.FailedDeposit, err error) { ctx := sdk.UnwrapSDKContext(goCtx) - totalAmountReserve0 := math.ZeroInt() - totalAmountReserve1 := math.ZeroInt() + amounts0Deposited, + amounts1Deposited, + totalAmountReserve0, + totalAmountReserve1, + sharesIssued, + failedDeposits, + err := k.CalculateDeposit(ctx, pairID, callerAddr, receiverAddr, amounts0, amounts1, tickIndices, fees, options) + if err != nil { + return nil, nil, nil, failedDeposits, err + } + + if totalAmountReserve0.IsPositive() { + coin0 := sdk.NewCoin(pairID.Token0, totalAmountReserve0) + if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, callerAddr, types.ModuleName, sdk.Coins{coin0}); err != nil { + return nil, nil, nil, nil, err + } + } + + if totalAmountReserve1.IsPositive() { + coin1 := sdk.NewCoin(pairID.Token1, totalAmountReserve1) + if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, callerAddr, types.ModuleName, sdk.Coins{coin1}); err != nil { + return nil, nil, nil, nil, err + } + } + + if err := k.MintShares(ctx, receiverAddr, sharesIssued); err != nil { + return nil, nil, nil, nil, err + } + + return amounts0Deposited, amounts1Deposited, sharesIssued, failedDeposits, nil +} + +func (k Keeper) CalculateDeposit( + ctx sdk.Context, + pairID *types.PairID, + callerAddr sdk.AccAddress, + receiverAddr sdk.AccAddress, + amounts0 []math.Int, + amounts1 []math.Int, + tickIndices []int64, + fees []uint64, + options []*types.DepositOptions) (amounts0Deposit, amounts1Deposit []math.Int, totalAmountReserve0, totalAmountReserve1 math.Int, sharesIssued sdk.Coins, failedDeposits []*types.FailedDeposit, err error) { + totalAmountReserve0 = math.ZeroInt() + totalAmountReserve1 = math.ZeroInt() amounts0Deposited := make([]math.Int, len(amounts0)) amounts1Deposited := make([]math.Int, len(amounts1)) sharesIssued = sdk.Coins{} @@ -56,14 +98,14 @@ func (k Keeper) DepositCore( autoswap := !option.DisableAutoswap if err := k.ValidateFee(ctx, fee); err != nil { - return nil, nil, nil, failedDeposits, err + return nil, nil, math.ZeroInt(), math.ZeroInt(), nil, nil, err } if k.IsPoolBehindEnemyLines(ctx, pairID, tickIndex, fee, amount0, amount1) { err = sdkerrors.Wrapf(types.ErrDepositBehindEnemyLines, "deposit failed at tick %d fee %d", tickIndex, fee) if option.FailTxOnBel { - return nil, nil, nil, failedDeposits, err + return nil, nil, math.ZeroInt(), math.ZeroInt(), nil, nil, err } failedDeposits = append(failedDeposits, &types.FailedDeposit{DepositIdx: uint64(i), Error: err.Error()}) continue @@ -76,7 +118,7 @@ func (k Keeper) DepositCore( fee, ) if err != nil { - return nil, nil, nil, failedDeposits, err + return nil, nil, math.ZeroInt(), math.ZeroInt(), nil, nil, err } existingShares := k.bankKeeper.GetSupply(ctx, pool.GetPoolDenom()).Amount @@ -86,11 +128,12 @@ func (k Keeper) DepositCore( k.SetPool(ctx, pool) if inAmount0.IsZero() && inAmount1.IsZero() { - return nil, nil, nil, failedDeposits, types.ErrZeroTrueDeposit + return nil, nil, math.ZeroInt(), math.ZeroInt(), nil, nil, types.ErrZeroTrueDeposit } if outShares.IsZero() { - return nil, nil, nil, failedDeposits, types.ErrDepositShareUnderflow + + return nil, nil, math.ZeroInt(), math.ZeroInt(), nil, nil, types.ErrDepositShareUnderflow } sharesIssued = append(sharesIssued, outShares) @@ -100,6 +143,7 @@ func (k Keeper) DepositCore( totalAmountReserve0 = totalAmountReserve0.Add(inAmount0) totalAmountReserve1 = totalAmountReserve1.Add(inAmount1) + //TODO: probably don't emit events here ctx.EventManager().EmitEvent(types.CreateDepositEvent( callerAddr, receiverAddr, @@ -116,26 +160,7 @@ func (k Keeper) DepositCore( // At this point shares issued is not sorted and may have duplicates // we must sanitize to convert it to a valid set of coins sharesIssued = utils.SanitizeCoins(sharesIssued) - - if totalAmountReserve0.IsPositive() { - coin0 := sdk.NewCoin(pairID.Token0, totalAmountReserve0) - if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, callerAddr, types.ModuleName, sdk.Coins{coin0}); err != nil { - return nil, nil, nil, failedDeposits, err - } - } - - if totalAmountReserve1.IsPositive() { - coin1 := sdk.NewCoin(pairID.Token1, totalAmountReserve1) - if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, callerAddr, types.ModuleName, sdk.Coins{coin1}); err != nil { - return nil, nil, nil, failedDeposits, err - } - } - - if err := k.MintShares(ctx, receiverAddr, sharesIssued); err != nil { - return nil, nil, nil, failedDeposits, err - } - - return amounts0Deposited, amounts1Deposited, sharesIssued, failedDeposits, nil + return amounts0Deposit, amounts1Deposit, totalAmountReserve0, totalAmountReserve1, sharesIssued, failedDeposits, nil } // Handles core logic for MsgWithdrawal; calculating and withdrawing reserve0,reserve1 from a specified tick @@ -152,6 +177,56 @@ func (k Keeper) WithdrawCore( fees []uint64, ) error { ctx := sdk.UnwrapSDKContext(goCtx) + + totalReserve0ToRemove, totalReserve1ToRemove, events, err := k.CalculateWithdraw(ctx, pairID, callerAddr, receiverAddr, sharesToRemoveList, tickIndicesNormalized, fees) + if err != nil { + return err + } + + ctx.EventManager().EmitEvents(events) + + if totalReserve0ToRemove.IsPositive() { + coin0 := sdk.NewCoin(pairID.Token0, totalReserve0ToRemove) + + err := k.bankKeeper.SendCoinsFromModuleToAccount( + ctx, + types.ModuleName, + receiverAddr, + sdk.Coins{coin0}, + ) + ctx.EventManager().EmitEvents(types.GetEventsWithdrawnAmount(sdk.Coins{coin0})) + if err != nil { + return err + } + } + + // sends totalReserve1ToRemove to receiverAddr + if totalReserve1ToRemove.IsPositive() { + coin1 := sdk.NewCoin(pairID.Token1, totalReserve1ToRemove) + err := k.bankKeeper.SendCoinsFromModuleToAccount( + ctx, + types.ModuleName, + receiverAddr, + sdk.Coins{coin1}, + ) + ctx.EventManager().EmitEvents(types.GetEventsWithdrawnAmount(sdk.Coins{coin1})) + if err != nil { + return err + } + } + + return nil +} + +func (k Keeper) CalculateWithdraw( + ctx sdk.Context, + pairID *types.PairID, + callerAddr sdk.AccAddress, + receiverAddr sdk.AccAddress, + sharesToRemoveList []math.Int, + tickIndicesNormalized []int64, + fees []uint64, +) (totalReserves0ToRemove, totalReserves1ToRemove math.Int, events sdk.Events, err error) { totalReserve0ToRemove := math.ZeroInt() totalReserve1ToRemove := math.ZeroInt() @@ -161,14 +236,14 @@ func (k Keeper) WithdrawCore( pool, err := k.GetOrInitPool(ctx, pairID, tickIndex, fee) if err != nil { - return err + return math.ZeroInt(), math.ZeroInt(), nil, err } poolDenom := pool.GetPoolDenom() totalShares := k.bankKeeper.GetSupply(ctx, poolDenom).Amount if totalShares.LT(sharesToRemove) { - return sdkerrors.Wrapf( + return math.ZeroInt(), math.ZeroInt(), nil, sdkerrors.Wrapf( types.ErrInsufficientShares, "%s does not have %s shares of type %s", callerAddr, @@ -182,14 +257,14 @@ func (k Keeper) WithdrawCore( if sharesToRemove.IsPositive() { if err := k.BurnShares(ctx, callerAddr, sharesToRemove, poolDenom); err != nil { - return err + return math.ZeroInt(), math.ZeroInt(), nil, err } } totalReserve0ToRemove = totalReserve0ToRemove.Add(outAmount0) totalReserve1ToRemove = totalReserve1ToRemove.Add(outAmount1) - ctx.EventManager().EmitEvent(types.CreateWithdrawEvent( + event := types.CreateWithdrawEvent( callerAddr, receiverAddr, pairID.Token0, @@ -199,40 +274,10 @@ func (k Keeper) WithdrawCore( outAmount0, outAmount1, sharesToRemove, - )) - } - - if totalReserve0ToRemove.IsPositive() { - coin0 := sdk.NewCoin(pairID.Token0, totalReserve0ToRemove) - - err := k.bankKeeper.SendCoinsFromModuleToAccount( - ctx, - types.ModuleName, - receiverAddr, - sdk.Coins{coin0}, - ) - ctx.EventManager().EmitEvents(types.GetEventsWithdrawnAmount(sdk.Coins{coin0})) - if err != nil { - return err - } - } - - // sends totalReserve1ToRemove to receiverAddr - if totalReserve1ToRemove.IsPositive() { - coin1 := sdk.NewCoin(pairID.Token1, totalReserve1ToRemove) - err := k.bankKeeper.SendCoinsFromModuleToAccount( - ctx, - types.ModuleName, - receiverAddr, - sdk.Coins{coin1}, ) - ctx.EventManager().EmitEvents(types.GetEventsWithdrawnAmount(sdk.Coins{coin1})) - if err != nil { - return err - } + events = append(events, event) } - - return nil + return totalReserve0ToRemove, totalReserve1ToRemove, events, nil } func (k Keeper) MultiHopSwapCore( @@ -245,47 +290,9 @@ func (k Keeper) MultiHopSwapCore( receiverAddr sdk.AccAddress, ) (coinOut sdk.Coin, err error) { ctx := sdk.UnwrapSDKContext(goCtx) - var routeErrors []error - initialInCoin := sdk.NewCoin(routes[0].Hops[0], amountIn) - stepCache := make(map[multihopCacheKey]StepResult) - var bestRoute struct { - write func() - coinOut sdk.Coin - route []string - dust sdk.Coins - } - bestRoute.coinOut = sdk.Coin{Amount: math.ZeroInt()} - - for _, route := range routes { - routeDust, routeCoinOut, writeRoute, err := k.RunMultihopRoute( - ctx, - *route, - initialInCoin, - exitLimitPrice, - stepCache, - ) - if err != nil { - routeErrors = append(routeErrors, err) - continue - } - - if !pickBestRoute || bestRoute.coinOut.Amount.LT(routeCoinOut.Amount) { - bestRoute.coinOut = routeCoinOut - bestRoute.write = writeRoute - bestRoute.route = route.Hops - bestRoute.dust = routeDust - } - if !pickBestRoute { - break - } - } - - if len(routeErrors) == len(routes) { - // All routes have failed - - allErr := errors.Join(append([]error{types.ErrAllMultiHopRoutesFailed}, routeErrors...)...) - - return sdk.Coin{}, allErr + bestRoute, initialInCoin, err := k.CalculateMultiHopSwap(ctx, amountIn, routes, exitLimitPrice, pickBestRoute, callerAddr, receiverAddr) + if err != nil { + return sdk.Coin{}, err } bestRoute.write() @@ -325,6 +332,56 @@ func (k Keeper) MultiHopSwapCore( return bestRoute.coinOut, nil } +func (k Keeper) CalculateMultiHopSwap( + ctx sdk.Context, + amountIn math.Int, + routes []*types.MultiHopRoute, + exitLimitPrice math_utils.PrecDec, + pickBestRoute bool, + callerAddr sdk.AccAddress, + receiverAddr sdk.AccAddress, +) (bestRoute routeOutput, initialInCoin sdk.Coin, err error) { + var routeErrors []error + initialInCoin = sdk.NewCoin(routes[0].Hops[0], amountIn) + stepCache := make(map[multihopCacheKey]StepResult) + + bestRoute.coinOut = sdk.Coin{Amount: math.ZeroInt()} + + for _, route := range routes { + routeDust, routeCoinOut, writeRoute, err := k.RunMultihopRoute( + ctx, + *route, + initialInCoin, + exitLimitPrice, + stepCache, + ) + if err != nil { + routeErrors = append(routeErrors, err) + continue + } + + if !pickBestRoute || bestRoute.coinOut.Amount.LT(routeCoinOut.Amount) { + bestRoute.coinOut = routeCoinOut + bestRoute.write = writeRoute + bestRoute.route = route.Hops + bestRoute.dust = routeDust + } + if !pickBestRoute { + break + } + } + + if len(routeErrors) == len(routes) { + // All routes have failed + + allErr := errors.Join(append([]error{types.ErrAllMultiHopRoutesFailed}, routeErrors...)...) + + return routeOutput{}, sdk.Coin{}, allErr + } + + return bestRoute, initialInCoin, nil +} + // PlaceLimitOrderCore handles MsgPlaceLimitOrder, initializing (tick, pair) data structures if needed, calculating and // storing information for a new limit order at a specific tick. func (k Keeper) PlaceLimitOrderCore( @@ -341,26 +398,83 @@ func (k Keeper) PlaceLimitOrderCore( ) (trancheKey string, totalInCoin, swapInCoin, swapOutCoin sdk.Coin, err error) { ctx := sdk.UnwrapSDKContext(goCtx) - var pairID *types.PairID - pairID, err = types.NewPairIDFromUnsorted(tokenIn, tokenOut) + takerTradePairID, err := types.NewTradePairID(tokenIn, tokenOut) + if err != nil { + return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err + } + trancheKey, totalIn, swapInCoin, swapOutCoin, sharesIssued, err := k.CalculatePlaceLimitOrder(ctx, takerTradePairID, amountIn, tickIndexInToOut, orderType, goodTil, maxAmountOut, receiverAddr) if err != nil { return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err } + if swapOutCoin.IsPositive() { + err = k.bankKeeper.SendCoinsFromModuleToAccount( + ctx, + types.ModuleName, + receiverAddr, + sdk.Coins{swapOutCoin}, + ) + if err != nil { + return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err + } + } + + if totalIn.IsPositive() { + totalInCoin = sdk.NewCoin(tokenIn, totalIn) + + err = k.bankKeeper.SendCoinsFromAccountToModule( + ctx, + callerAddr, + types.ModuleName, + sdk.Coins{totalInCoin}, + ) + if err != nil { + return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err + } + } + + // This is ok because we've already successfully constructed a TradePairID above + pairID := takerTradePairID.MustPairID() + ctx.EventManager().EmitEvent(types.CreatePlaceLimitOrderEvent( + callerAddr, + receiverAddr, + pairID.Token0, + pairID.Token1, + tokenIn, + tokenOut, + totalIn, + tickIndexInToOut, + orderType.String(), + sharesIssued, + trancheKey, + )) + + return trancheKey, totalInCoin, swapInCoin, swapOutCoin, nil +} + +func (k Keeper) CalculatePlaceLimitOrder( + ctx sdk.Context, + takerTradePairID *types.TradePairID, + amountIn math.Int, + tickIndexInToOut int64, + orderType types.LimitOrderType, + goodTil *time.Time, + maxAmountOut *math.Int, + receiverAddr sdk.AccAddress, +) (trancheKey string, totalIn math.Int, swapInCoin, swapOutCoin sdk.Coin, sharesIssued math.Int, err error) { + amountLeft := amountIn - // This is ok because tokenOut is provided to the constructor of PairID above - takerTradePairID := pairID.MustTradePairIDFromMaker(tokenOut) var limitPrice math_utils.PrecDec limitPrice, err = types.CalcPrice(tickIndexInToOut) if err != nil { - return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err + return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err } // Ensure that after rounding user will get at least 1 token out. err = types.ValidateFairOutput(amountIn, limitPrice) if err != nil { - return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err + return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err } var orderFilled bool @@ -370,26 +484,13 @@ func (k Keeper) PlaceLimitOrderCore( swapInCoin, swapOutCoin, orderFilled, err = k.MakerLimitOrderSwap(ctx, *takerTradePairID, amountIn, limitPrice) } if err != nil { - return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err + return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err } - totalIn := swapInCoin.Amount + totalIn = swapInCoin.Amount amountLeft = amountLeft.Sub(swapInCoin.Amount) - if swapOutCoin.IsPositive() { - err = k.bankKeeper.SendCoinsFromModuleToAccount( - ctx, - types.ModuleName, - receiverAddr, - sdk.Coins{swapOutCoin}, - ) - if err != nil { - return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err - } - } - - // This is ok because pairID was constructed from tokenIn above. - makerTradePairID := pairID.MustTradePairIDFromMaker(tokenIn) + makerTradePairID := takerTradePairID.Reversed() makerTickIndexTakerToMaker := tickIndexInToOut * -1 var placeTranche *types.LimitOrderTranche placeTranche, err = k.GetOrInitPlaceTranche( @@ -400,7 +501,7 @@ func (k Keeper) PlaceLimitOrderCore( orderType, ) if err != nil { - return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err + return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err } trancheKey = placeTranche.Key.TrancheKey @@ -413,7 +514,6 @@ func (k Keeper) PlaceLimitOrderCore( receiverAddr.String(), ) - sharesIssued := math.ZeroInt() // FOR GTC, JIT & GoodTil try to place a maker limitOrder with remaining Amount if amountLeft.IsPositive() && !orderFilled && (orderType.IsGTC() || orderType.IsJIT() || orderType.IsGoodTil()) { @@ -424,7 +524,7 @@ func (k Keeper) PlaceLimitOrderCore( // order with the remaining liquidity. err = types.ValidateFairOutput(amountLeft, limitPrice) if err != nil { - return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err + return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err } placeTranche.PlaceMakerLimitOrder(amountLeft) trancheUser.SharesOwned = trancheUser.SharesOwned.Add(amountLeft) @@ -443,43 +543,15 @@ func (k Keeper) PlaceLimitOrderCore( k.SaveTrancheUser(ctx, trancheUser) - if totalIn.IsPositive() { - totalInCoin = sdk.NewCoin(tokenIn, totalIn) - - err = k.bankKeeper.SendCoinsFromAccountToModule( - ctx, - callerAddr, - types.ModuleName, - sdk.Coins{totalInCoin}, - ) - if err != nil { - return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err - } - } - if orderType.IsJIT() { err = k.AssertCanPlaceJIT(ctx) if err != nil { - return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err + return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err } k.IncrementJITsInBlock(ctx) } - ctx.EventManager().EmitEvent(types.CreatePlaceLimitOrderEvent( - callerAddr, - receiverAddr, - pairID.Token0, - pairID.Token1, - tokenIn, - tokenOut, - totalIn, - tickIndexInToOut, - orderType.String(), - sharesIssued, - trancheKey, - )) - - return trancheKey, totalInCoin, swapInCoin, swapOutCoin, nil + return trancheKey, totalIn, swapInCoin, swapOutCoin, sharesIssued, nil } // CancelLimitOrderCore handles MsgCancelLimitOrder, removing a specified number of shares from a limit order @@ -491,9 +563,43 @@ func (k Keeper) CancelLimitOrderCore( ) error { ctx := sdk.UnwrapSDKContext(goCtx) + coinOut, tradePairID, err := k.CalculateCancelLimitOrder(ctx, trancheKey, callerAddr) + if err != nil { + return err + } + + err = k.bankKeeper.SendCoinsFromModuleToAccount( + ctx, + types.ModuleName, + callerAddr, + sdk.Coins{coinOut}, + ) + if err != nil { + return err + } + + pairID := tradePairID.MustPairID() + ctx.EventManager().EmitEvent(types.CancelLimitOrderEvent( + callerAddr, + pairID.Token0, + pairID.Token1, + tradePairID.MakerDenom, + tradePairID.TakerDenom, + coinOut.Amount, + trancheKey, + )) + + return nil +} + +func (k Keeper) CalculateCancelLimitOrder( + ctx sdk.Context, + trancheKey string, + callerAddr sdk.AccAddress, +) (coinOut sdk.Coin, tradePairID *types.TradePairID, error error) { trancheUser, found := k.GetLimitOrderTrancheUser(ctx, callerAddr.String(), trancheKey) if !found { - return types.ErrActiveLimitOrderNotFound + return sdk.Coin{}, nil, types.ErrActiveLimitOrderNotFound } tradePairID, tickIndex := trancheUser.TradePairId, trancheUser.TickIndexTakerToMaker @@ -506,69 +612,80 @@ func (k Keeper) CancelLimitOrderCore( }, ) if tranche == nil { - return types.ErrActiveLimitOrderNotFound + return sdk.Coin{}, nil, types.ErrActiveLimitOrderNotFound } amountToCancel := tranche.RemoveTokenIn(trancheUser) trancheUser.SharesCancelled = trancheUser.SharesCancelled.Add(amountToCancel) - if amountToCancel.IsPositive() { - coinOut := sdk.NewCoin(tradePairID.MakerDenom, amountToCancel) + if !amountToCancel.IsPositive() { + return sdk.Coin{}, nil, sdkerrors.Wrapf(types.ErrCancelEmptyLimitOrder, "%s", tranche.Key.TrancheKey) + } - err := k.bankKeeper.SendCoinsFromModuleToAccount( - ctx, - types.ModuleName, - callerAddr, - sdk.Coins{coinOut}, - ) - if err != nil { - return err - } + k.SaveTrancheUser(ctx, trancheUser) + k.SaveTranche(ctx, tranche) - k.SaveTrancheUser(ctx, trancheUser) - k.SaveTranche(ctx, tranche) + if trancheUser.OrderType.HasExpiration() { + k.RemoveLimitOrderExpiration(ctx, *tranche.ExpirationTime, tranche.Key.KeyMarshal()) + } + coinOut = sdk.NewCoin(tradePairID.MakerDenom, amountToCancel) - if trancheUser.OrderType.HasExpiration() { - k.RemoveLimitOrderExpiration(ctx, *tranche.ExpirationTime, tranche.Key.KeyMarshal()) - } - } else { - return sdkerrors.Wrapf(types.ErrCancelEmptyLimitOrder, "%s", tranche.Key.TrancheKey) + return coinOut, tradePairID, nil + +} + +// WithdrawFilledLimitOrderCore handles MsgWithdrawFilledLimitOrder, calculates and sends filled liquidity from module to user +// for a limit order based on amount wished to receive. +func (k Keeper) WithdrawFilledLimitOrderCore( + goCtx context.Context, + trancheKey string, + callerAddr sdk.AccAddress, +) error { + ctx := sdk.UnwrapSDKContext(goCtx) + + amountOutTokenOut, remainingTokenIn, tradePairID, err := k.CalculateFilledLimitOrderCore(ctx, trancheKey, callerAddr) + if err != nil { + return err + } + + coinTakerDenomOut := sdk.NewCoin(tradePairID.TakerDenom, amountOutTokenOut) + coinMakerDenomRefund := sdk.NewCoin(tradePairID.MakerDenom, remainingTokenIn) + coins := sdk.NewCoins(coinTakerDenomOut, coinMakerDenomRefund) + ctx.EventManager().EmitEvents(types.GetEventsWithdrawnAmount(sdk.NewCoins(coinTakerDenomOut))) + if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, callerAddr, coins); err != nil { + return err } + // tradePairID has already been constructed so this will not error pairID := tradePairID.MustPairID() - ctx.EventManager().EmitEvent(types.CancelLimitOrderEvent( + ctx.EventManager().EmitEvent(types.WithdrawFilledLimitOrderEvent( callerAddr, pairID.Token0, pairID.Token1, tradePairID.MakerDenom, tradePairID.TakerDenom, - amountToCancel, + amountOutTokenOut, trancheKey, )) return nil } -// WithdrawFilledLimitOrderCore handles MsgWithdrawFilledLimitOrder, calculates and sends filled liquidity from module to user -// for a limit order based on amount wished to receive. -func (k Keeper) WithdrawFilledLimitOrderCore( - goCtx context.Context, +func (k Keeper) CalculateFilledLimitOrderCore( + ctx sdk.Context, trancheKey string, callerAddr sdk.AccAddress, -) error { - ctx := sdk.UnwrapSDKContext(goCtx) - +) (amountOutTokenOut, remainingTokenIn math.Int, tradePairID *types.TradePairID, err error) { trancheUser, found := k.GetLimitOrderTrancheUser( ctx, callerAddr.String(), trancheKey, ) if !found { - return sdkerrors.Wrapf(types.ErrValidLimitOrderTrancheNotFound, "%s", trancheKey) + return math.ZeroInt(), math.ZeroInt(), nil, sdkerrors.Wrapf(types.ErrValidLimitOrderTrancheNotFound, "%s", trancheKey) } tradePairID, tickIndex := trancheUser.TradePairId, trancheUser.TickIndexTakerToMaker - pairID := tradePairID.MustPairID() tranche, wasFilled, found := k.FindLimitOrderTranche( ctx, @@ -579,8 +696,8 @@ func (k Keeper) WithdrawFilledLimitOrderCore( }, ) - amountOutTokenOut := math.ZeroInt() - remainingTokenIn := math.ZeroInt() + amountOutTokenOut = math.ZeroInt() + remainingTokenIn = math.ZeroInt() // It's possible that a TrancheUser exists but tranche does not if LO was filled entirely through a swap if found { var amountOutTokenIn math.Int @@ -603,27 +720,10 @@ func (k Keeper) WithdrawFilledLimitOrderCore( k.SaveTrancheUser(ctx, trancheUser) - if amountOutTokenOut.IsPositive() || remainingTokenIn.IsPositive() { - coinTakerDenomOut := sdk.NewCoin(tradePairID.TakerDenom, amountOutTokenOut) - coinMakerDenomRefund := sdk.NewCoin(tradePairID.MakerDenom, remainingTokenIn) - coins := sdk.NewCoins(coinTakerDenomOut, coinMakerDenomRefund) - ctx.EventManager().EmitEvents(types.GetEventsWithdrawnAmount(sdk.NewCoins(coinTakerDenomOut))) - if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, callerAddr, coins); err != nil { - return err - } - } else { - return types.ErrWithdrawEmptyLimitOrder - } + if !amountOutTokenOut.IsPositive() && !remainingTokenIn.IsPositive() { - ctx.EventManager().EmitEvent(types.WithdrawFilledLimitOrderEvent( - callerAddr, - pairID.Token0, - pairID.Token1, - tradePairID.MakerDenom, - tradePairID.TakerDenom, - amountOutTokenOut, - trancheKey, - )) + return math.ZeroInt(), math.ZeroInt(), tradePairID, types.ErrWithdrawEmptyLimitOrder + } - return nil + return amountOutTokenOut, remainingTokenIn, tradePairID, nil } diff --git a/x/dex/keeper/multihop_swap.go b/x/dex/keeper/multihop_swap.go index 54bc23d34..e6071e68c 100644 --- a/x/dex/keeper/multihop_swap.go +++ b/x/dex/keeper/multihop_swap.go @@ -17,6 +17,13 @@ type MultihopStep struct { tradePairID *types.TradePairID } +type routeOutput struct { + write func() + coinOut sdk.Coin + route []string + dust sdk.Coins +} + func (k Keeper) HopsToRouteData( ctx sdk.Context, hops []string, From b171d74392a433c52e4968db89aa0e726f1d53e0 Mon Sep 17 00:00:00 2001 From: Julian Compagni Portis Date: Wed, 3 Jul 2024 19:24:06 -0400 Subject: [PATCH 2/5] Create separate files for each dex operation Nice cleanup for monolithic core.go. Also makes future refactors a bit simpler --- x/dex/keeper/cancel_limit_order.go | 92 +++ x/dex/keeper/core.go | 729 -------------------- x/dex/keeper/deposit.go | 166 +++++ x/dex/keeper/multihop_swap.go | 108 ++- x/dex/keeper/place_limit_order.go | 194 ++++++ x/dex/keeper/withdraw.go | 137 ++++ x/dex/keeper/withdraw_filled_limit_order.go | 108 +++ 7 files changed, 804 insertions(+), 730 deletions(-) create mode 100644 x/dex/keeper/cancel_limit_order.go delete mode 100644 x/dex/keeper/core.go create mode 100644 x/dex/keeper/deposit.go create mode 100644 x/dex/keeper/place_limit_order.go create mode 100644 x/dex/keeper/withdraw.go create mode 100644 x/dex/keeper/withdraw_filled_limit_order.go diff --git a/x/dex/keeper/cancel_limit_order.go b/x/dex/keeper/cancel_limit_order.go new file mode 100644 index 000000000..8ab4f8bfe --- /dev/null +++ b/x/dex/keeper/cancel_limit_order.go @@ -0,0 +1,92 @@ +package keeper + +import ( + "context" + + sdkerrors "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/neutron-org/neutron/v4/x/dex/types" +) + +// CancelLimitOrderCore handles the logic for MsgCancelLimitOrder including bank operations and event emissions. +func (k Keeper) CancelLimitOrderCore( + goCtx context.Context, + trancheKey string, + callerAddr sdk.AccAddress, +) error { + ctx := sdk.UnwrapSDKContext(goCtx) + + coinOut, tradePairID, err := k.ExecuteCancelLimitOrder(ctx, trancheKey, callerAddr) + if err != nil { + return err + } + + err = k.bankKeeper.SendCoinsFromModuleToAccount( + ctx, + types.ModuleName, + callerAddr, + sdk.Coins{coinOut}, + ) + if err != nil { + return err + } + + // This will never panic since TradePairID has already been successfully constructed by ExecuteCancelLimitOrder + pairID := tradePairID.MustPairID() + ctx.EventManager().EmitEvent(types.CancelLimitOrderEvent( + callerAddr, + pairID.Token0, + pairID.Token1, + tradePairID.MakerDenom, + tradePairID.TakerDenom, + coinOut.Amount, + trancheKey, + )) + + return nil +} + +// ExecuteCancelLimitOrder handles the core logic for CancelLimitOrder -- removing remaining TokenIn from the +// LimitOrderTranche and returning it to the user, updating the number of canceled shares on the LimitOrderTrancheUser. +// IT DOES NOT PERFORM ANY BANKING OPERATIONS +func (k Keeper) ExecuteCancelLimitOrder( + ctx sdk.Context, + trancheKey string, + callerAddr sdk.AccAddress, +) (coinOut sdk.Coin, tradePairID *types.TradePairID, error error) { + trancheUser, found := k.GetLimitOrderTrancheUser(ctx, callerAddr.String(), trancheKey) + if !found { + return sdk.Coin{}, nil, types.ErrActiveLimitOrderNotFound + } + + tradePairID, tickIndex := trancheUser.TradePairId, trancheUser.TickIndexTakerToMaker + tranche := k.GetLimitOrderTranche( + ctx, + &types.LimitOrderTrancheKey{ + TradePairId: tradePairID, + TickIndexTakerToMaker: tickIndex, + TrancheKey: trancheKey, + }, + ) + if tranche == nil { + return sdk.Coin{}, nil, types.ErrActiveLimitOrderNotFound + } + + amountToCancel := tranche.RemoveTokenIn(trancheUser) + trancheUser.SharesCancelled = trancheUser.SharesCancelled.Add(amountToCancel) + + if !amountToCancel.IsPositive() { + return sdk.Coin{}, nil, sdkerrors.Wrapf(types.ErrCancelEmptyLimitOrder, "%s", tranche.Key.TrancheKey) + } + + k.SaveTrancheUser(ctx, trancheUser) + k.SaveTranche(ctx, tranche) + + if trancheUser.OrderType.HasExpiration() { + k.RemoveLimitOrderExpiration(ctx, *tranche.ExpirationTime, tranche.Key.KeyMarshal()) + } + coinOut = sdk.NewCoin(tradePairID.MakerDenom, amountToCancel) + + return coinOut, tradePairID, nil +} diff --git a/x/dex/keeper/core.go b/x/dex/keeper/core.go deleted file mode 100644 index 33753bf94..000000000 --- a/x/dex/keeper/core.go +++ /dev/null @@ -1,729 +0,0 @@ -package keeper - -import ( - "context" - "errors" - "fmt" - "time" - - sdkerrors "cosmossdk.io/errors" - "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/neutron-org/neutron/v4/utils" - math_utils "github.com/neutron-org/neutron/v4/utils/math" - "github.com/neutron-org/neutron/v4/x/dex/types" -) - -// NOTE: Currently we are using TruncateInt in multiple places for converting Decs back into math.Ints. -// This may create some accounting anomalies but seems preferable to other alternatives. -// See full ADR here: https://www.notion.so/dualityxyz/A-Modest-Proposal-For-Truncating-696a919d59254876a617f82fb9567895 - -// Handles core logic for MsgDeposit, checking and initializing data structures (tick, pair), calculating -// shares based on amount deposited, and sending funds to moduleAddress. -func (k Keeper) DepositCore( - goCtx context.Context, - pairID *types.PairID, - callerAddr sdk.AccAddress, - receiverAddr sdk.AccAddress, - amounts0 []math.Int, - amounts1 []math.Int, - tickIndices []int64, - fees []uint64, - options []*types.DepositOptions, -) (amounts0Deposit, amounts1Deposit []math.Int, sharesIssued sdk.Coins, failedDeposits []*types.FailedDeposit, err error) { - ctx := sdk.UnwrapSDKContext(goCtx) - - amounts0Deposited, - amounts1Deposited, - totalAmountReserve0, - totalAmountReserve1, - sharesIssued, - failedDeposits, - err := k.CalculateDeposit(ctx, pairID, callerAddr, receiverAddr, amounts0, amounts1, tickIndices, fees, options) - if err != nil { - return nil, nil, nil, failedDeposits, err - } - - if totalAmountReserve0.IsPositive() { - coin0 := sdk.NewCoin(pairID.Token0, totalAmountReserve0) - if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, callerAddr, types.ModuleName, sdk.Coins{coin0}); err != nil { - return nil, nil, nil, nil, err - } - } - - if totalAmountReserve1.IsPositive() { - coin1 := sdk.NewCoin(pairID.Token1, totalAmountReserve1) - if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, callerAddr, types.ModuleName, sdk.Coins{coin1}); err != nil { - return nil, nil, nil, nil, err - } - } - - if err := k.MintShares(ctx, receiverAddr, sharesIssued); err != nil { - return nil, nil, nil, nil, err - } - - return amounts0Deposited, amounts1Deposited, sharesIssued, failedDeposits, nil -} - -func (k Keeper) CalculateDeposit( - ctx sdk.Context, - pairID *types.PairID, - callerAddr sdk.AccAddress, - receiverAddr sdk.AccAddress, - amounts0 []math.Int, - amounts1 []math.Int, - tickIndices []int64, - fees []uint64, - options []*types.DepositOptions) (amounts0Deposit, amounts1Deposit []math.Int, totalAmountReserve0, totalAmountReserve1 math.Int, sharesIssued sdk.Coins, failedDeposits []*types.FailedDeposit, err error) { - totalAmountReserve0 = math.ZeroInt() - totalAmountReserve1 = math.ZeroInt() - amounts0Deposited := make([]math.Int, len(amounts0)) - amounts1Deposited := make([]math.Int, len(amounts1)) - sharesIssued = sdk.Coins{} - - for i := 0; i < len(amounts0); i++ { - amounts0Deposited[i] = math.ZeroInt() - amounts1Deposited[i] = math.ZeroInt() - } - - for i, amount0 := range amounts0 { - amount1 := amounts1[i] - tickIndex := tickIndices[i] - fee := fees[i] - option := options[i] - if option == nil { - option = &types.DepositOptions{} - } - autoswap := !option.DisableAutoswap - - if err := k.ValidateFee(ctx, fee); err != nil { - return nil, nil, math.ZeroInt(), math.ZeroInt(), nil, nil, err - } - - if k.IsPoolBehindEnemyLines(ctx, pairID, tickIndex, fee, amount0, amount1) { - err = sdkerrors.Wrapf(types.ErrDepositBehindEnemyLines, - "deposit failed at tick %d fee %d", tickIndex, fee) - if option.FailTxOnBel { - return nil, nil, math.ZeroInt(), math.ZeroInt(), nil, nil, err - } - failedDeposits = append(failedDeposits, &types.FailedDeposit{DepositIdx: uint64(i), Error: err.Error()}) - continue - } - - pool, err := k.GetOrInitPool( - ctx, - pairID, - tickIndex, - fee, - ) - if err != nil { - return nil, nil, math.ZeroInt(), math.ZeroInt(), nil, nil, err - } - - existingShares := k.bankKeeper.GetSupply(ctx, pool.GetPoolDenom()).Amount - - inAmount0, inAmount1, outShares := pool.Deposit(amount0, amount1, existingShares, autoswap) - - k.SetPool(ctx, pool) - - if inAmount0.IsZero() && inAmount1.IsZero() { - return nil, nil, math.ZeroInt(), math.ZeroInt(), nil, nil, types.ErrZeroTrueDeposit - } - - if outShares.IsZero() { - - return nil, nil, math.ZeroInt(), math.ZeroInt(), nil, nil, types.ErrDepositShareUnderflow - } - - sharesIssued = append(sharesIssued, outShares) - - amounts0Deposited[i] = inAmount0 - amounts1Deposited[i] = inAmount1 - totalAmountReserve0 = totalAmountReserve0.Add(inAmount0) - totalAmountReserve1 = totalAmountReserve1.Add(inAmount1) - - //TODO: probably don't emit events here - ctx.EventManager().EmitEvent(types.CreateDepositEvent( - callerAddr, - receiverAddr, - pairID.Token0, - pairID.Token1, - tickIndex, - fee, - inAmount0, - inAmount1, - outShares.Amount, - )) - } - - // At this point shares issued is not sorted and may have duplicates - // we must sanitize to convert it to a valid set of coins - sharesIssued = utils.SanitizeCoins(sharesIssued) - return amounts0Deposit, amounts1Deposit, totalAmountReserve0, totalAmountReserve1, sharesIssued, failedDeposits, nil -} - -// Handles core logic for MsgWithdrawal; calculating and withdrawing reserve0,reserve1 from a specified tick -// given a specified number of shares to remove. -// Calculates the amount of reserve0, reserve1 to withdraw based on the percentage of the desired -// number of shares to remove compared to the total number of shares at the given tick. -func (k Keeper) WithdrawCore( - goCtx context.Context, - pairID *types.PairID, - callerAddr sdk.AccAddress, - receiverAddr sdk.AccAddress, - sharesToRemoveList []math.Int, - tickIndicesNormalized []int64, - fees []uint64, -) error { - ctx := sdk.UnwrapSDKContext(goCtx) - - totalReserve0ToRemove, totalReserve1ToRemove, events, err := k.CalculateWithdraw(ctx, pairID, callerAddr, receiverAddr, sharesToRemoveList, tickIndicesNormalized, fees) - if err != nil { - return err - } - - ctx.EventManager().EmitEvents(events) - - if totalReserve0ToRemove.IsPositive() { - coin0 := sdk.NewCoin(pairID.Token0, totalReserve0ToRemove) - - err := k.bankKeeper.SendCoinsFromModuleToAccount( - ctx, - types.ModuleName, - receiverAddr, - sdk.Coins{coin0}, - ) - ctx.EventManager().EmitEvents(types.GetEventsWithdrawnAmount(sdk.Coins{coin0})) - if err != nil { - return err - } - } - - // sends totalReserve1ToRemove to receiverAddr - if totalReserve1ToRemove.IsPositive() { - coin1 := sdk.NewCoin(pairID.Token1, totalReserve1ToRemove) - err := k.bankKeeper.SendCoinsFromModuleToAccount( - ctx, - types.ModuleName, - receiverAddr, - sdk.Coins{coin1}, - ) - ctx.EventManager().EmitEvents(types.GetEventsWithdrawnAmount(sdk.Coins{coin1})) - if err != nil { - return err - } - } - - return nil -} - -func (k Keeper) CalculateWithdraw( - ctx sdk.Context, - pairID *types.PairID, - callerAddr sdk.AccAddress, - receiverAddr sdk.AccAddress, - sharesToRemoveList []math.Int, - tickIndicesNormalized []int64, - fees []uint64, -) (totalReserves0ToRemove, totalReserves1ToRemove math.Int, events sdk.Events, err error) { - totalReserve0ToRemove := math.ZeroInt() - totalReserve1ToRemove := math.ZeroInt() - - for i, fee := range fees { - sharesToRemove := sharesToRemoveList[i] - tickIndex := tickIndicesNormalized[i] - - pool, err := k.GetOrInitPool(ctx, pairID, tickIndex, fee) - if err != nil { - return math.ZeroInt(), math.ZeroInt(), nil, err - } - - poolDenom := pool.GetPoolDenom() - - totalShares := k.bankKeeper.GetSupply(ctx, poolDenom).Amount - if totalShares.LT(sharesToRemove) { - return math.ZeroInt(), math.ZeroInt(), nil, sdkerrors.Wrapf( - types.ErrInsufficientShares, - "%s does not have %s shares of type %s", - callerAddr, - sharesToRemove, - poolDenom, - ) - } - - outAmount0, outAmount1 := pool.Withdraw(sharesToRemove, totalShares) - k.SetPool(ctx, pool) - - if sharesToRemove.IsPositive() { - if err := k.BurnShares(ctx, callerAddr, sharesToRemove, poolDenom); err != nil { - return math.ZeroInt(), math.ZeroInt(), nil, err - } - } - - totalReserve0ToRemove = totalReserve0ToRemove.Add(outAmount0) - totalReserve1ToRemove = totalReserve1ToRemove.Add(outAmount1) - - event := types.CreateWithdrawEvent( - callerAddr, - receiverAddr, - pairID.Token0, - pairID.Token1, - tickIndex, - fee, - outAmount0, - outAmount1, - sharesToRemove, - ) - events = append(events, event) - } - return totalReserve0ToRemove, totalReserve1ToRemove, events, nil -} - -func (k Keeper) MultiHopSwapCore( - goCtx context.Context, - amountIn math.Int, - routes []*types.MultiHopRoute, - exitLimitPrice math_utils.PrecDec, - pickBestRoute bool, - callerAddr sdk.AccAddress, - receiverAddr sdk.AccAddress, -) (coinOut sdk.Coin, err error) { - ctx := sdk.UnwrapSDKContext(goCtx) - bestRoute, initialInCoin, err := k.CalculateMultiHopSwap(ctx, amountIn, routes, exitLimitPrice, pickBestRoute, callerAddr, receiverAddr) - if err != nil { - return sdk.Coin{}, err - } - - bestRoute.write() - err = k.bankKeeper.SendCoinsFromAccountToModule( - ctx, - callerAddr, - types.ModuleName, - sdk.Coins{initialInCoin}, - ) - if err != nil { - return sdk.Coin{}, err - } - - // send both dust and coinOut to receiver - // note that dust can be multiple coins collected from multiple hops. - err = k.bankKeeper.SendCoinsFromModuleToAccount( - ctx, - types.ModuleName, - receiverAddr, - bestRoute.dust.Add(bestRoute.coinOut), - ) - if err != nil { - return sdk.Coin{}, fmt.Errorf("failed to send out coin and dust to the receiver: %w", err) - } - - ctx.EventManager().EmitEvent(types.CreateMultihopSwapEvent( - callerAddr, - receiverAddr, - initialInCoin.Denom, - bestRoute.coinOut.Denom, - initialInCoin.Amount, - bestRoute.coinOut.Amount, - bestRoute.route, - bestRoute.dust, - )) - - return bestRoute.coinOut, nil -} - -func (k Keeper) CalculateMultiHopSwap( - ctx sdk.Context, - amountIn math.Int, - routes []*types.MultiHopRoute, - exitLimitPrice math_utils.PrecDec, - pickBestRoute bool, - callerAddr sdk.AccAddress, - receiverAddr sdk.AccAddress, -) (bestRoute routeOutput, initialInCoin sdk.Coin, err error) { - var routeErrors []error - initialInCoin = sdk.NewCoin(routes[0].Hops[0], amountIn) - stepCache := make(map[multihopCacheKey]StepResult) - - bestRoute.coinOut = sdk.Coin{Amount: math.ZeroInt()} - - for _, route := range routes { - routeDust, routeCoinOut, writeRoute, err := k.RunMultihopRoute( - ctx, - *route, - initialInCoin, - exitLimitPrice, - stepCache, - ) - if err != nil { - routeErrors = append(routeErrors, err) - continue - } - - if !pickBestRoute || bestRoute.coinOut.Amount.LT(routeCoinOut.Amount) { - bestRoute.coinOut = routeCoinOut - bestRoute.write = writeRoute - bestRoute.route = route.Hops - bestRoute.dust = routeDust - } - if !pickBestRoute { - break - } - } - - if len(routeErrors) == len(routes) { - // All routes have failed - - allErr := errors.Join(append([]error{types.ErrAllMultiHopRoutesFailed}, routeErrors...)...) - - return routeOutput{}, sdk.Coin{}, allErr - } - - return bestRoute, initialInCoin, nil -} - -// PlaceLimitOrderCore handles MsgPlaceLimitOrder, initializing (tick, pair) data structures if needed, calculating and -// storing information for a new limit order at a specific tick. -func (k Keeper) PlaceLimitOrderCore( - goCtx context.Context, - tokenIn string, - tokenOut string, - amountIn math.Int, - tickIndexInToOut int64, - orderType types.LimitOrderType, - goodTil *time.Time, - maxAmountOut *math.Int, - callerAddr sdk.AccAddress, - receiverAddr sdk.AccAddress, -) (trancheKey string, totalInCoin, swapInCoin, swapOutCoin sdk.Coin, err error) { - ctx := sdk.UnwrapSDKContext(goCtx) - - takerTradePairID, err := types.NewTradePairID(tokenIn, tokenOut) - if err != nil { - return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err - } - trancheKey, totalIn, swapInCoin, swapOutCoin, sharesIssued, err := k.CalculatePlaceLimitOrder(ctx, takerTradePairID, amountIn, tickIndexInToOut, orderType, goodTil, maxAmountOut, receiverAddr) - if err != nil { - return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err - } - - if swapOutCoin.IsPositive() { - err = k.bankKeeper.SendCoinsFromModuleToAccount( - ctx, - types.ModuleName, - receiverAddr, - sdk.Coins{swapOutCoin}, - ) - if err != nil { - return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err - } - } - - if totalIn.IsPositive() { - totalInCoin = sdk.NewCoin(tokenIn, totalIn) - - err = k.bankKeeper.SendCoinsFromAccountToModule( - ctx, - callerAddr, - types.ModuleName, - sdk.Coins{totalInCoin}, - ) - if err != nil { - return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err - } - } - - // This is ok because we've already successfully constructed a TradePairID above - pairID := takerTradePairID.MustPairID() - ctx.EventManager().EmitEvent(types.CreatePlaceLimitOrderEvent( - callerAddr, - receiverAddr, - pairID.Token0, - pairID.Token1, - tokenIn, - tokenOut, - totalIn, - tickIndexInToOut, - orderType.String(), - sharesIssued, - trancheKey, - )) - - return trancheKey, totalInCoin, swapInCoin, swapOutCoin, nil -} - -func (k Keeper) CalculatePlaceLimitOrder( - ctx sdk.Context, - takerTradePairID *types.TradePairID, - amountIn math.Int, - tickIndexInToOut int64, - orderType types.LimitOrderType, - goodTil *time.Time, - maxAmountOut *math.Int, - receiverAddr sdk.AccAddress, -) (trancheKey string, totalIn math.Int, swapInCoin, swapOutCoin sdk.Coin, sharesIssued math.Int, err error) { - - amountLeft := amountIn - - var limitPrice math_utils.PrecDec - limitPrice, err = types.CalcPrice(tickIndexInToOut) - if err != nil { - return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err - } - - // Ensure that after rounding user will get at least 1 token out. - err = types.ValidateFairOutput(amountIn, limitPrice) - if err != nil { - return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err - } - - var orderFilled bool - if orderType.IsTakerOnly() { - swapInCoin, swapOutCoin, err = k.TakerLimitOrderSwap(ctx, *takerTradePairID, amountIn, maxAmountOut, limitPrice, orderType) - } else { - swapInCoin, swapOutCoin, orderFilled, err = k.MakerLimitOrderSwap(ctx, *takerTradePairID, amountIn, limitPrice) - } - if err != nil { - return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err - } - - totalIn = swapInCoin.Amount - amountLeft = amountLeft.Sub(swapInCoin.Amount) - - makerTradePairID := takerTradePairID.Reversed() - makerTickIndexTakerToMaker := tickIndexInToOut * -1 - var placeTranche *types.LimitOrderTranche - placeTranche, err = k.GetOrInitPlaceTranche( - ctx, - makerTradePairID, - makerTickIndexTakerToMaker, - goodTil, - orderType, - ) - if err != nil { - return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err - } - - trancheKey = placeTranche.Key.TrancheKey - trancheUser := k.GetOrInitLimitOrderTrancheUser( - ctx, - makerTradePairID, - makerTickIndexTakerToMaker, - trancheKey, - orderType, - receiverAddr.String(), - ) - - // FOR GTC, JIT & GoodTil try to place a maker limitOrder with remaining Amount - if amountLeft.IsPositive() && !orderFilled && - (orderType.IsGTC() || orderType.IsJIT() || orderType.IsGoodTil()) { - - // Ensure that the maker portion will generate at least 1 token of output - // NOTE: This does mean that a successful taker leg of the trade will be thrown away since the entire tx will fail. - // In most circumstances this seems preferable to executing the taker leg and exiting early before placing a maker - // order with the remaining liquidity. - err = types.ValidateFairOutput(amountLeft, limitPrice) - if err != nil { - return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err - } - placeTranche.PlaceMakerLimitOrder(amountLeft) - trancheUser.SharesOwned = trancheUser.SharesOwned.Add(amountLeft) - - if orderType.HasExpiration() { - goodTilRecord := NewLimitOrderExpiration(placeTranche) - k.SetLimitOrderExpiration(ctx, goodTilRecord) - ctx.GasMeter().ConsumeGas(types.ExpiringLimitOrderGas, "Expiring LimitOrder Fee") - } - - k.SaveTranche(ctx, placeTranche) - - totalIn = totalIn.Add(amountLeft) - sharesIssued = amountLeft - } - - k.SaveTrancheUser(ctx, trancheUser) - - if orderType.IsJIT() { - err = k.AssertCanPlaceJIT(ctx) - if err != nil { - return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err - } - k.IncrementJITsInBlock(ctx) - } - - return trancheKey, totalIn, swapInCoin, swapOutCoin, sharesIssued, nil -} - -// CancelLimitOrderCore handles MsgCancelLimitOrder, removing a specified number of shares from a limit order -// and returning the respective amount in terms of the reserve to the user. -func (k Keeper) CancelLimitOrderCore( - goCtx context.Context, - trancheKey string, - callerAddr sdk.AccAddress, -) error { - ctx := sdk.UnwrapSDKContext(goCtx) - - coinOut, tradePairID, err := k.CalculateCancelLimitOrder(ctx, trancheKey, callerAddr) - if err != nil { - return err - } - - err = k.bankKeeper.SendCoinsFromModuleToAccount( - ctx, - types.ModuleName, - callerAddr, - sdk.Coins{coinOut}, - ) - if err != nil { - return err - } - - pairID := tradePairID.MustPairID() - ctx.EventManager().EmitEvent(types.CancelLimitOrderEvent( - callerAddr, - pairID.Token0, - pairID.Token1, - tradePairID.MakerDenom, - tradePairID.TakerDenom, - coinOut.Amount, - trancheKey, - )) - - return nil -} - -func (k Keeper) CalculateCancelLimitOrder( - ctx sdk.Context, - trancheKey string, - callerAddr sdk.AccAddress, -) (coinOut sdk.Coin, tradePairID *types.TradePairID, error error) { - trancheUser, found := k.GetLimitOrderTrancheUser(ctx, callerAddr.String(), trancheKey) - if !found { - return sdk.Coin{}, nil, types.ErrActiveLimitOrderNotFound - } - - tradePairID, tickIndex := trancheUser.TradePairId, trancheUser.TickIndexTakerToMaker - tranche := k.GetLimitOrderTranche( - ctx, - &types.LimitOrderTrancheKey{ - TradePairId: tradePairID, - TickIndexTakerToMaker: tickIndex, - TrancheKey: trancheKey, - }, - ) - if tranche == nil { - return sdk.Coin{}, nil, types.ErrActiveLimitOrderNotFound - } - - amountToCancel := tranche.RemoveTokenIn(trancheUser) - trancheUser.SharesCancelled = trancheUser.SharesCancelled.Add(amountToCancel) - - if !amountToCancel.IsPositive() { - return sdk.Coin{}, nil, sdkerrors.Wrapf(types.ErrCancelEmptyLimitOrder, "%s", tranche.Key.TrancheKey) - } - - k.SaveTrancheUser(ctx, trancheUser) - k.SaveTranche(ctx, tranche) - - if trancheUser.OrderType.HasExpiration() { - k.RemoveLimitOrderExpiration(ctx, *tranche.ExpirationTime, tranche.Key.KeyMarshal()) - } - coinOut = sdk.NewCoin(tradePairID.MakerDenom, amountToCancel) - - return coinOut, tradePairID, nil - -} - -// WithdrawFilledLimitOrderCore handles MsgWithdrawFilledLimitOrder, calculates and sends filled liquidity from module to user -// for a limit order based on amount wished to receive. -func (k Keeper) WithdrawFilledLimitOrderCore( - goCtx context.Context, - trancheKey string, - callerAddr sdk.AccAddress, -) error { - ctx := sdk.UnwrapSDKContext(goCtx) - - amountOutTokenOut, remainingTokenIn, tradePairID, err := k.CalculateFilledLimitOrderCore(ctx, trancheKey, callerAddr) - if err != nil { - return err - } - - coinTakerDenomOut := sdk.NewCoin(tradePairID.TakerDenom, amountOutTokenOut) - coinMakerDenomRefund := sdk.NewCoin(tradePairID.MakerDenom, remainingTokenIn) - coins := sdk.NewCoins(coinTakerDenomOut, coinMakerDenomRefund) - ctx.EventManager().EmitEvents(types.GetEventsWithdrawnAmount(sdk.NewCoins(coinTakerDenomOut))) - if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, callerAddr, coins); err != nil { - return err - } - - // tradePairID has already been constructed so this will not error - pairID := tradePairID.MustPairID() - ctx.EventManager().EmitEvent(types.WithdrawFilledLimitOrderEvent( - callerAddr, - pairID.Token0, - pairID.Token1, - tradePairID.MakerDenom, - tradePairID.TakerDenom, - amountOutTokenOut, - trancheKey, - )) - - return nil -} - -func (k Keeper) CalculateFilledLimitOrderCore( - ctx sdk.Context, - trancheKey string, - callerAddr sdk.AccAddress, -) (amountOutTokenOut, remainingTokenIn math.Int, tradePairID *types.TradePairID, err error) { - trancheUser, found := k.GetLimitOrderTrancheUser( - ctx, - callerAddr.String(), - trancheKey, - ) - if !found { - return math.ZeroInt(), math.ZeroInt(), nil, sdkerrors.Wrapf(types.ErrValidLimitOrderTrancheNotFound, "%s", trancheKey) - } - - tradePairID, tickIndex := trancheUser.TradePairId, trancheUser.TickIndexTakerToMaker - - tranche, wasFilled, found := k.FindLimitOrderTranche( - ctx, - &types.LimitOrderTrancheKey{ - TradePairId: tradePairID, - TickIndexTakerToMaker: tickIndex, - TrancheKey: trancheKey, - }, - ) - - amountOutTokenOut = math.ZeroInt() - remainingTokenIn = math.ZeroInt() - // It's possible that a TrancheUser exists but tranche does not if LO was filled entirely through a swap - if found { - var amountOutTokenIn math.Int - amountOutTokenIn, amountOutTokenOut = tranche.Withdraw(trancheUser) - - if wasFilled { - // This is only relevant for inactive JIT and GoodTil limit orders - remainingTokenIn = tranche.RemoveTokenIn(trancheUser) - k.SaveInactiveTranche(ctx, tranche) - - // Treat the removed tokenIn as cancelled shares - trancheUser.SharesCancelled = trancheUser.SharesCancelled.Add(remainingTokenIn) - - } else { - k.SetLimitOrderTranche(ctx, tranche) - } - - trancheUser.SharesWithdrawn = trancheUser.SharesWithdrawn.Add(amountOutTokenIn) - } - - k.SaveTrancheUser(ctx, trancheUser) - - if !amountOutTokenOut.IsPositive() && !remainingTokenIn.IsPositive() { - - return math.ZeroInt(), math.ZeroInt(), tradePairID, types.ErrWithdrawEmptyLimitOrder - } - - return amountOutTokenOut, remainingTokenIn, tradePairID, nil -} diff --git a/x/dex/keeper/deposit.go b/x/dex/keeper/deposit.go new file mode 100644 index 000000000..6bb9bcf1e --- /dev/null +++ b/x/dex/keeper/deposit.go @@ -0,0 +1,166 @@ +package keeper + +import ( + "context" + + sdkerrors "cosmossdk.io/errors" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/neutron-org/neutron/v4/utils" + "github.com/neutron-org/neutron/v4/x/dex/types" +) + +// DepositCore handles core logic for MsgDeposit including bank operations and event emissions +func (k Keeper) DepositCore( + goCtx context.Context, + pairID *types.PairID, + callerAddr sdk.AccAddress, + receiverAddr sdk.AccAddress, + amounts0 []math.Int, + amounts1 []math.Int, + tickIndices []int64, + fees []uint64, + options []*types.DepositOptions, +) (amounts0Deposit, amounts1Deposit []math.Int, sharesIssued sdk.Coins, failedDeposits []*types.FailedDeposit, err error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + amounts0Deposited, + amounts1Deposited, + totalAmountReserve0, + totalAmountReserve1, + sharesIssued, + events, + failedDeposits, + err := k.ExecuteDeposit(ctx, pairID, callerAddr, receiverAddr, amounts0, amounts1, tickIndices, fees, options) + if err != nil { + return nil, nil, nil, failedDeposits, err + } + + ctx.EventManager().EmitEvents(events) + + if totalAmountReserve0.IsPositive() { + coin0 := sdk.NewCoin(pairID.Token0, totalAmountReserve0) + if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, callerAddr, types.ModuleName, sdk.Coins{coin0}); err != nil { + return nil, nil, nil, nil, err + } + } + + if totalAmountReserve1.IsPositive() { + coin1 := sdk.NewCoin(pairID.Token1, totalAmountReserve1) + if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, callerAddr, types.ModuleName, sdk.Coins{coin1}); err != nil { + return nil, nil, nil, nil, err + } + } + + if err := k.MintShares(ctx, receiverAddr, sharesIssued); err != nil { + return nil, nil, nil, nil, err + } + + return amounts0Deposited, amounts1Deposited, sharesIssued, failedDeposits, nil +} + +// ExecuteDeposit handles core logic for deposits -- checking and initializing data structures (tick, pair), calculating +// shares based on amount deposited. IT DOES NOT PERFORM ANY BANKING OPERATIONS. +func (k Keeper) ExecuteDeposit( + ctx sdk.Context, + pairID *types.PairID, + callerAddr sdk.AccAddress, + receiverAddr sdk.AccAddress, + amounts0 []math.Int, + amounts1 []math.Int, + tickIndices []int64, + fees []uint64, + options []*types.DepositOptions) ( + amounts0Deposit, amounts1Deposit []math.Int, + totalAmountReserve0, totalAmountReserve1 math.Int, + sharesIssued sdk.Coins, + events sdk.Events, + failedDeposits []*types.FailedDeposit, + err error, +) { + totalAmountReserve0 = math.ZeroInt() + totalAmountReserve1 = math.ZeroInt() + amounts0Deposited := make([]math.Int, len(amounts0)) + amounts1Deposited := make([]math.Int, len(amounts1)) + sharesIssued = sdk.Coins{} + + for i := 0; i < len(amounts0); i++ { + amounts0Deposited[i] = math.ZeroInt() + amounts1Deposited[i] = math.ZeroInt() + } + + for i, amount0 := range amounts0 { + amount1 := amounts1[i] + tickIndex := tickIndices[i] + fee := fees[i] + option := options[i] + if option == nil { + option = &types.DepositOptions{} + } + autoswap := !option.DisableAutoswap + + if err := k.ValidateFee(ctx, fee); err != nil { + return nil, nil, math.ZeroInt(), math.ZeroInt(), nil, nil, nil, err + } + + if k.IsPoolBehindEnemyLines(ctx, pairID, tickIndex, fee, amount0, amount1) { + err = sdkerrors.Wrapf(types.ErrDepositBehindEnemyLines, + "deposit failed at tick %d fee %d", tickIndex, fee) + if option.FailTxOnBel { + return nil, nil, math.ZeroInt(), math.ZeroInt(), nil, nil, nil, err + } + failedDeposits = append(failedDeposits, &types.FailedDeposit{DepositIdx: uint64(i), Error: err.Error()}) + continue + } + + pool, err := k.GetOrInitPool( + ctx, + pairID, + tickIndex, + fee, + ) + if err != nil { + return nil, nil, math.ZeroInt(), math.ZeroInt(), nil, nil, nil, err + } + + existingShares := k.bankKeeper.GetSupply(ctx, pool.GetPoolDenom()).Amount + + inAmount0, inAmount1, outShares := pool.Deposit(amount0, amount1, existingShares, autoswap) + + k.SetPool(ctx, pool) + + if inAmount0.IsZero() && inAmount1.IsZero() { + return nil, nil, math.ZeroInt(), math.ZeroInt(), nil, nil, nil, types.ErrZeroTrueDeposit + } + + if outShares.IsZero() { + return nil, nil, math.ZeroInt(), math.ZeroInt(), nil, nil, nil, types.ErrDepositShareUnderflow + } + + sharesIssued = append(sharesIssued, outShares) + + amounts0Deposited[i] = inAmount0 + amounts1Deposited[i] = inAmount1 + totalAmountReserve0 = totalAmountReserve0.Add(inAmount0) + totalAmountReserve1 = totalAmountReserve1.Add(inAmount1) + + depositEvent := types.CreateDepositEvent( + callerAddr, + receiverAddr, + pairID.Token0, + pairID.Token1, + tickIndex, + fee, + inAmount0, + inAmount1, + outShares.Amount, + ) + events = append(events, depositEvent) + } + + // At this point shares issued is not sorted and may have duplicates + // we must sanitize to convert it to a valid set of coins + sharesIssued = utils.SanitizeCoins(sharesIssued) + return amounts0Deposit, amounts1Deposit, totalAmountReserve0, totalAmountReserve1, sharesIssued, events, failedDeposits, nil +} diff --git a/x/dex/keeper/multihop_swap.go b/x/dex/keeper/multihop_swap.go index e6071e68c..f81f8f8c4 100644 --- a/x/dex/keeper/multihop_swap.go +++ b/x/dex/keeper/multihop_swap.go @@ -1,6 +1,8 @@ package keeper import ( + "context" + "errors" "fmt" sdkerrors "cosmossdk.io/errors" @@ -17,13 +19,117 @@ type MultihopStep struct { tradePairID *types.TradePairID } -type routeOutput struct { +type MultiHopRouteOutput struct { write func() coinOut sdk.Coin route []string dust sdk.Coins } +// MultiHopSwapCore handles logic for MsgMultihopSwap including bank operations and event emissions. +func (k Keeper) MultiHopSwapCore( + goCtx context.Context, + amountIn math.Int, + routes []*types.MultiHopRoute, + exitLimitPrice math_utils.PrecDec, + pickBestRoute bool, + callerAddr sdk.AccAddress, + receiverAddr sdk.AccAddress, +) (coinOut sdk.Coin, err error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + bestRoute, initialInCoin, err := k.ExecuteMultiHopSwap(ctx, amountIn, routes, exitLimitPrice, pickBestRoute) + if err != nil { + return sdk.Coin{}, err + } + + bestRoute.write() + err = k.bankKeeper.SendCoinsFromAccountToModule( + ctx, + callerAddr, + types.ModuleName, + sdk.Coins{initialInCoin}, + ) + if err != nil { + return sdk.Coin{}, err + } + + // send both dust and coinOut to receiver + // note that dust can be multiple coins collected from multiple hops. + err = k.bankKeeper.SendCoinsFromModuleToAccount( + ctx, + types.ModuleName, + receiverAddr, + bestRoute.dust.Add(bestRoute.coinOut), + ) + if err != nil { + return sdk.Coin{}, fmt.Errorf("failed to send out coin and dust to the receiver: %w", err) + } + + ctx.EventManager().EmitEvent(types.CreateMultihopSwapEvent( + callerAddr, + receiverAddr, + initialInCoin.Denom, + bestRoute.coinOut.Denom, + initialInCoin.Amount, + bestRoute.coinOut.Amount, + bestRoute.route, + bestRoute.dust, + )) + + return bestRoute.coinOut, nil +} + +// ExecuteMultiHopSwap handles the core logic for MultiHopSwap -- simulating swap operations across all routes (when applicable) +// and picking the best route to execute. IT DOES NOT PERFORM ANY BANKING OPERATIONS. +func (k Keeper) ExecuteMultiHopSwap( + ctx sdk.Context, + amountIn math.Int, + routes []*types.MultiHopRoute, + exitLimitPrice math_utils.PrecDec, + pickBestRoute bool, +) (bestRoute MultiHopRouteOutput, initialInCoin sdk.Coin, err error) { + var routeErrors []error + initialInCoin = sdk.NewCoin(routes[0].Hops[0], amountIn) + stepCache := make(map[multihopCacheKey]StepResult) + + bestRoute.coinOut = sdk.Coin{Amount: math.ZeroInt()} + + for _, route := range routes { + routeDust, routeCoinOut, writeRoute, err := k.RunMultihopRoute( + ctx, + *route, + initialInCoin, + exitLimitPrice, + stepCache, + ) + if err != nil { + routeErrors = append(routeErrors, err) + continue + } + + if !pickBestRoute || bestRoute.coinOut.Amount.LT(routeCoinOut.Amount) { + bestRoute.coinOut = routeCoinOut + bestRoute.write = writeRoute + bestRoute.route = route.Hops + bestRoute.dust = routeDust + } + if !pickBestRoute { + break + } + } + + if len(routeErrors) == len(routes) { + // All routes have failed + + allErr := errors.Join(append([]error{types.ErrAllMultiHopRoutesFailed}, routeErrors...)...) + + return MultiHopRouteOutput{}, sdk.Coin{}, allErr + } + + return bestRoute, initialInCoin, nil +} + func (k Keeper) HopsToRouteData( ctx sdk.Context, hops []string, diff --git a/x/dex/keeper/place_limit_order.go b/x/dex/keeper/place_limit_order.go new file mode 100644 index 000000000..161707765 --- /dev/null +++ b/x/dex/keeper/place_limit_order.go @@ -0,0 +1,194 @@ +package keeper + +import ( + "context" + "time" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + + math_utils "github.com/neutron-org/neutron/v4/utils/math" + "github.com/neutron-org/neutron/v4/x/dex/types" +) + +// PlaceLimitOrderCore handles the logic for MsgPlaceLimitOrder including bank operations and event emissions. +func (k Keeper) PlaceLimitOrderCore( + goCtx context.Context, + tokenIn string, + tokenOut string, + amountIn math.Int, + tickIndexInToOut int64, + orderType types.LimitOrderType, + goodTil *time.Time, + maxAmountOut *math.Int, + callerAddr sdk.AccAddress, + receiverAddr sdk.AccAddress, +) (trancheKey string, totalInCoin, swapInCoin, swapOutCoin sdk.Coin, err error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + takerTradePairID, err := types.NewTradePairID(tokenIn, tokenOut) + if err != nil { + return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err + } + trancheKey, totalIn, swapInCoin, swapOutCoin, sharesIssued, err := k.ExecutePlaceLimitOrder( + ctx, + takerTradePairID, + amountIn, + tickIndexInToOut, + orderType, + goodTil, + maxAmountOut, + receiverAddr, + ) + if err != nil { + return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err + } + + if swapOutCoin.IsPositive() { + err = k.bankKeeper.SendCoinsFromModuleToAccount( + ctx, + types.ModuleName, + receiverAddr, + sdk.Coins{swapOutCoin}, + ) + if err != nil { + return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err + } + } + + if totalIn.IsPositive() { + totalInCoin = sdk.NewCoin(tokenIn, totalIn) + + err = k.bankKeeper.SendCoinsFromAccountToModule( + ctx, + callerAddr, + types.ModuleName, + sdk.Coins{totalInCoin}, + ) + if err != nil { + return trancheKey, totalInCoin, swapInCoin, swapOutCoin, err + } + } + + // This will never panic because we've already successfully constructed a TradePairID above + pairID := takerTradePairID.MustPairID() + ctx.EventManager().EmitEvent(types.CreatePlaceLimitOrderEvent( + callerAddr, + receiverAddr, + pairID.Token0, + pairID.Token1, + tokenIn, + tokenOut, + totalIn, + tickIndexInToOut, + orderType.String(), + sharesIssued, + trancheKey, + )) + + return trancheKey, totalInCoin, swapInCoin, swapOutCoin, nil +} + +// ExecutePlaceLimitOrder handles the core logic for PlaceLimitOrder -- performing taker a swap +// and (when applicable) adding a maker limit order to the orderbook. +// IT DOES NOT PERFORM ANY BANKING OPERATIONS +func (k Keeper) ExecutePlaceLimitOrder( + ctx sdk.Context, + takerTradePairID *types.TradePairID, + amountIn math.Int, + tickIndexInToOut int64, + orderType types.LimitOrderType, + goodTil *time.Time, + maxAmountOut *math.Int, + receiverAddr sdk.AccAddress, +) (trancheKey string, totalIn math.Int, swapInCoin, swapOutCoin sdk.Coin, sharesIssued math.Int, err error) { + amountLeft := amountIn + + var limitPrice math_utils.PrecDec + limitPrice, err = types.CalcPrice(tickIndexInToOut) + if err != nil { + return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err + } + + // Ensure that after rounding user will get at least 1 token out. + err = types.ValidateFairOutput(amountIn, limitPrice) + if err != nil { + return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err + } + + var orderFilled bool + if orderType.IsTakerOnly() { + swapInCoin, swapOutCoin, err = k.TakerLimitOrderSwap(ctx, *takerTradePairID, amountIn, maxAmountOut, limitPrice, orderType) + } else { + swapInCoin, swapOutCoin, orderFilled, err = k.MakerLimitOrderSwap(ctx, *takerTradePairID, amountIn, limitPrice) + } + if err != nil { + return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err + } + + totalIn = swapInCoin.Amount + amountLeft = amountLeft.Sub(swapInCoin.Amount) + + makerTradePairID := takerTradePairID.Reversed() + makerTickIndexTakerToMaker := tickIndexInToOut * -1 + var placeTranche *types.LimitOrderTranche + placeTranche, err = k.GetOrInitPlaceTranche( + ctx, + makerTradePairID, + makerTickIndexTakerToMaker, + goodTil, + orderType, + ) + if err != nil { + return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err + } + + trancheKey = placeTranche.Key.TrancheKey + trancheUser := k.GetOrInitLimitOrderTrancheUser( + ctx, + makerTradePairID, + makerTickIndexTakerToMaker, + trancheKey, + orderType, + receiverAddr.String(), + ) + + // FOR GTC, JIT & GoodTil try to place a maker limitOrder with remaining Amount + if amountLeft.IsPositive() && !orderFilled && + (orderType.IsGTC() || orderType.IsJIT() || orderType.IsGoodTil()) { + + // Ensure that the maker portion will generate at least 1 token of output + // NOTE: This does mean that a successful taker leg of the trade will be thrown away since the entire tx will fail. + // In most circumstances this seems preferable to executing the taker leg and exiting early before placing a maker + // order with the remaining liquidity. + err = types.ValidateFairOutput(amountLeft, limitPrice) + if err != nil { + return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err + } + placeTranche.PlaceMakerLimitOrder(amountLeft) + trancheUser.SharesOwned = trancheUser.SharesOwned.Add(amountLeft) + + if orderType.HasExpiration() { + goodTilRecord := NewLimitOrderExpiration(placeTranche) + k.SetLimitOrderExpiration(ctx, goodTilRecord) + ctx.GasMeter().ConsumeGas(types.ExpiringLimitOrderGas, "Expiring LimitOrder Fee") + } + + k.SaveTranche(ctx, placeTranche) + + totalIn = totalIn.Add(amountLeft) + sharesIssued = amountLeft + } + + k.SaveTrancheUser(ctx, trancheUser) + + if orderType.IsJIT() { + err = k.AssertCanPlaceJIT(ctx) + if err != nil { + return trancheKey, totalIn, swapInCoin, swapOutCoin, math.ZeroInt(), err + } + k.IncrementJITsInBlock(ctx) + } + + return trancheKey, totalIn, swapInCoin, swapOutCoin, sharesIssued, nil +} diff --git a/x/dex/keeper/withdraw.go b/x/dex/keeper/withdraw.go new file mode 100644 index 000000000..3c1430f35 --- /dev/null +++ b/x/dex/keeper/withdraw.go @@ -0,0 +1,137 @@ +package keeper + +import ( + "context" + + sdkerrors "cosmossdk.io/errors" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/neutron-org/neutron/v4/x/dex/types" +) + +// WithdrawCore handles logic for MsgWithdrawal including bank operations and event emissions. +func (k Keeper) WithdrawCore( + goCtx context.Context, + pairID *types.PairID, + callerAddr sdk.AccAddress, + receiverAddr sdk.AccAddress, + sharesToRemoveList []math.Int, + tickIndicesNormalized []int64, + fees []uint64, +) error { + ctx := sdk.UnwrapSDKContext(goCtx) + + totalReserve0ToRemove, totalReserve1ToRemove, events, err := k.ExecuteWithdraw( + ctx, + pairID, + callerAddr, + receiverAddr, + sharesToRemoveList, + tickIndicesNormalized, + fees, + ) + if err != nil { + return err + } + + ctx.EventManager().EmitEvents(events) + + if totalReserve0ToRemove.IsPositive() { + coin0 := sdk.NewCoin(pairID.Token0, totalReserve0ToRemove) + + err := k.bankKeeper.SendCoinsFromModuleToAccount( + ctx, + types.ModuleName, + receiverAddr, + sdk.Coins{coin0}, + ) + ctx.EventManager().EmitEvents(types.GetEventsWithdrawnAmount(sdk.Coins{coin0})) + if err != nil { + return err + } + } + + if totalReserve1ToRemove.IsPositive() { + coin1 := sdk.NewCoin(pairID.Token1, totalReserve1ToRemove) + err := k.bankKeeper.SendCoinsFromModuleToAccount( + ctx, + types.ModuleName, + receiverAddr, + sdk.Coins{coin1}, + ) + ctx.EventManager().EmitEvents(types.GetEventsWithdrawnAmount(sdk.Coins{coin1})) + if err != nil { + return err + } + } + + return nil +} + +// ExecuteWithdraw handles the core Withdraw logic including calculating and withdrawing reserve0,reserve1 from a specified tick +// given a specified number of shares to remove. +// Calculates the amount of reserve0, reserve1 to withdraw based on the percentage of the desired +// number of shares to remove compared to the total number of shares at the given tick. +// IT DOES NOT PERFORM ANY BANKING OPERATIONS. +func (k Keeper) ExecuteWithdraw( + ctx sdk.Context, + pairID *types.PairID, + callerAddr sdk.AccAddress, + receiverAddr sdk.AccAddress, + sharesToRemoveList []math.Int, + tickIndicesNormalized []int64, + fees []uint64, +) (totalReserves0ToRemove, totalReserves1ToRemove math.Int, events sdk.Events, err error) { + totalReserve0ToRemove := math.ZeroInt() + totalReserve1ToRemove := math.ZeroInt() + + for i, fee := range fees { + sharesToRemove := sharesToRemoveList[i] + tickIndex := tickIndicesNormalized[i] + + pool, err := k.GetOrInitPool(ctx, pairID, tickIndex, fee) + if err != nil { + return math.ZeroInt(), math.ZeroInt(), nil, err + } + + poolDenom := pool.GetPoolDenom() + + totalShares := k.bankKeeper.GetSupply(ctx, poolDenom).Amount + if totalShares.LT(sharesToRemove) { + return math.ZeroInt(), math.ZeroInt(), nil, sdkerrors.Wrapf( + types.ErrInsufficientShares, + "%s does not have %s shares of type %s", + callerAddr, + sharesToRemove, + poolDenom, + ) + } + + outAmount0, outAmount1 := pool.Withdraw(sharesToRemove, totalShares) + k.SetPool(ctx, pool) + + if sharesToRemove.IsPositive() { + if err := k.BurnShares(ctx, callerAddr, sharesToRemove, poolDenom); err != nil { + return math.ZeroInt(), math.ZeroInt(), nil, err + } + } + + totalReserve0ToRemove = totalReserve0ToRemove.Add(outAmount0) + totalReserve1ToRemove = totalReserve1ToRemove.Add(outAmount1) + + withdrawEvent := types.CreateWithdrawEvent( + callerAddr, + receiverAddr, + pairID.Token0, + pairID.Token1, + tickIndex, + fee, + outAmount0, + outAmount1, + sharesToRemove, + ) + events = append(events, withdrawEvent) + } + return totalReserve0ToRemove, totalReserve1ToRemove, events, nil +} diff --git a/x/dex/keeper/withdraw_filled_limit_order.go b/x/dex/keeper/withdraw_filled_limit_order.go new file mode 100644 index 000000000..bce7910d8 --- /dev/null +++ b/x/dex/keeper/withdraw_filled_limit_order.go @@ -0,0 +1,108 @@ +package keeper + +import ( + "context" + + sdkerrors "cosmossdk.io/errors" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/neutron-org/neutron/v4/x/dex/types" +) + +// WithdrawFilledLimitOrderCore handles MsgWithdrawFilledLimitOrder including bank operations and event emissions. +func (k Keeper) WithdrawFilledLimitOrderCore( + goCtx context.Context, + trancheKey string, + callerAddr sdk.AccAddress, +) error { + ctx := sdk.UnwrapSDKContext(goCtx) + + amountOutTokenOut, remainingTokenIn, tradePairID, err := k.ExecuteWithdrawFilledLimitOrder(ctx, trancheKey, callerAddr) + if err != nil { + return err + } + + coinTakerDenomOut := sdk.NewCoin(tradePairID.TakerDenom, amountOutTokenOut) + coinMakerDenomRefund := sdk.NewCoin(tradePairID.MakerDenom, remainingTokenIn) + // NOTE: it is possible for coinTakerDenomOut xor coinMakerDenomOut to be zero. These are removed by the sanitize call in sdk.NewCoins + // ExecuteWithdrawFilledLimitOrder ensures that at least one of these has am amount > 0. + coins := sdk.NewCoins(coinTakerDenomOut, coinMakerDenomRefund) + ctx.EventManager().EmitEvents(types.GetEventsWithdrawnAmount(sdk.NewCoins(coinTakerDenomOut))) + if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, callerAddr, coins); err != nil { + return err + } + + // This will never panic since TradePairID has already been successfully constructed by ExecuteWithdrawFilledLimitOrder + pairID := tradePairID.MustPairID() + ctx.EventManager().EmitEvent(types.WithdrawFilledLimitOrderEvent( + callerAddr, + pairID.Token0, + pairID.Token1, + tradePairID.MakerDenom, + tradePairID.TakerDenom, + amountOutTokenOut, + trancheKey, + )) + + return nil +} + +// ExecuteWithdrawFilledLimitOrder handles the for logic for WithdrawFilledLimitOrder -- calculates and sends filled liquidity from module to user, +// returns any remaining TokenIn from inactive limit orders, and updates the LimitOrderTranche and LimitOrderTrancheUser. +// IT DOES NOT PERFORM ANY BANKING OPERATIONS +func (k Keeper) ExecuteWithdrawFilledLimitOrder( + ctx sdk.Context, + trancheKey string, + callerAddr sdk.AccAddress, +) (amountOutTokenOut, remainingTokenIn math.Int, tradePairID *types.TradePairID, err error) { + trancheUser, found := k.GetLimitOrderTrancheUser( + ctx, + callerAddr.String(), + trancheKey, + ) + if !found { + return math.ZeroInt(), math.ZeroInt(), nil, sdkerrors.Wrapf(types.ErrValidLimitOrderTrancheNotFound, "%s", trancheKey) + } + + tradePairID, tickIndex := trancheUser.TradePairId, trancheUser.TickIndexTakerToMaker + + tranche, wasFilled, found := k.FindLimitOrderTranche( + ctx, + &types.LimitOrderTrancheKey{ + TradePairId: tradePairID, + TickIndexTakerToMaker: tickIndex, + TrancheKey: trancheKey, + }, + ) + + amountOutTokenOut = math.ZeroInt() + remainingTokenIn = math.ZeroInt() + // It's possible that a TrancheUser exists but tranche does not if LO was filled entirely through a swap + if found { + var amountOutTokenIn math.Int + amountOutTokenIn, amountOutTokenOut = tranche.Withdraw(trancheUser) + + if wasFilled { + // This is only relevant for inactive JIT and GoodTil limit orders + remainingTokenIn = tranche.RemoveTokenIn(trancheUser) + k.SaveInactiveTranche(ctx, tranche) + + // Treat the removed tokenIn as cancelled shares + trancheUser.SharesCancelled = trancheUser.SharesCancelled.Add(remainingTokenIn) + + } else { + k.SetLimitOrderTranche(ctx, tranche) + } + + trancheUser.SharesWithdrawn = trancheUser.SharesWithdrawn.Add(amountOutTokenIn) + } + + k.SaveTrancheUser(ctx, trancheUser) + + if !amountOutTokenOut.IsPositive() && !remainingTokenIn.IsPositive() { + return math.ZeroInt(), math.ZeroInt(), tradePairID, types.ErrWithdrawEmptyLimitOrder + } + + return amountOutTokenOut, remainingTokenIn, tradePairID, nil +} From 5da37b695275b3196b2fde153283cc35275a0fdc Mon Sep 17 00:00:00 2001 From: Julian Compagni Portis Date: Tue, 9 Jul 2024 16:03:53 -0400 Subject: [PATCH 3/5] remove burn logic from ExecuteWithdraw --- x/dex/keeper/withdraw.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/x/dex/keeper/withdraw.go b/x/dex/keeper/withdraw.go index 3c1430f35..59ffeca8b 100644 --- a/x/dex/keeper/withdraw.go +++ b/x/dex/keeper/withdraw.go @@ -22,7 +22,7 @@ func (k Keeper) WithdrawCore( ) error { ctx := sdk.UnwrapSDKContext(goCtx) - totalReserve0ToRemove, totalReserve1ToRemove, events, err := k.ExecuteWithdraw( + totalReserve0ToRemove, totalReserve1ToRemove, sharesToRemove, poolDenom, events, err := k.ExecuteWithdraw( ctx, pairID, callerAddr, @@ -37,6 +37,12 @@ func (k Keeper) WithdrawCore( ctx.EventManager().EmitEvents(events) + if sharesToRemove.IsPositive() { + if err := k.BurnShares(ctx, callerAddr, sharesToRemove, poolDenom); err != nil { + return err + } + } + if totalReserve0ToRemove.IsPositive() { coin0 := sdk.NewCoin(pairID.Token0, totalReserve0ToRemove) @@ -82,7 +88,7 @@ func (k Keeper) ExecuteWithdraw( sharesToRemoveList []math.Int, tickIndicesNormalized []int64, fees []uint64, -) (totalReserves0ToRemove, totalReserves1ToRemove math.Int, events sdk.Events, err error) { +) (totalReserves0ToRemove, totalReserves1ToRemove math.Int, sharesToRemove math.Int, poolDenom string, events sdk.Events, err error) { totalReserve0ToRemove := math.ZeroInt() totalReserve1ToRemove := math.ZeroInt() @@ -92,14 +98,14 @@ func (k Keeper) ExecuteWithdraw( pool, err := k.GetOrInitPool(ctx, pairID, tickIndex, fee) if err != nil { - return math.ZeroInt(), math.ZeroInt(), nil, err + return math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), "", nil, err } poolDenom := pool.GetPoolDenom() totalShares := k.bankKeeper.GetSupply(ctx, poolDenom).Amount if totalShares.LT(sharesToRemove) { - return math.ZeroInt(), math.ZeroInt(), nil, sdkerrors.Wrapf( + return math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), poolDenom, nil, sdkerrors.Wrapf( types.ErrInsufficientShares, "%s does not have %s shares of type %s", callerAddr, @@ -111,12 +117,6 @@ func (k Keeper) ExecuteWithdraw( outAmount0, outAmount1 := pool.Withdraw(sharesToRemove, totalShares) k.SetPool(ctx, pool) - if sharesToRemove.IsPositive() { - if err := k.BurnShares(ctx, callerAddr, sharesToRemove, poolDenom); err != nil { - return math.ZeroInt(), math.ZeroInt(), nil, err - } - } - totalReserve0ToRemove = totalReserve0ToRemove.Add(outAmount0) totalReserve1ToRemove = totalReserve1ToRemove.Add(outAmount1) @@ -133,5 +133,5 @@ func (k Keeper) ExecuteWithdraw( ) events = append(events, withdrawEvent) } - return totalReserve0ToRemove, totalReserve1ToRemove, events, nil + return totalReserve0ToRemove, totalReserve1ToRemove, sharesToRemove, poolDenom, events, nil } From 8a4e47b1a85671fc3ef7b1c9a19ec041c113c82a Mon Sep 17 00:00:00 2001 From: Julian Compagni Portis Date: Tue, 9 Jul 2024 16:04:21 -0400 Subject: [PATCH 4/5] cleanup --- x/dex/keeper/multihop_swap.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x/dex/keeper/multihop_swap.go b/x/dex/keeper/multihop_swap.go index f81f8f8c4..ab1ad4798 100644 --- a/x/dex/keeper/multihop_swap.go +++ b/x/dex/keeper/multihop_swap.go @@ -38,7 +38,7 @@ func (k Keeper) MultiHopSwapCore( ) (coinOut sdk.Coin, err error) { ctx := sdk.UnwrapSDKContext(goCtx) - bestRoute, initialInCoin, err := k.ExecuteMultiHopSwap(ctx, amountIn, routes, exitLimitPrice, pickBestRoute) + bestRoute, initialInCoin, err := k.CalulateMultiHopSwap(ctx, amountIn, routes, exitLimitPrice, pickBestRoute) if err != nil { return sdk.Coin{}, err } @@ -80,9 +80,9 @@ func (k Keeper) MultiHopSwapCore( return bestRoute.coinOut, nil } -// ExecuteMultiHopSwap handles the core logic for MultiHopSwap -- simulating swap operations across all routes (when applicable) -// and picking the best route to execute. IT DOES NOT PERFORM ANY BANKING OPERATIONS. -func (k Keeper) ExecuteMultiHopSwap( +// CalulateMultiHopSwap handles the core logic for MultiHopSwap -- simulating swap operations across all routes (when applicable) +// and picking the best route to execute. It uses a cache and does not modify state. +func (k Keeper) CalulateMultiHopSwap( ctx sdk.Context, amountIn math.Int, routes []*types.MultiHopRoute, From 6f5c0ba64980bf731d6c3d33905b78ac403b9286 Mon Sep 17 00:00:00 2001 From: Julian Compagni Portis Date: Tue, 9 Jul 2024 16:51:11 -0400 Subject: [PATCH 5/5] don't burn shares in ExecuteWithdraw --- x/dex/keeper/core_helper.go | 8 +++----- x/dex/keeper/withdraw.go | 23 +++++++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/x/dex/keeper/core_helper.go b/x/dex/keeper/core_helper.go index a67b248e1..e608f3c25 100644 --- a/x/dex/keeper/core_helper.go +++ b/x/dex/keeper/core_helper.go @@ -142,16 +142,14 @@ func (k Keeper) MintShares(ctx sdk.Context, addr sdk.AccAddress, sharesCoins sdk func (k Keeper) BurnShares( ctx sdk.Context, addr sdk.AccAddress, - amount math.Int, - sharesID string, + coins sdk.Coins, ) error { - sharesCoins := sdk.Coins{sdk.NewCoin(sharesID, amount)} // transfer tokens to module - if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, addr, types.ModuleName, sharesCoins); err != nil { + if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, addr, types.ModuleName, coins); err != nil { return err } // burn tokens - err := k.bankKeeper.BurnCoins(ctx, types.ModuleName, sharesCoins) + err := k.bankKeeper.BurnCoins(ctx, types.ModuleName, coins) return err } diff --git a/x/dex/keeper/withdraw.go b/x/dex/keeper/withdraw.go index 59ffeca8b..ff81dd3f1 100644 --- a/x/dex/keeper/withdraw.go +++ b/x/dex/keeper/withdraw.go @@ -22,7 +22,7 @@ func (k Keeper) WithdrawCore( ) error { ctx := sdk.UnwrapSDKContext(goCtx) - totalReserve0ToRemove, totalReserve1ToRemove, sharesToRemove, poolDenom, events, err := k.ExecuteWithdraw( + totalReserve0ToRemove, totalReserve1ToRemove, coinsToBurn, events, err := k.ExecuteWithdraw( ctx, pairID, callerAddr, @@ -37,10 +37,8 @@ func (k Keeper) WithdrawCore( ctx.EventManager().EmitEvents(events) - if sharesToRemove.IsPositive() { - if err := k.BurnShares(ctx, callerAddr, sharesToRemove, poolDenom); err != nil { - return err - } + if err := k.BurnShares(ctx, callerAddr, coinsToBurn); err != nil { + return err } if totalReserve0ToRemove.IsPositive() { @@ -88,7 +86,7 @@ func (k Keeper) ExecuteWithdraw( sharesToRemoveList []math.Int, tickIndicesNormalized []int64, fees []uint64, -) (totalReserves0ToRemove, totalReserves1ToRemove math.Int, sharesToRemove math.Int, poolDenom string, events sdk.Events, err error) { +) (totalReserves0ToRemove, totalReserves1ToRemove math.Int, coinsToBurn sdk.Coins, events sdk.Events, err error) { totalReserve0ToRemove := math.ZeroInt() totalReserve1ToRemove := math.ZeroInt() @@ -98,14 +96,17 @@ func (k Keeper) ExecuteWithdraw( pool, err := k.GetOrInitPool(ctx, pairID, tickIndex, fee) if err != nil { - return math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), "", nil, err + return math.ZeroInt(), math.ZeroInt(), nil, nil, err } poolDenom := pool.GetPoolDenom() - totalShares := k.bankKeeper.GetSupply(ctx, poolDenom).Amount + // TODO: this is a bit hacky. Since it is possible to have multiple withdrawals from the same pool we have to artificially update the bank balance + // In the future we should enforce only one withdraw operation per pool in the message validation + alreadyWithdrawnOfDenom := coinsToBurn.AmountOf(poolDenom) + totalShares := k.bankKeeper.GetSupply(ctx, poolDenom).Amount.Sub(alreadyWithdrawnOfDenom) if totalShares.LT(sharesToRemove) { - return math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), poolDenom, nil, sdkerrors.Wrapf( + return math.ZeroInt(), math.ZeroInt(), nil, nil, sdkerrors.Wrapf( types.ErrInsufficientShares, "%s does not have %s shares of type %s", callerAddr, @@ -120,6 +121,8 @@ func (k Keeper) ExecuteWithdraw( totalReserve0ToRemove = totalReserve0ToRemove.Add(outAmount0) totalReserve1ToRemove = totalReserve1ToRemove.Add(outAmount1) + coinsToBurn = coinsToBurn.Add(sdk.NewCoin(poolDenom, sharesToRemove)) + withdrawEvent := types.CreateWithdrawEvent( callerAddr, receiverAddr, @@ -133,5 +136,5 @@ func (k Keeper) ExecuteWithdraw( ) events = append(events, withdrawEvent) } - return totalReserve0ToRemove, totalReserve1ToRemove, sharesToRemove, poolDenom, events, nil + return totalReserve0ToRemove, totalReserve1ToRemove, coinsToBurn, events, nil }