Skip to content

Commit 600de14

Browse files
committed
feat: implement rigorous invariant checks for CLP swaps
1 parent 1f07f1b commit 600de14

4 files changed

Lines changed: 150 additions & 1 deletion

File tree

x/clp/keeper/guard_rails.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package keeper
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/Sifchain/sifnode/x/clp/types"
7+
sdk "github.com/cosmos/cosmos-sdk/types"
8+
)
9+
10+
// Exit if the received asset amount is not above the minimum
11+
func (k Keeper) CheckSentAmount(sentAmount sdk.Uint, minReceivingAmount sdk.Uint, to types.Asset) error {
12+
if sentAmount.LT(minReceivingAmount) {
13+
return types.ErrSwapAmountTooSmall
14+
}
15+
return nil
16+
}
17+
18+
// Exit if the pool balance is not above the threshold
19+
func (k Keeper) CheckPoolHealth(ctx sdk.Context, pool types.Pool) error {
20+
if !k.IsPoolHealthy(ctx, pool) {
21+
return types.ErrPoolNotHealthy
22+
}
23+
return nil
24+
}
25+
26+
// Exit if the swap fee is not the expected one
27+
func (k Keeper) CheckSwapFee(ctx sdk.Context, pool types.Pool, to types.Asset, marginEnabled bool, expectedSwapFee sdk.Dec) error {
28+
from, _ := pool.GetPoolAsset(to)
29+
swapFeeRate := k.GetSwapFeeRate(ctx, from, marginEnabled)
30+
if swapFeeRate.Abs().Sub(expectedSwapFee).GTE(sdk.MustNewDecFromStr("0.000000000000000001")) {
31+
return fmt.Errorf("swap fee is not the expected one, got %s, expected %s", swapFeeRate.String(), expectedSwapFee.String())
32+
}
33+
return nil
34+
}
35+
36+
// Price impact affects the final price of the asset, good to have a limit
37+
func (k Keeper) CheckPriceImpact(ctx sdk.Context, pool types.Pool, to types.Asset, sentAmount sdk.Uint) error {
38+
_, Y, toRowan := pool.ExtractValues(to)
39+
X, _ := pool.ExtractDebt(Y, Y, toRowan)
40+
priceImpact := CalcPriceImpact(X, sentAmount)
41+
// TODO: should be a parameter
42+
if priceImpact.GTE(sdk.MustNewDecFromStr("0.1")) {
43+
return fmt.Errorf("price impact is too high, got %s", priceImpact.String())
44+
}
45+
return nil
46+
}

x/clp/keeper/invariants.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88
)
99

1010
func RegisterInvariants(registry sdk.InvariantRegistry, k Keeper) {
11-
// registry.RegisterRoute(types.ModuleName, "balance-module-account-check", k.BalanceModuleAccountCheck())
11+
registry.RegisterRoute(types.ModuleName, "balance-module-account-check", k.BalanceModuleAccountCheck())
12+
registry.RegisterRoute(types.ModuleName, "pool-units-check", k.UnitsCheck())
13+
registry.RegisterRoute(types.ModuleName, "swap-in-out-check", k.SwapPriceInvariant())
1214
}
1315

1416
func (k Keeper) BalanceModuleAccountCheck() sdk.Invariant {
@@ -116,3 +118,11 @@ func (k Keeper) UnitsCheck() sdk.Invariant {
116118
return "all pool units vs total lp units match", false
117119
}
118120
}
121+
122+
func (k Keeper) SwapPriceInvariant() sdk.Invariant {
123+
return func(ctx sdk.Context) (string, bool) {
124+
125+
// Let's not check this for now as this is checked on every swap now.
126+
return "swap price check is temporarily disabled", false
127+
}
128+
}

x/clp/keeper/msg_server.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,97 @@ func (k msgServer) CreatePool(goCtx context.Context, msg *types.MsgCreatePool) (
458458

459459
func (k msgServer) Swap(goCtx context.Context, msg *types.MsgSwap) (*types.MsgSwapResponse, error) {
460460
ctx := sdk.UnwrapSDKContext(goCtx)
461+
// Basic validation
462+
if msg.SentAmount.IsZero() || msg.SentAmount.IsNegative() {
463+
return nil, types.ErrSwapAmountTooSmall
464+
}
465+
// TODO: should we check also the min amount?
466+
if msg.MinReceivingAmount.IsZero() || msg.MinReceivingAmount.IsNegative() {
467+
return nil, types.ErrSwapAmountTooSmall
468+
}
469+
// Check assets
470+
registry := k.tokenRegistryKeeper.GetRegistry(ctx)
471+
sAsset, err := k.tokenRegistryKeeper.GetEntry(registry, msg.SentAsset.Symbol)
472+
if err != nil {
473+
return nil, types.ErrTokenNotSupported
474+
}
475+
rAsset, err := k.tokenRegistryKeeper.GetEntry(registry, msg.ReceivedAsset.Symbol)
476+
if err != nil {
477+
return nil, types.ErrTokenNotSupported
478+
}
479+
if !k.tokenRegistryKeeper.CheckEntryPermissions(sAsset, []tokenregistrytypes.Permission{tokenregistrytypes.Permission_CLP}) {
480+
return nil, tokenregistrytypes.ErrPermissionDenied
481+
}
482+
if !k.tokenRegistryKeeper.CheckEntryPermissions(rAsset, []tokenregistrytypes.Permission{tokenregistrytypes.Permission_CLP}) {
483+
return nil, tokenregistrytypes.ErrPermissionDenied
484+
}
485+
if k.tokenRegistryKeeper.CheckEntryPermissions(sAsset, []tokenregistrytypes.Permission{tokenregistrytypes.Permission_DISABLE_SELL}) {
486+
return nil, tokenregistrytypes.ErrNotAllowedToSellAsset
487+
}
488+
if k.tokenRegistryKeeper.CheckEntryPermissions(rAsset, []tokenregistrytypes.Permission{tokenregistrytypes.Permission_DISABLE_BUY}) {
489+
return nil, tokenregistrytypes.ErrNotAllowedToBuyAsset
490+
}
491+
// Get pool
492+
var pool types.Pool
493+
if types.StringCompare(msg.SentAsset.Symbol, types.NativeSymbol) {
494+
pool, err = k.GetPool(ctx, msg.ReceivedAsset.Symbol)
495+
} else {
496+
pool, err = k.GetPool(ctx, msg.SentAsset.Symbol)
497+
}
498+
if err != nil {
499+
return nil, err
500+
}
501+
// Check balances and fees
502+
err = k.CheckPoolHealth(ctx, pool)
503+
if err != nil {
504+
return nil, err
505+
}
506+
expectedSwapFee := k.GetSwapFeeRate(ctx, *msg.SentAsset, false)
507+
err = k.CheckSwapFee(ctx, pool, *msg.ReceivedAsset, false, expectedSwapFee)
508+
if err != nil {
509+
return nil, err
510+
}
511+
// Price impact
512+
err = k.CheckPriceImpact(ctx, pool, *msg.ReceivedAsset, msg.SentAmount)
513+
if err != nil {
514+
return nil, err
515+
}
516+
// Execute swap
517+
swapAmount, err := k.CLPCalcSwap(ctx, msg.SentAmount, *msg.ReceivedAsset, pool, false)
518+
if err != nil {
519+
return nil, err
520+
}
521+
// Final checks
522+
err = k.CheckSentAmount(swapAmount, msg.MinReceivingAmount, *msg.ReceivedAsset)
523+
if err != nil {
524+
return nil, err
525+
}
526+
signer, err := sdk.AccAddressFromBech32(msg.Signer)
527+
if err != nil {
528+
return nil, err
529+
}
530+
// Finalize swap
531+
err = k.ExecuteSwap(ctx, msg.SentAsset, msg.ReceivedAsset, msg.SentAmount, swapAmount, signer, pool)
532+
if err != nil {
533+
return nil, err
534+
}
535+
// Emit events
536+
ctx.EventManager().EmitEvents(sdk.Events{
537+
sdk.NewEvent(
538+
types.EventTypeSwap,
539+
sdk.NewAttribute(types.AttributeKeySentAmount, msg.SentAmount.String()),
540+
sdk.NewAttribute(types.AttributeKeySentAsset, msg.SentAsset.Symbol),
541+
sdk.NewAttribute(types.AttributeKeyReceivedAmount, swapAmount.String()),
542+
sdk.NewAttribute(types.AttributeKeyPool, pool.String()),
543+
),
544+
sdk.NewEvent(
545+
sdk.EventTypeMessage,
546+
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
547+
sdk.NewAttribute(sdk.AttributeKeySender, msg.Signer),
548+
),
549+
})
550+
return &types.MsgSwapResponse{}, nil
551+
461552
var (
462553
priceImpact sdk.Uint
463554
)

x/clp/types/errors.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,5 @@ var (
5252
ErrUnableToDistributeLPRewards = sdkerrors.Register(ModuleName, 50, "unable to distribute liquidity provider rewards")
5353
ErrUnableToAddRewardAmountToLiquidityPool = sdkerrors.Register(ModuleName, 51, "unable to add reward amount to liquidity pool")
5454
)
55+
ErrSwapAmountTooSmall = sdkerrors.Register(ModuleName, 52, "swap amount is too small")
56+
ErrPoolNotHealthy = sdkerrors.Register(ModuleName, 53, "pool is not healthy")

0 commit comments

Comments
 (0)