diff --git a/x/tokenfactory/README.md b/x/tokenfactory/README.md index b9ca2ec..9619c8d 100644 --- a/x/tokenfactory/README.md +++ b/x/tokenfactory/README.md @@ -1,21 +1,10 @@ # Token Factory -The tokenfactory module allows any account to create a new token with -the name `factory/{creator address}/{subdenom}`. Because tokens are -namespaced by creator address, this allows token minting to be -permissionless, due to not needing to resolve name collisions. A single -account can create multiple denoms, by providing a unique subdenom for each -created denom. Once a denom is created, the original creator is given -"admin" privileges over the asset. This allows them to: - -- Mint their denom to any account -- Burn their denom from any account -- Create a transfer of their denom between any two accounts -- Change the admin. In the future, more admin capabilities may be added. Admins - can choose to share admin privileges with other accounts using the authz - module. The `ChangeAdmin` functionality, allows changing the master admin - account, or even setting it to `""`, meaning no account has admin privileges - of the asset. +The tokenfactory module allows any account to create a new token with the name `factory/{creator address}/{subdenom}`. Because tokens are namespaced by creator address, this allows token minting to be permissionless, due to not needing to resolve name collisions. A single account can create multiple denoms, by providing a unique subdenom for each created denom. Once a denom is created, the original creator is given "admin" privileges over the asset. This allows them to: +* Mint their denom to any account +* Burn their denom from any account +* Create a transfer of their denom between any two accounts +* Change the admin. ## Messages @@ -179,7 +168,6 @@ This message is expected to fail if: When this message is processed the following actions occur: * The admin of the specified denom is changed to the `new_admin` address -* If the new admin is set to an empty string, the denom becomes a fixed supply token with no further mint and burn operations allowed ### MsgSetDenomMetadata @@ -615,9 +603,7 @@ tokend tx tokenfactory force-transfer 1000factory/cosmos1...addr.../mytoken cosm ##### change-admin The command `change-admin` allows denom admins to change the admin of a denom. - * The admin address can be set to the gov module account. -* The admin address can be set to an empty string to renounce admin control entirely. Usage: diff --git a/x/tokenfactory/keeper/admins_test.go b/x/tokenfactory/keeper/admins_test.go index 4ac80fe..a4ca725 100644 --- a/x/tokenfactory/keeper/admins_test.go +++ b/x/tokenfactory/keeper/admins_test.go @@ -38,29 +38,41 @@ func (suite *KeeperTestSuite) TestAdminMsgs() { suite.Require().Nil(adminRes.Denoms) // Test minting to admins own account - _, err = suite.msgServer.Mint(suite.Ctx, types.NewMsgMint(suite.TestAccs[0].String(), sdk.NewInt64Coin(suite.defaultDenom, 10))) - addr0bal += 10 + amount := int64(10) + _, err = suite.msgServer.Mint(suite.Ctx, types.NewMsgMint(suite.TestAccs[0].String(), sdk.NewInt64Coin(suite.defaultDenom, amount))) + addr0bal += amount suite.Require().NoError(err) - suite.Require().True(bankKeeper.GetBalance(suite.Ctx, suite.TestAccs[0], suite.defaultDenom).Amount.Int64() == addr0bal, bankKeeper.GetBalance(suite.Ctx, suite.TestAccs[0], suite.defaultDenom)) + suite.Require().Equal(addr0bal, bankKeeper.GetBalance(suite.Ctx, suite.TestAccs[0], suite.defaultDenom).Amount.Int64()) // Test minting to a different account - _, err = suite.msgServer.Mint(suite.Ctx, types.NewMsgMintTo(suite.TestAccs[0].String(), sdk.NewInt64Coin(suite.defaultDenom, 10), suite.TestAccs[1].String())) - addr1bal += 10 + _, err = suite.msgServer.Mint(suite.Ctx, types.NewMsgMintTo(suite.TestAccs[0].String(), sdk.NewInt64Coin(suite.defaultDenom, amount), suite.TestAccs[1].String())) + addr1bal += amount suite.Require().NoError(err) - suite.Require().True(suite.App.BankKeeper.GetBalance(suite.Ctx, suite.TestAccs[1], suite.defaultDenom).Amount.Int64() == addr1bal, suite.App.BankKeeper.GetBalance(suite.Ctx, suite.TestAccs[1], suite.defaultDenom)) + suite.Require().Equal(addr1bal, suite.App.BankKeeper.GetBalance(suite.Ctx, suite.TestAccs[1], suite.defaultDenom).Amount.Int64()) - // Test force transferring - _, err = suite.msgServer.ForceTransfer(suite.Ctx, types.NewMsgForceTransfer(suite.TestAccs[0].String(), sdk.NewInt64Coin(suite.defaultDenom, 5), suite.TestAccs[1].String(), suite.TestAccs[0].String())) - addr1bal -= 5 - addr0bal += 5 + // Test force transferring from account 1 to account 0 + amount = 5 + _, err = suite.msgServer.ForceTransfer(suite.Ctx, types.NewMsgForceTransfer(suite.TestAccs[0].String(), sdk.NewInt64Coin(suite.defaultDenom, amount), suite.TestAccs[1].String(), suite.TestAccs[0].String())) + addr1bal -= amount + addr0bal += amount suite.Require().NoError(err) - suite.Require().True(suite.App.BankKeeper.GetBalance(suite.Ctx, suite.TestAccs[0], suite.defaultDenom).Amount.Int64() == addr0bal, suite.App.BankKeeper.GetBalance(suite.Ctx, suite.TestAccs[0], suite.defaultDenom)) - suite.Require().True(suite.App.BankKeeper.GetBalance(suite.Ctx, suite.TestAccs[1], suite.defaultDenom).Amount.Int64() == addr1bal, suite.App.BankKeeper.GetBalance(suite.Ctx, suite.TestAccs[1], suite.defaultDenom)) + suite.Require().Equal(addr0bal, suite.App.BankKeeper.GetBalance(suite.Ctx, suite.TestAccs[0], suite.defaultDenom).Amount.Int64()) + suite.Require().Equal(addr1bal, suite.App.BankKeeper.GetBalance(suite.Ctx, suite.TestAccs[1], suite.defaultDenom).Amount.Int64()) // Test burning from own account - _, err = suite.msgServer.Burn(suite.Ctx, types.NewMsgBurn(suite.TestAccs[0].String(), sdk.NewInt64Coin(suite.defaultDenom, 5))) + _, err = suite.msgServer.Burn(suite.Ctx, types.NewMsgBurn(suite.TestAccs[0].String(), sdk.NewInt64Coin(suite.defaultDenom, amount))) + addr0bal -= amount + suite.Require().NoError(err) + suite.Require().Equal(addr0bal, bankKeeper.GetBalance(suite.Ctx, suite.TestAccs[0], suite.defaultDenom).Amount.Int64()) + suite.Require().Equal(addr1bal, bankKeeper.GetBalance(suite.Ctx, suite.TestAccs[1], suite.defaultDenom).Amount.Int64()) + + // Test burning from a different account + amount = 2 + _, err = suite.msgServer.Burn(suite.Ctx, types.NewMsgBurnFrom(suite.TestAccs[0].String(), sdk.NewInt64Coin(suite.defaultDenom, amount), suite.TestAccs[1].String())) + addr1bal -= amount suite.Require().NoError(err) - suite.Require().True(bankKeeper.GetBalance(suite.Ctx, suite.TestAccs[1], suite.defaultDenom).Amount.Int64() == addr1bal) + suite.Require().Equal(addr0bal, bankKeeper.GetBalance(suite.Ctx, suite.TestAccs[0], suite.defaultDenom).Amount.Int64()) + suite.Require().Equal(addr1bal, bankKeeper.GetBalance(suite.Ctx, suite.TestAccs[1], suite.defaultDenom).Amount.Int64()) // Test Change Admin _, err = suite.msgServer.ChangeAdmin(suite.Ctx, types.NewMsgChangeAdmin(suite.TestAccs[0].String(), suite.defaultDenom, suite.TestAccs[1].String())) @@ -90,10 +102,11 @@ func (suite *KeeperTestSuite) TestAdminMsgs() { suite.Require().Error(err) // Make sure the new admin works - _, err = suite.msgServer.Mint(suite.Ctx, types.NewMsgMint(suite.TestAccs[1].String(), sdk.NewInt64Coin(suite.defaultDenom, 5))) - addr1bal += 5 + amount = 5 + _, err = suite.msgServer.Mint(suite.Ctx, types.NewMsgMint(suite.TestAccs[1].String(), sdk.NewInt64Coin(suite.defaultDenom, amount))) + addr1bal += amount suite.Require().NoError(err) - suite.Require().True(bankKeeper.GetBalance(suite.Ctx, suite.TestAccs[1], suite.defaultDenom).Amount.Int64() == addr1bal) + suite.Require().Equal(addr1bal, bankKeeper.GetBalance(suite.Ctx, suite.TestAccs[1], suite.defaultDenom).Amount.Int64()) // Try setting admin to empty _, err = suite.msgServer.ChangeAdmin(suite.Ctx, types.NewMsgChangeAdmin(suite.TestAccs[1].String(), suite.defaultDenom, "")) @@ -345,12 +358,12 @@ func (suite *KeeperTestSuite) TestForceTransferDenom() { fromAddr, err := sdk.AccAddressFromBech32(tc.forceTransferMsg.TransferFromAddress) suite.Require().NoError(err) fromBal := suite.App.BankKeeper.GetBalance(suite.Ctx, fromAddr, suite.defaultDenom).Amount - suite.Require().True(fromBal.Int64() == balances[tc.forceTransferMsg.TransferFromAddress]) + suite.Require().Equal(balances[tc.forceTransferMsg.TransferFromAddress], fromBal.Int64()) toAddr, err := sdk.AccAddressFromBech32(tc.forceTransferMsg.TransferToAddress) suite.Require().NoError(err) toBal := suite.App.BankKeeper.GetBalance(suite.Ctx, toAddr, suite.defaultDenom).Amount - suite.Require().True(toBal.Int64() == balances[tc.forceTransferMsg.TransferToAddress]) + suite.Require().Equal(balances[tc.forceTransferMsg.TransferToAddress], toBal.Int64()) }) } } diff --git a/x/tokenfactory/keeper/createdenom_test.go b/x/tokenfactory/keeper/createdenom_test.go index 19bef24..d9590d5 100644 --- a/x/tokenfactory/keeper/createdenom_test.go +++ b/x/tokenfactory/keeper/createdenom_test.go @@ -3,6 +3,7 @@ package keeper_test import ( "fmt" + "github.com/cosmos/tokenfactory/x/tokenfactory/keeper" "github.com/cosmos/tokenfactory/x/tokenfactory/types" sdkmath "cosmossdk.io/math" @@ -163,3 +164,169 @@ func (suite *KeeperTestSuite) TestCreateDenom() { }) } } + +func (suite *KeeperTestSuite) TestCreateDenomGasConsumption() { + for _, tc := range []struct { + desc string + denomCreationGasConsume uint64 + expectedGasConsumed uint64 + }{ + { + desc: "gas consumption is zero", + denomCreationGasConsume: 0, + expectedGasConsumed: 0, + }, + { + desc: "gas consumption is set to 1000", + denomCreationGasConsume: 1000, + expectedGasConsumed: 1000, + }, + { + desc: "gas consumption is set to 2_000_000 (default)", + denomCreationGasConsume: 2_000_000, + expectedGasConsumed: 2_000_000, + }, + { + desc: "gas consumption is set to large value", + denomCreationGasConsume: 10_000_000, + expectedGasConsumed: 10_000_000, + }, + } { + suite.Run(fmt.Sprintf("Case %s", tc.desc), func() { + suite.SetupTest() + + // Set the gas consumption parameter + params := types.DefaultParams() + params.DenomCreationGasConsume = tc.denomCreationGasConsume + err := suite.App.TokenFactoryKeeper.SetParams(suite.Ctx, params) + suite.Require().NoError(err) + + // Get gas consumed before creating denom + gasConsumedBefore := suite.Ctx.GasMeter().GasConsumed() + + // Create a denom + _, err = suite.msgServer.CreateDenom(suite.Ctx, types.NewMsgCreateDenom(suite.TestAccs[0].String(), "testcoin")) + suite.Require().NoError(err) + + // Get gas consumed after creating denom + gasConsumedAfter := suite.Ctx.GasMeter().GasConsumed() + + // Calculate the gas consumed by CreateDenom + actualGasConsumed := gasConsumedAfter - gasConsumedBefore + + // The actual gas consumed should be at least the expected amount + // (it may be slightly more due to other operations in CreateDenom) + suite.Require().GreaterOrEqual(actualGasConsumed, tc.expectedGasConsumed, + "Expected at least %d gas to be consumed, but only %d was consumed", + tc.expectedGasConsumed, actualGasConsumed) + + // Verify the gas was consumed with the correct descriptor + // We can't directly check the descriptor, but we can verify the amount is within a reasonable range + // CreateDenom has other operations, so allow for some overhead (e.g., 100k gas) + const overhead = uint64(100_000) + suite.Require().LessOrEqual(actualGasConsumed, tc.expectedGasConsumed+overhead, + "Gas consumed (%d) exceeds expected (%d) plus overhead (%d)", + actualGasConsumed, tc.expectedGasConsumed, overhead) + }) + } +} + +func (suite *KeeperTestSuite) TestCommunityPoolFunding() { + for _, tc := range []struct { + desc string + enableCommunityPoolFee bool + denomCreationFee sdk.Coins + expectCommunityPoolDelta bool + }{ + { + desc: "community pool funding enabled - fees should go to community pool", + enableCommunityPoolFee: true, + denomCreationFee: sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(50_000_000))), + expectCommunityPoolDelta: true, + }, + { + desc: "community pool funding disabled - fees should be burned", + enableCommunityPoolFee: false, + denomCreationFee: sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(50_000_000))), + expectCommunityPoolDelta: false, + }, + { + desc: "nil fee - no changes expected", + enableCommunityPoolFee: true, + denomCreationFee: nil, + expectCommunityPoolDelta: false, + }, + } { + suite.Run(fmt.Sprintf("Case %s", tc.desc), func() { + suite.SetupTest() + + // Configure the capability + // Note: TokenFactoryKeeper is stored by value in the app. We can modify it using + // SetEnabledCapabilities (which has a pointer receiver), but we must recreate the + // msgServer afterward because it has its own copy of the keeper from SetupTest(). + var capabilities []string + if tc.enableCommunityPoolFee { + capabilities = []string{types.EnableCommunityPoolFeeFunding} + } + + // Use the pointer to the keeper in the app struct to set capabilities + keeperPtr := &suite.App.TokenFactoryKeeper + keeperPtr.SetEnabledCapabilities(suite.Ctx, capabilities) + + // IMPORTANT: Recreate the msgServer because it has a copy of the keeper + // The msgServer was created in SetupTest() before we modified the capabilities + suite.msgServer = keeper.NewMsgServerImpl(suite.App.TokenFactoryKeeper) + + // Set the denom creation fee parameter + params := types.DefaultParams() + params.DenomCreationFee = tc.denomCreationFee + err := suite.App.TokenFactoryKeeper.SetParams(suite.Ctx, params) + suite.Require().NoError(err) + + // Get initial community pool balance + communityPoolBefore := suite.GetCommunityPoolBalance() + + // Get initial user balance + userBalanceBefore := suite.App.BankKeeper.GetBalance(suite.Ctx, suite.TestAccs[0], "stake") + + // Create a denom + _, err = suite.msgServer.CreateDenom(suite.Ctx, types.NewMsgCreateDenom(suite.TestAccs[0].String(), "testcoin")) + suite.Require().NoError(err) + + // Get final community pool balance + communityPoolAfter := suite.GetCommunityPoolBalance() + + // Get final user balance + userBalanceAfter := suite.App.BankKeeper.GetBalance(suite.Ctx, suite.TestAccs[0], "stake") + + // Calculate changes + communityPoolDelta := communityPoolAfter.Sub(communityPoolBefore) + userBalanceDelta := userBalanceBefore.Sub(userBalanceAfter) + + if tc.expectCommunityPoolDelta { + // Verify fees went to community pool + expectedDelta := sdk.NewDecCoinsFromCoins(tc.denomCreationFee...) + suite.Require().Equal(expectedDelta, communityPoolDelta, + "Community pool should increase by the denom creation fee amount") + + // Verify user balance decreased by the fee amount + suite.Require().Equal(tc.denomCreationFee[0], userBalanceDelta, + "User balance should decrease by the denom creation fee amount") + } else { + // Verify community pool did NOT increase + suite.Require().True(communityPoolDelta.IsZero(), + "Community pool should not increase when capability is disabled or fee is nil") + + if tc.denomCreationFee != nil { + // Verify user was still charged (fees were burned) + suite.Require().Equal(tc.denomCreationFee[0], userBalanceDelta, + "User balance should decrease when fees are burned") + } else { + // Verify user was not charged when fee is nil + suite.Require().True(userBalanceDelta.IsZero(), + "User balance should not change when fee is nil") + } + } + }) + } +} diff --git a/x/tokenfactory/keeper/genesis_test.go b/x/tokenfactory/keeper/genesis_test.go index faacf3e..bea5ed9 100644 --- a/x/tokenfactory/keeper/genesis_test.go +++ b/x/tokenfactory/keeper/genesis_test.go @@ -9,6 +9,10 @@ import ( func (suite *KeeperTestSuite) TestGenesis() { genesisState := types.GenesisState{ + Params: types.Params{ + DenomCreationFee: sdk.Coins{sdk.NewInt64Coin("stake", 10_000_000)}, + DenomCreationGasConsume: 5_000_000, + }, FactoryDenoms: []types.GenesisDenom{ { Denom: "factory/cosmos1t7egva48prqmzl59x5ngv4zx0dtrwewcdqdjr8/bitcoin", @@ -42,7 +46,7 @@ func (suite *KeeperTestSuite) TestGenesis() { } } - if err := app.TokenFactoryKeeper.SetParams(suite.Ctx, types.Params{DenomCreationFee: sdk.Coins{sdk.NewInt64Coin("stake", 100)}}); err != nil { + if err := app.TokenFactoryKeeper.SetParams(suite.Ctx, genesisState.Params); err != nil { panic(err) } app.TokenFactoryKeeper.InitGenesis(suite.Ctx, genesisState) diff --git a/x/tokenfactory/keeper/keeper_test.go b/x/tokenfactory/keeper/keeper_test.go index e0bdbb9..073d38f 100644 --- a/x/tokenfactory/keeper/keeper_test.go +++ b/x/tokenfactory/keeper/keeper_test.go @@ -24,6 +24,13 @@ type KeeperTestSuite struct { defaultDenom string } +// GetCommunityPoolBalance returns the current community pool balance +func (suite *KeeperTestSuite) GetCommunityPoolBalance() sdk.DecCoins { + feePool, err := suite.App.DistrKeeper.FeePool.Get(suite.Ctx) + suite.Require().NoError(err) + return feePool.GetCommunityPool() +} + func TestKeeperTestSuite(t *testing.T) { suite.Run(t, new(KeeperTestSuite)) }