diff --git a/app/apptesting/gamm.go b/app/apptesting/gamm.go new file mode 100644 index 000000000..9d7fda2b4 --- /dev/null +++ b/app/apptesting/gamm.go @@ -0,0 +1,141 @@ +package apptesting + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + + gammkeeper "github.com/osmosis-labs/osmosis/v15/x/gamm/keeper" + "github.com/osmosis-labs/osmosis/v15/x/gamm/pool-models/balancer" + gammtypes "github.com/osmosis-labs/osmosis/v15/x/gamm/types" + poolmanagertypes "github.com/osmosis-labs/osmosis/v15/x/poolmanager/types" +) + +// 10^18 multiplier +var EXP = sdk.NewIntFromBigInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)) + +// Default sender for amm messages +var sender = sdk.MustAccAddressFromBech32(alice) + +var DefaultAcctFunds sdk.Coins = sdk.NewCoins( + sdk.NewCoin("adym", EXP.Mul(sdk.NewInt(1_000_000))), + sdk.NewCoin("foo", EXP.Mul(sdk.NewInt(1_000_000))), + sdk.NewCoin("bar", EXP.Mul(sdk.NewInt(1_000_000))), + sdk.NewCoin("baz", EXP.Mul(sdk.NewInt(1_000_000))), +) + +var DefaultPoolParams = balancer.PoolParams{ + SwapFee: sdk.NewDec(0), + ExitFee: sdk.NewDec(0), +} + +var DefaultPoolAssets = []balancer.PoolAsset{ + { + Weight: sdk.NewInt(100), + Token: sdk.NewCoin("foo", EXP.Mul(sdk.NewInt(500))), + }, + { + Weight: sdk.NewInt(100), + Token: sdk.NewCoin("adym", EXP.Mul(sdk.NewInt(500))), + }, +} + +// PrepareCustomPool sets up a Balancer pool with an array of assets and given parameters +// This is the generic method called by other PreparePool wrappers +// It funds the sender account with DefaultAcctFunds +func (s *KeeperTestHelper) PrepareCustomPool(assets []balancer.PoolAsset, params balancer.PoolParams) uint64 { + s.FundAcc(sender, DefaultAcctFunds) + + msg := balancer.NewMsgCreateBalancerPool(sender, params, assets, "") + poolId, err := s.App.PoolManagerKeeper.CreatePool(s.Ctx, msg) + s.NoError(err) + return poolId +} + +// PrepareDefaultPool sets up a pool with default pool assets and parameters. +func (s *KeeperTestHelper) PrepareDefaultPool() uint64 { + poolId := s.PrepareCustomPool(DefaultPoolAssets, DefaultPoolParams) + + spotPrice, err := s.App.GAMMKeeper.CalculateSpotPrice(s.Ctx, poolId, "foo", "adym") + s.NoError(err) + s.Equal(sdk.NewDec(1).String(), spotPrice.String()) + + return poolId +} + +// PreparePoolWithCoins returns a pool consisted of given coins with equal weight and default pool parameters. +func (s *KeeperTestHelper) PreparePoolWithCoins(coins sdk.Coins) uint64 { + poolAssets := coinsToAssets(coins) + return s.PrepareCustomPool(poolAssets, DefaultPoolParams) +} + +// PreparePoolWithPoolParams sets up a pool with given poolParams and default pool assets. +func (s *KeeperTestHelper) PreparePoolWithPoolParams(poolParams balancer.PoolParams) uint64 { + return s.PrepareCustomPool(DefaultPoolAssets, poolParams) +} + +// PrepareCustomPoolFromCoins sets up a Balancer pool with an array of coins and given parameters +// The coins are converted to pool assets where each asset has a weight of 1. +func (s *KeeperTestHelper) PrepareCustomPoolFromCoins(coins sdk.Coins, params balancer.PoolParams) uint64 { + poolAssets := coinsToAssets(coins) + return s.PrepareCustomPool(poolAssets, params) +} + +func coinsToAssets(coins sdk.Coins) []balancer.PoolAsset { + var poolAssets []balancer.PoolAsset + for _, coin := range coins { + poolAsset := balancer.PoolAsset{ + Weight: sdk.NewInt(1), + Token: coin, + } + poolAssets = append(poolAssets, poolAsset) + } + return poolAssets +} + +func (s *KeeperTestHelper) RunBasicSwap(poolId uint64, from string, swapIn sdk.Coin, outDenom string) { + msg := gammtypes.MsgSwapExactAmountIn{ + Sender: from, + Routes: []poolmanagertypes.SwapAmountInRoute{{PoolId: poolId, TokenOutDenom: outDenom}}, + TokenIn: swapIn, + TokenOutMinAmount: sdk.ZeroInt(), + } + + gammMsgServer := gammkeeper.NewMsgServerImpl(s.App.GAMMKeeper) + _, err := gammMsgServer.SwapExactAmountIn(sdk.WrapSDKContext(s.Ctx), &msg) + s.Require().NoError(err) +} + +func (s *KeeperTestHelper) RunBasicExit(poolId uint64, shares sdk.Int, from string) (out sdk.Coins) { + msg := gammtypes.MsgExitPool{ + Sender: from, + PoolId: poolId, + ShareInAmount: shares, + TokenOutMins: sdk.NewCoins(), + } + + gammMsgServer := gammkeeper.NewMsgServerImpl(s.App.GAMMKeeper) + res, err := gammMsgServer.ExitPool(sdk.WrapSDKContext(s.Ctx), &msg) + s.Require().NoError(err) + return res.TokenOut +} + +// RunBasicJoin joins the pool with 10% of the total pool shares +func (s *KeeperTestHelper) RunBasicJoin(poolId uint64, from string) (shares sdk.Int, cost sdk.Coins) { + pool, err := s.App.GAMMKeeper.GetPoolAndPoke(s.Ctx, poolId) + s.Require().NoError(err) + + totalPoolShare := pool.GetTotalShares() + msg := gammtypes.MsgJoinPool{ + Sender: from, + PoolId: poolId, + ShareOutAmount: totalPoolShare.Quo(sdk.NewInt(10)), + TokenInMaxs: sdk.NewCoins(), + } + + gammMsgServer := gammkeeper.NewMsgServerImpl(s.App.GAMMKeeper) + res, err := gammMsgServer.JoinPool(sdk.WrapSDKContext(s.Ctx), &msg) + s.Require().NoError(err) + + return res.ShareOutAmount, res.TokenIn +} diff --git a/go.mod b/go.mod index 33ec916eb..70d3ab513 100644 --- a/go.mod +++ b/go.mod @@ -241,7 +241,7 @@ replace ( github.com/evmos/ethermint => github.com/dymensionxyz/ethermint v0.22.0-dymension-v0.4.1.0.20240625101522-b1506ae83050 github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 github.com/osmosis-labs/osmosis/osmomath => github.com/dymensionxyz/osmosis/osmomath v0.0.6-dymension-v0.1.0.20240820121212-c0e21fa21e43 - github.com/osmosis-labs/osmosis/v15 => github.com/dymensionxyz/osmosis/v15 v15.2.1-0.20240627111157-f2243f47cdb3 + github.com/osmosis-labs/osmosis/v15 => github.com/dymensionxyz/osmosis/v15 v15.2.1-0.20240820121212-c0e21fa21e43 // broken goleveldb github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 diff --git a/go.sum b/go.sum index 7e00c383d..b957deb8e 100644 --- a/go.sum +++ b/go.sum @@ -506,8 +506,8 @@ github.com/dymensionxyz/gerr-cosmos v1.0.0 h1:oi91rgOkpJWr41oX9JOyjvvBnhGY54tj51 github.com/dymensionxyz/gerr-cosmos v1.0.0/go.mod h1:n+0olxPogzWqFKba45mCpvrHLGmeS8W9UZjggHnWk6c= github.com/dymensionxyz/osmosis/osmomath v0.0.6-dymension-v0.1.0.20240820121212-c0e21fa21e43 h1:EskhZ6ILN3vwJ6l8gPWPZ49RFSB52WghT5v+pmzrNCI= github.com/dymensionxyz/osmosis/osmomath v0.0.6-dymension-v0.1.0.20240820121212-c0e21fa21e43/go.mod h1:SdGCL9CZb14twRAJUSzb7bRE0OoopRpF2Hnd1UhJpFU= -github.com/dymensionxyz/osmosis/v15 v15.2.1-0.20240627111157-f2243f47cdb3 h1:4VD23Jv5d8hqXEhLNNcLXlpSDJCWAGYJLF0kisJtkIk= -github.com/dymensionxyz/osmosis/v15 v15.2.1-0.20240627111157-f2243f47cdb3/go.mod h1:2rsnXAdjYfXtyEw0mNwAdOiAccALYjAPvINGUf9Qg7Y= +github.com/dymensionxyz/osmosis/v15 v15.2.1-0.20240820121212-c0e21fa21e43 h1:ugbpHwwlckB4W/aNXUTEsxaakPFgXi+LAsCtvfJ200Q= +github.com/dymensionxyz/osmosis/v15 v15.2.1-0.20240820121212-c0e21fa21e43/go.mod h1:2rsnXAdjYfXtyEw0mNwAdOiAccALYjAPvINGUf9Qg7Y= github.com/dymensionxyz/sdk-utils v0.2.7 h1:9Hy56ivv81ISIG7x9AMaNo6GABRuEgZQ+MmS2PdfuW0= github.com/dymensionxyz/sdk-utils v0.2.7/go.mod h1:it9owYOpnIe17+ftTATQNDN4z+mBQx20/2Jm8SK15Rk= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= diff --git a/x/gamm/amm_test.go b/x/gamm/amm_test.go new file mode 100644 index 000000000..deec4d7b6 --- /dev/null +++ b/x/gamm/amm_test.go @@ -0,0 +1,123 @@ +package keeper_test + +import ( + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/osmosis-labs/osmosis/v15/x/gamm/pool-models/balancer" + "github.com/stretchr/testify/suite" + + cometbftproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/dymensionxyz/dymension/v3/app/apptesting" + "github.com/dymensionxyz/dymension/v3/testutil/sample" +) + +type KeeperTestSuite struct { + apptesting.KeeperTestHelper +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} + +func (s *KeeperTestSuite) SetupTest() { + app := apptesting.Setup(s.T(), false) + ctx := app.GetBaseApp().NewContext(false, cometbftproto.Header{}) + + // set txfees basedenom + err := app.TxFeesKeeper.SetBaseDenom(ctx, "adym") + s.Require().NoError(err) + + s.App = app + s.Ctx = ctx +} + +func (s *KeeperTestSuite) TestSwapsRevenue() { + // Create a pool with 100_000 DYM and 100_000 FOO + poolCoins := sdk.NewCoins( + sdk.NewCoin("adym", apptesting.EXP.Mul(sdk.NewInt(100_000))), + sdk.NewCoin("foo", apptesting.EXP.Mul(sdk.NewInt(100_000))), + ) + + testCases := []struct { + name string + swapFee sdk.Dec + takerFee sdk.Dec + expRevenue bool + }{ + { + name: "1% swap fee, 1% taker fee", + swapFee: sdk.NewDecWithPrec(1, 2), // 1% + takerFee: sdk.NewDecWithPrec(1, 2), // 1% + expRevenue: true, + }, + { + name: "1% swap fee, no taker fee", + swapFee: sdk.NewDecWithPrec(1, 2), // 1% + takerFee: sdk.ZeroDec(), // 0% + expRevenue: true, + }, + { + name: "0% swap fee, 1% taker fee", + swapFee: sdk.ZeroDec(), // 0% + takerFee: sdk.NewDecWithPrec(1, 2), // 1% + expRevenue: false, + }, + { + name: "0% swap fee, no taker fee", + swapFee: sdk.ZeroDec(), // 0% + takerFee: sdk.ZeroDec(), // 0% + expRevenue: false, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + params := s.App.GAMMKeeper.GetParams(s.Ctx) + params.TakerFee = tc.takerFee + s.App.GAMMKeeper.SetParams(s.Ctx, params) + + poolId := s.PrepareCustomPoolFromCoins(poolCoins, balancer.PoolParams{ + SwapFee: tc.swapFee, + ExitFee: sdk.ZeroDec(), + }) + + // join pool + addr := sample.Acc() + s.FundAcc(addr, apptesting.DefaultAcctFunds) + shares, _ := s.RunBasicJoin(poolId, addr.String()) + + // check position + p, _ := s.App.GAMMKeeper.GetPool(s.Ctx, poolId) + pool := p.(*balancer.Pool) // nolint: errcheck + position, err := pool.CalcExitPoolCoinsFromShares(s.Ctx, shares, sdk.ZeroDec()) + s.Require().NoError(err) + liquidity := pool.GetTotalPoolLiquidity(s.Ctx) + spot, err := s.App.GAMMKeeper.CalculateSpotPrice(s.Ctx, poolId, "foo", "adym") + s.Require().NoError(err) + s.T().Logf("positionBefore: %s, liquidity: %s, spot: %s", position, liquidity, spot) + + // swap tokens (swap 5 DYM for FOO) and vice versa + s.RunBasicSwap(poolId, addr.String(), sdk.NewCoin("adym", apptesting.EXP.Mul(sdk.NewInt(5))), "foo") + s.RunBasicSwap(poolId, addr.String(), sdk.NewCoin("foo", apptesting.EXP.Mul(sdk.NewInt(5))), "adym") + + // check position + p, _ = s.App.GAMMKeeper.GetPool(s.Ctx, poolId) + pool = p.(*balancer.Pool) // nolint: errcheck + liquidity = pool.GetTotalPoolLiquidity(s.Ctx) + positionAfter, err := pool.CalcExitPoolCoinsFromShares(s.Ctx, shares, sdk.ZeroDec()) + s.Require().NoError(err) + spot, err = s.App.GAMMKeeper.CalculateSpotPrice(s.Ctx, poolId, "foo", "adym") + s.Require().NoError(err) + s.T().Logf("positionAfterSwap: %s, liquidity: %s, spot: %s", positionAfter, liquidity, spot) + + // assert + if tc.expRevenue { + s.True(positionAfter.IsAllGT(position), fmt.Sprintf("positionBefore: %s, positionAfter: %s", position, positionAfter)) + } else { + s.True(positionAfter.IsAnyGT(position), fmt.Sprintf("positionBefore: %s, positionAfter: %s", position, positionAfter)) + } + }) + } +}