diff --git a/router/usecase/pools/routable_cw_alloy_transmuter_pool.go b/router/usecase/pools/routable_cw_alloy_transmuter_pool.go index d9ecb8a9f..543906ced 100644 --- a/router/usecase/pools/routable_cw_alloy_transmuter_pool.go +++ b/router/usecase/pools/routable_cw_alloy_transmuter_pool.go @@ -3,7 +3,6 @@ package pools import ( "context" "fmt" - "strings" "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" @@ -29,10 +28,6 @@ type routableAlloyTransmuterPoolImpl struct { SpreadFactor osmomath.Dec "json:\"spread_factor\"" } -const ( - alloyedLPShareDenomComponent = "all" -) - // GetId implements domain.RoutablePool. func (r *routableAlloyTransmuterPoolImpl) GetId() uint64 { return r.ChainPool.PoolId @@ -187,7 +182,6 @@ func (r *routableAlloyTransmuterPoolImpl) CalcTokenOutAmt(tokenIn sdk.Coin, toke } // Check static upper rate limiter - // We only need to check it for the token in coin since that is the only one that is increased by the current quote. if err := r.checkStaticRateLimiter(tokenIn); err != nil { return osmomath.BigDec{}, err } @@ -202,9 +196,14 @@ func (r *routableAlloyTransmuterPoolImpl) CalcTokenOutAmt(tokenIn sdk.Coin, toke return tokenOutAmount, nil } -// checkStaticRateLimiter checks the static rate limiter for the token in coin. +// checkStaticRateLimiter checks the static rate limiter. +// If token in denom is not alloyed, we only need to validate the token in balance. +// Since the token in balance is the only one that is increased by the current quote. +// +// If token in denom is alloyed, we need to validate all assets' balances except token out. +// Since the token out composition is decreasing, other assets' weights are increasing. +// // Note: static rate limit only has an upper limit. -// Therefore, we only need to validate the token in balance. // No-op if the static rate limiter is not set. // Returns error if the token in weight is greater than the upper limit. // Returns nil if the token in weight is less than or equal to the upper limit. @@ -214,12 +213,6 @@ func (r *routableAlloyTransmuterPoolImpl) checkStaticRateLimiter(tokenInCoin sdk return nil } - // Check if the static rate limiter exists for the token in denom updated balance. - tokeInStaticLimiter, ok := r.AlloyTransmuterData.RateLimiterConfig.GetStaticLimiter(tokenInCoin.Denom) - if !ok { - return nil - } - preComputedData := r.AlloyTransmuterData.PreComputedData normalizationFactors := preComputedData.NormalizationScalingFactors @@ -232,8 +225,8 @@ func (r *routableAlloyTransmuterPoolImpl) checkStaticRateLimiter(tokenInCoin sdk assetConfig := r.AlloyTransmuterData.AssetConfigs[i] assetDenom := assetConfig.Denom - // Skip if the asset is alloyed LP hsare - if strings.Contains(assetDenom, alloyedLPShareDenomComponent) { + // Skip if the asset is alloyed LP share + if assetDenom == r.AlloyTransmuterData.AlloyedDenom { continue } @@ -244,6 +237,11 @@ func (r *routableAlloyTransmuterPoolImpl) checkStaticRateLimiter(tokenInCoin sdk assetBalance = assetBalance.Add(tokenInCoin.Amount) } + // Subtract the token out balance from the asset balance + if assetDenom == r.TokenOutDenom { + assetBalance = assetBalance.Sub(tokenInCoin.Amount) + } + normalizationScalingFactor, ok := normalizationFactors[assetDenom] if !ok { return fmt.Errorf("normalization scaling factor not found for asset %s, pool id %d", assetDenom, r.GetId()) @@ -259,34 +257,65 @@ func (r *routableAlloyTransmuterPoolImpl) checkStaticRateLimiter(tokenInCoin sdk normalizeTotal = normalizeTotal.Add(normalizedBalance) } - // Calculate weights - // Note: -1 for the alloyed LP share. - weights := make(map[string]osmomath.Dec, len(r.AlloyTransmuterData.AssetConfigs)-1) - for i := 0; i < len(r.AlloyTransmuterData.AssetConfigs); i++ { - assetConfig := r.AlloyTransmuterData.AssetConfigs[i] - assetDenom := assetConfig.Denom - - // Skip if the asset is alloyed LP hsare - if strings.Contains(assetDenom, alloyedLPShareDenomComponent) { - continue + // If token in denom is alloyed, we need to validate limiters for all assets' balances except token out. + // Since the token out composition is decreasing, other assets' weights are increasing. + // else, we only need to validate the token in denom limiter. + if tokenInCoin.Denom == r.AlloyTransmuterData.AlloyedDenom { + for i := 0; i < len(r.AlloyTransmuterData.AssetConfigs); i++ { + assetConfig := r.AlloyTransmuterData.AssetConfigs[i] + assetDenom := assetConfig.Denom + + // Skip if the asset is alloyed LP share + if assetDenom == r.AlloyTransmuterData.AlloyedDenom { + continue + } + + // skip if the asset is token out, since its weight is decreasing, no need to check limiter + if assetDenom == r.TokenOutDenom { + continue + } + + // Check if the static rate limiter exists for the asset denom updated balance. + // If not, continue to the next asset + staticLimiter, ok := r.AlloyTransmuterData.RateLimiterConfig.GetStaticLimiter(assetDenom) + if !ok { + continue + } + + // Validate upper limit + upperLimitInt := osmomath.MustNewDecFromStr(staticLimiter.UpperLimit) + + // Asset weight + assetWeight := normalizedBalances[assetDenom].ToLegacyDec().Quo(normalizeTotal.ToLegacyDec()) + + // Check the upper limit + if assetWeight.GT(upperLimitInt) { + return domain.StaticRateLimiterInvalidUpperLimitError{ + UpperLimit: staticLimiter.UpperLimit, + Weight: assetWeight.String(), + Denom: assetDenom, + } + } + } + } else { + tokeInStaticLimiter, ok := r.AlloyTransmuterData.RateLimiterConfig.GetStaticLimiter(tokenInCoin.Denom) + if !ok { + return nil } - // Calculate weight - weights[assetDenom] = normalizedBalances[assetDenom].ToLegacyDec().Quo(normalizeTotal.ToLegacyDec()) - } - - // Validate upper limit - upperLimitInt := osmomath.MustNewDecFromStr(tokeInStaticLimiter.UpperLimit) + // Validate upper limit + upperLimitInt := osmomath.MustNewDecFromStr(tokeInStaticLimiter.UpperLimit) - // Token in weight - tokenInWeight := weights[tokenInCoin.Denom] + // Token in weight + tokenInWeight := normalizedBalances[tokenInCoin.Denom].ToLegacyDec().Quo(normalizeTotal.ToLegacyDec()) - // Check the upper limit - if tokenInWeight.GT(upperLimitInt) { - return domain.StaticRateLimiterInvalidUpperLimitError{ - UpperLimit: tokeInStaticLimiter.UpperLimit, - Weight: tokenInWeight.String(), - Denom: tokenInCoin.Denom, + // Check the upper limit + if tokenInWeight.GT(upperLimitInt) { + return domain.StaticRateLimiterInvalidUpperLimitError{ + UpperLimit: tokeInStaticLimiter.UpperLimit, + Weight: tokenInWeight.String(), + Denom: tokenInCoin.Denom, + } } } diff --git a/router/usecase/pools/routable_cw_alloy_transmuter_pool_test.go b/router/usecase/pools/routable_cw_alloy_transmuter_pool_test.go index 9397faf7a..e9f8cc952 100644 --- a/router/usecase/pools/routable_cw_alloy_transmuter_pool_test.go +++ b/router/usecase/pools/routable_cw_alloy_transmuter_pool_test.go @@ -41,10 +41,10 @@ func (s *RoutablePoolTestSuite) SetupRoutableAlloyTransmuterPoolCustom(tokenInDe AlloyTransmuter: &cosmwasmpool.AlloyTransmuterData{ AlloyedDenom: ALLUSD, AssetConfigs: []cosmwasmpool.TransmuterAssetConfig{ - {Denom: USDC, NormalizationFactor: osmomath.NewInt(100)}, - {Denom: USDT, NormalizationFactor: osmomath.NewInt(1)}, {Denom: OVERLY_PRECISE_USD, NormalizationFactor: veryBigNormalizationFactor}, {Denom: NO_PRECISION_USD, NormalizationFactor: osmomath.ZeroInt()}, + {Denom: USDT, NormalizationFactor: osmomath.NewInt(1)}, + {Denom: USDC, NormalizationFactor: osmomath.NewInt(100)}, {Denom: ALLUSD, NormalizationFactor: osmomath.NewInt(10)}, }, @@ -320,6 +320,7 @@ func (s *RoutablePoolTestSuite) TestCheckStaticRateLimiter() { USDT: osmomath.NewInt(1), OVERLY_PRECISE_USD: osmomath.NewInt(1), NO_PRECISION_USD: osmomath.NewInt(1), + ALLUSD: osmomath.NewInt(1), } oneInt := osmomath.NewInt(1) @@ -337,6 +338,7 @@ func (s *RoutablePoolTestSuite) TestCheckStaticRateLimiter() { tests := map[string]struct { tokenInCoin sdk.Coin + tokenOutDenom string initialBalances sdk.Coins standardNormFactor osmomath.Int normalizationScalingFactors map[string]osmomath.Int @@ -345,6 +347,7 @@ func (s *RoutablePoolTestSuite) TestCheckStaticRateLimiter() { }{ "valid token in - below upper limit": { tokenInCoin: sdk.NewCoin(USDC, osmomath.NewInt(100_000)), + tokenOutDenom: USDT, initialBalances: defaultInitialBalances, standardNormFactor: defaultStandardNormFactor, normalizationScalingFactors: defaultScalingFactors, @@ -353,6 +356,7 @@ func (s *RoutablePoolTestSuite) TestCheckStaticRateLimiter() { }, "invalid token in - exceeds upper limit": { tokenInCoin: sdk.NewCoin(USDC, osmomath.NewInt(2_000_000)), + tokenOutDenom: USDT, initialBalances: defaultInitialBalances, standardNormFactor: defaultStandardNormFactor, normalizationScalingFactors: defaultScalingFactors, @@ -360,11 +364,12 @@ func (s *RoutablePoolTestSuite) TestCheckStaticRateLimiter() { expectError: domain.StaticRateLimiterInvalidUpperLimitError{ Denom: USDC, UpperLimit: "0.5", - Weight: osmomath.MustNewDecFromStr("0.6").String(), + Weight: osmomath.MustNewDecFromStr("1").String(), }, }, "no static limiter configured": { tokenInCoin: sdk.NewCoin(USDC, osmomath.NewInt(1_000_000)), + tokenOutDenom: USDT, initialBalances: defaultInitialBalances, standardNormFactor: defaultStandardNormFactor, normalizationScalingFactors: defaultScalingFactors, @@ -372,15 +377,30 @@ func (s *RoutablePoolTestSuite) TestCheckStaticRateLimiter() { expectError: nil, }, "static limiter not set for token in denom": { - tokenInCoin: sdk.NewCoin(USDC, osmomath.NewInt(1_000_000)), + tokenInCoin: sdk.NewCoin(USDT, osmomath.NewInt(1_000_000)), + tokenOutDenom: USDC, initialBalances: defaultInitialBalances, standardNormFactor: defaultStandardNormFactor, normalizationScalingFactors: defaultScalingFactors, staticLimiterConfig: defaultStaticLimiterConfig, expectError: nil, }, + "check all assets' static limiters when token in denom is alloyed": { + tokenInCoin: sdk.NewCoin(ALLUSD, osmomath.NewInt(1_000_001)), + tokenOutDenom: USDT, + initialBalances: defaultInitialBalances, + standardNormFactor: defaultStandardNormFactor, + normalizationScalingFactors: defaultScalingFactors, + staticLimiterConfig: defaultStaticLimiterConfig, + expectError: domain.StaticRateLimiterInvalidUpperLimitError{ + Denom: USDC, + UpperLimit: "0.5", + Weight: osmomath.MustNewDecFromStr("0.500000250000125").String(), + }, + }, "different normalization factors": { - tokenInCoin: sdk.NewCoin(USDC, osmomath.NewInt(500_000)), + tokenInCoin: sdk.NewCoin(USDC, osmomath.NewInt(500_000)), + tokenOutDenom: USDT, initialBalances: sdk.NewCoins( sdk.NewCoin(USDC, osmomath.NewInt(1_000_000)), sdk.NewCoin(USDT, osmomath.NewInt(2_000_000)), @@ -402,7 +422,7 @@ func (s *RoutablePoolTestSuite) TestCheckStaticRateLimiter() { for name, tc := range tests { s.Run(name, func() { s.Setup() - routablePool := s.SetupRoutableAlloyTransmuterPoolCustom(USDT, USDC, tc.initialBalances, osmomath.ZeroDec(), cosmwasmpool.AlloyedRateLimiter{ + routablePool := s.SetupRoutableAlloyTransmuterPoolCustom(tc.tokenInCoin.Denom, tc.tokenOutDenom, tc.initialBalances, osmomath.ZeroDec(), cosmwasmpool.AlloyedRateLimiter{ StaticLimiterByDenomMap: tc.staticLimiterConfig, }, cosmwasmpool.PrecomputedData{ StdNormFactor: tc.standardNormFactor,