Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 5 additions & 19 deletions x/tokenfactory/README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:

Expand Down
51 changes: 32 additions & 19 deletions x/tokenfactory/keeper/admins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down Expand Up @@ -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, ""))
Expand Down Expand Up @@ -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())
})
}
}
Expand Down
167 changes: 167 additions & 0 deletions x/tokenfactory/keeper/createdenom_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
}
}
})
}
}
6 changes: 5 additions & 1 deletion x/tokenfactory/keeper/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions x/tokenfactory/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
Loading