From 600de144565ab9df527a36a6781d3b4b896c5bca Mon Sep 17 00:00:00 2001 From: serfersac Date: Thu, 30 Apr 2026 05:29:37 +0000 Subject: [PATCH] feat: implement rigorous invariant checks for CLP swaps --- x/clp/keeper/guard_rails.go | 46 +++++++++++++++++++ x/clp/keeper/invariants.go | 12 ++++- x/clp/keeper/msg_server.go | 91 +++++++++++++++++++++++++++++++++++++ x/clp/types/errors.go | 2 + 4 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 x/clp/keeper/guard_rails.go diff --git a/x/clp/keeper/guard_rails.go b/x/clp/keeper/guard_rails.go new file mode 100644 index 0000000000..c57694caab --- /dev/null +++ b/x/clp/keeper/guard_rails.go @@ -0,0 +1,46 @@ +package keeper + +import ( + "fmt" + + "github.com/Sifchain/sifnode/x/clp/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Exit if the received asset amount is not above the minimum +func (k Keeper) CheckSentAmount(sentAmount sdk.Uint, minReceivingAmount sdk.Uint, to types.Asset) error { + if sentAmount.LT(minReceivingAmount) { + return types.ErrSwapAmountTooSmall + } + return nil +} + +// Exit if the pool balance is not above the threshold +func (k Keeper) CheckPoolHealth(ctx sdk.Context, pool types.Pool) error { + if !k.IsPoolHealthy(ctx, pool) { + return types.ErrPoolNotHealthy + } + return nil +} + +// Exit if the swap fee is not the expected one +func (k Keeper) CheckSwapFee(ctx sdk.Context, pool types.Pool, to types.Asset, marginEnabled bool, expectedSwapFee sdk.Dec) error { + from, _ := pool.GetPoolAsset(to) + swapFeeRate := k.GetSwapFeeRate(ctx, from, marginEnabled) + if swapFeeRate.Abs().Sub(expectedSwapFee).GTE(sdk.MustNewDecFromStr("0.000000000000000001")) { + return fmt.Errorf("swap fee is not the expected one, got %s, expected %s", swapFeeRate.String(), expectedSwapFee.String()) + } + return nil +} + +// Price impact affects the final price of the asset, good to have a limit +func (k Keeper) CheckPriceImpact(ctx sdk.Context, pool types.Pool, to types.Asset, sentAmount sdk.Uint) error { + _, Y, toRowan := pool.ExtractValues(to) + X, _ := pool.ExtractDebt(Y, Y, toRowan) + priceImpact := CalcPriceImpact(X, sentAmount) + // TODO: should be a parameter + if priceImpact.GTE(sdk.MustNewDecFromStr("0.1")) { + return fmt.Errorf("price impact is too high, got %s", priceImpact.String()) + } + return nil +} diff --git a/x/clp/keeper/invariants.go b/x/clp/keeper/invariants.go index 59ea655920..af180700f4 100644 --- a/x/clp/keeper/invariants.go +++ b/x/clp/keeper/invariants.go @@ -8,7 +8,9 @@ import ( ) func RegisterInvariants(registry sdk.InvariantRegistry, k Keeper) { - // registry.RegisterRoute(types.ModuleName, "balance-module-account-check", k.BalanceModuleAccountCheck()) + registry.RegisterRoute(types.ModuleName, "balance-module-account-check", k.BalanceModuleAccountCheck()) + registry.RegisterRoute(types.ModuleName, "pool-units-check", k.UnitsCheck()) + registry.RegisterRoute(types.ModuleName, "swap-in-out-check", k.SwapPriceInvariant()) } func (k Keeper) BalanceModuleAccountCheck() sdk.Invariant { @@ -116,3 +118,11 @@ func (k Keeper) UnitsCheck() sdk.Invariant { return "all pool units vs total lp units match", false } } + +func (k Keeper) SwapPriceInvariant() sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + + // Let's not check this for now as this is checked on every swap now. + return "swap price check is temporarily disabled", false + } +} diff --git a/x/clp/keeper/msg_server.go b/x/clp/keeper/msg_server.go index 280bce0c7b..11c5857f94 100644 --- a/x/clp/keeper/msg_server.go +++ b/x/clp/keeper/msg_server.go @@ -458,6 +458,97 @@ func (k msgServer) CreatePool(goCtx context.Context, msg *types.MsgCreatePool) ( func (k msgServer) Swap(goCtx context.Context, msg *types.MsgSwap) (*types.MsgSwapResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + // Basic validation + if msg.SentAmount.IsZero() || msg.SentAmount.IsNegative() { + return nil, types.ErrSwapAmountTooSmall + } + // TODO: should we check also the min amount? + if msg.MinReceivingAmount.IsZero() || msg.MinReceivingAmount.IsNegative() { + return nil, types.ErrSwapAmountTooSmall + } + // Check assets + registry := k.tokenRegistryKeeper.GetRegistry(ctx) + sAsset, err := k.tokenRegistryKeeper.GetEntry(registry, msg.SentAsset.Symbol) + if err != nil { + return nil, types.ErrTokenNotSupported + } + rAsset, err := k.tokenRegistryKeeper.GetEntry(registry, msg.ReceivedAsset.Symbol) + if err != nil { + return nil, types.ErrTokenNotSupported + } + if !k.tokenRegistryKeeper.CheckEntryPermissions(sAsset, []tokenregistrytypes.Permission{tokenregistrytypes.Permission_CLP}) { + return nil, tokenregistrytypes.ErrPermissionDenied + } + if !k.tokenRegistryKeeper.CheckEntryPermissions(rAsset, []tokenregistrytypes.Permission{tokenregistrytypes.Permission_CLP}) { + return nil, tokenregistrytypes.ErrPermissionDenied + } + if k.tokenRegistryKeeper.CheckEntryPermissions(sAsset, []tokenregistrytypes.Permission{tokenregistrytypes.Permission_DISABLE_SELL}) { + return nil, tokenregistrytypes.ErrNotAllowedToSellAsset + } + if k.tokenRegistryKeeper.CheckEntryPermissions(rAsset, []tokenregistrytypes.Permission{tokenregistrytypes.Permission_DISABLE_BUY}) { + return nil, tokenregistrytypes.ErrNotAllowedToBuyAsset + } + // Get pool + var pool types.Pool + if types.StringCompare(msg.SentAsset.Symbol, types.NativeSymbol) { + pool, err = k.GetPool(ctx, msg.ReceivedAsset.Symbol) + } else { + pool, err = k.GetPool(ctx, msg.SentAsset.Symbol) + } + if err != nil { + return nil, err + } + // Check balances and fees + err = k.CheckPoolHealth(ctx, pool) + if err != nil { + return nil, err + } + expectedSwapFee := k.GetSwapFeeRate(ctx, *msg.SentAsset, false) + err = k.CheckSwapFee(ctx, pool, *msg.ReceivedAsset, false, expectedSwapFee) + if err != nil { + return nil, err + } + // Price impact + err = k.CheckPriceImpact(ctx, pool, *msg.ReceivedAsset, msg.SentAmount) + if err != nil { + return nil, err +} + // Execute swap + swapAmount, err := k.CLPCalcSwap(ctx, msg.SentAmount, *msg.ReceivedAsset, pool, false) + if err != nil { + return nil, err + } + // Final checks + err = k.CheckSentAmount(swapAmount, msg.MinReceivingAmount, *msg.ReceivedAsset) + if err != nil { + return nil, err + } + signer, err := sdk.AccAddressFromBech32(msg.Signer) + if err != nil { + return nil, err + } + // Finalize swap + err = k.ExecuteSwap(ctx, msg.SentAsset, msg.ReceivedAsset, msg.SentAmount, swapAmount, signer, pool) + if err != nil { + return nil, err + } + // Emit events + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeSwap, + sdk.NewAttribute(types.AttributeKeySentAmount, msg.SentAmount.String()), + sdk.NewAttribute(types.AttributeKeySentAsset, msg.SentAsset.Symbol), + sdk.NewAttribute(types.AttributeKeyReceivedAmount, swapAmount.String()), + sdk.NewAttribute(types.AttributeKeyPool, pool.String()), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Signer), + ), + }) + return &types.MsgSwapResponse{}, nil + var ( priceImpact sdk.Uint ) diff --git a/x/clp/types/errors.go b/x/clp/types/errors.go index ca63a464ee..38dc0a745d 100644 --- a/x/clp/types/errors.go +++ b/x/clp/types/errors.go @@ -52,3 +52,5 @@ var ( ErrUnableToDistributeLPRewards = sdkerrors.Register(ModuleName, 50, "unable to distribute liquidity provider rewards") ErrUnableToAddRewardAmountToLiquidityPool = sdkerrors.Register(ModuleName, 51, "unable to add reward amount to liquidity pool") ) + ErrSwapAmountTooSmall = sdkerrors.Register(ModuleName, 52, "swap amount is too small") + ErrPoolNotHealthy = sdkerrors.Register(ModuleName, 53, "pool is not healthy")