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
84 changes: 84 additions & 0 deletions integration-tests/modules/assetft_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1550,6 +1550,90 @@ func TestAssetFTBurn(t *testing.T) {
assertT.Equal(burnCoin, oldSupply.GetAmount().Sub(newSupply.GetAmount()))
}

// TestAssetFTBurn_GovernanceDenom tests burning the governance/bond denom (core/testcore/devcore).
// Event-based check ensures the burn was applied; balance and supply checks run right after the tx.
func TestAssetFTBurn_GovernanceDenom(t *testing.T) {
t.Parallel()

ctx, chain := integrationtests.NewTXChainTestingContext(t)

requireT := require.New(t)

user := chain.GenAccount()
bondDenom := chain.ChainSettings.Denom
bankClient := banktypes.NewQueryClient(chain.ClientContext)

fundAmount := sdkmath.NewInt(2_000_000_000_000) // 2 million CORE for burn + fees
chain.FundAccountWithOptions(ctx, t, user, integration.BalancesOptions{
Messages: []sdk.Msg{
&assetfttypes.MsgBurn{},
},
Amount: fundAmount,
})

balanceBefore, err := bankClient.Balance(ctx, &banktypes.QueryBalanceRequest{
Address: user.String(),
Denom: bondDenom,
})
requireT.NoError(err)
balBeforeAmount := balanceBefore.GetBalance().Amount

supplyResBefore, err := bankClient.SupplyOf(ctx, &banktypes.QuerySupplyOfRequest{Denom: bondDenom})
requireT.NoError(err)
supplyBefore := supplyResBefore.GetAmount()

burnAmount := sdkmath.NewInt(1_000_000_000_000)
burnCoin := sdk.NewCoin(bondDenom, burnAmount)
burnMsg := &assetfttypes.MsgBurn{
Sender: user.String(),
Coin: burnCoin,
}

res, err := client.BroadcastTx(
ctx,
chain.ClientContext.WithFromAddress(user),
chain.TxFactory().WithGas(chain.GasLimitByMsgs(burnMsg)),
burnMsg,
)
requireT.NoError(err)

// CHECK 1: Tx must emit a burn event for the exact amount.
burntStr, err := event.FindStringEventAttribute(res.Events, banktypes.EventTypeCoinBurn, sdk.AttributeKeyAmount)
requireT.NoError(err)
requireT.Equal(burnCoin.String(), burntStr, "tx should emit burn event for exact burn amount and denom")

// CHECK 2: Balance decreased by burn amount + fees (within 0.01% for fee variance).
balanceAfter, err := bankClient.Balance(ctx, &banktypes.QueryBalanceRequest{
Address: user.String(),
Denom: bondDenom,
})
requireT.NoError(err)
actualBalanceDecrease := balBeforeAmount.Sub(balanceAfter.GetBalance().Amount)
requireT.InEpsilon(
burnAmount.Int64(),
actualBalanceDecrease.Int64(),
0.0001,
"account balance should decrease by burn amount + fees, expected ~%s, actual: %s",
burnAmount,
actualBalanceDecrease,
)

// CHECK 3: Supply decreased by burn amount. Use looser tolerance (0.5%) because inflation
// or other supply changes can occur in the same block between before/after queries.
supplyResAfter, err := bankClient.SupplyOf(ctx, &banktypes.QuerySupplyOfRequest{Denom: bondDenom})
requireT.NoError(err)
supplyAfter := supplyResAfter.GetAmount()
actualSupplyDecrease := supplyBefore.Amount.Sub(supplyAfter.Amount)
requireT.InEpsilon(
burnAmount.Int64(),
actualSupplyDecrease.Int64(),
0.005,
"supply decrease should be within 0.5%% of burn amount, expected ~%s, actual: %s",
burnAmount,
actualSupplyDecrease,
)
}

// TestAssetFTBurnRate tests burn rate functionality of fungible tokens.
func TestAssetFTBurnRate(t *testing.T) {
t.Parallel()
Expand Down
13 changes: 9 additions & 4 deletions x/asset/ft/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,14 +315,19 @@ func CmdTxBurn() *cobra.Command {
cmd := &cobra.Command{
Use: "burn [amount] --from [sender]",
Args: cobra.ExactArgs(1),
Short: "burn some amount of fungible token",
Short: "burn some amount of fungible token or governance denom",
Long: strings.TrimSpace(
fmt.Sprintf(`Burn some amount of fungible token.
fmt.Sprintf(`Burn some amount of fungible token or the governance/bond denom (core/testcore/devcore).

Example:
$ %s tx %s burn 100000ABC-%s --from [sender]
For AssetFT tokens, the token must have the burning feature enabled.
For the governance denom, no token definition is required.

Examples:
$ %s tx %s burn 100000ABC-%s --from [sender] # Burn AssetFT token
$ %s tx %s burn 50000udevcore --from [sender] # Burn governance denom
`,
version.AppName, types.ModuleName, constant.AddressSampleTest,
version.AppName, types.ModuleName,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down
38 changes: 38 additions & 0 deletions x/asset/ft/client/cli/tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,44 @@ func TestMintBurn(t *testing.T) {
requireT.Equal(sdk.NewInt64Coin(denom, 777).String(), supplyRes.Amount.String())
}

func TestBurnGovernanceDenom(t *testing.T) {
requireT := require.New(t)
testNetwork := network.New(t)

ctx := testNetwork.Validators[0].ClientCtx
burner := testNetwork.Validators[0].Address

var balanceBeforeRes banktypes.QueryBalanceResponse
txchainclitestutil.ExecRootQueryCmd(
t,
ctx,
[]string{banktypes.ModuleName, "balance", burner.String(), testNetwork.Config.BondDenom},
&balanceBeforeRes,
)
balanceBefore := balanceBeforeRes.Balance.Amount

burnAmount := sdk.NewInt64Coin(testNetwork.Config.BondDenom, 1000000)
args := append([]string{burnAmount.String()}, txValidator1Args(testNetwork)...)
_, err := txchainclitestutil.ExecTxCmd(ctx, testNetwork, cli.CmdTxBurn(), args)
requireT.NoError(err)

var balanceAfterRes banktypes.QueryBalanceResponse
txchainclitestutil.ExecRootQueryCmd(
t,
ctx,
[]string{banktypes.ModuleName, "balance", burner.String(), testNetwork.Config.BondDenom},
&balanceAfterRes,
)
balanceDecrease := balanceBefore.Sub(balanceAfterRes.Balance.Amount)
requireT.True(
balanceDecrease.GTE(burnAmount.Amount),
"balance should decrease by at least burn amount, got %s, expected at least %s",
balanceDecrease,
burnAmount.Amount,
)
requireT.True(balanceAfterRes.Balance.Amount.LT(balanceBefore), "balance should decrease after burn")
}

func TestFreezeAndQueryFrozen(t *testing.T) {
requireT := require.New(t)
testNetwork := network.New(t)
Expand Down
33 changes: 32 additions & 1 deletion x/asset/ft/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,8 +458,39 @@ func (k Keeper) Mint(ctx sdk.Context, sender, recipient sdk.AccAddress, coin sdk
return k.mintIfReceivable(ctx, def, coin.Amount, recipient)
}

// Burn burns fungible token.
// Burn burns fungible tokens.
//
// For the bond denom (governance token: core/testcore/devcore), burning is permitted
// without a token definition; AssetFT feature gates do not apply. The bond denom is
// determined dynamically via stakingKeeper.BondDenom(ctx).
//
// For all other denoms, standard AssetFT burn logic applies: the token must have a
// definition with Feature_burning enabled.
//
// IBC denoms (ibc/...) and arbitrary non-AssetFT denoms are rejected.
func (k Keeper) Burn(ctx sdk.Context, sender sdk.AccAddress, coin sdk.Coin) error {
if !coin.IsPositive() {
return sdkerrors.Wrap(cosmoserrors.ErrInvalidCoins, "burn amount must be positive")
}

// Special-case: governance denom (core/testcore/devcore) — no definition exists.
// Always query bond denom dynamically; never hardcode.
stakingParams, err := k.stakingKeeper.GetParams(ctx)
if err != nil {
return sdkerrors.Wrapf(err, "not able to get staking params")
}
bondDenom := stakingParams.BondDenom

if coin.Denom == bondDenom {
// Make sure funds are actually spendable (not vesting/locked/DEX-locked).
if err := k.validateCoinIsNotLockedByDEXAndBank(ctx, sender, coin); err != nil {
return err
}

// Reuse the same burn plumbing used by AssetFT (send to module -> bank burn).
return k.burn(ctx, sender, sdk.NewCoins(coin))
}

def, err := k.GetDefinition(ctx, coin.Denom)
if err != nil {
return sdkerrors.Wrapf(err, "not able to get token info for denom:%s", coin.Denom)
Expand Down
Loading
Loading