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

// TestAssetFTBurn_GovernanceDenom tests burning the governance/bond denom (core/testcore/devcore).
func TestAssetFTBurn_GovernanceDenom(t *testing.T) {
t.Parallel()

ctx, chain := integrationtests.NewCoreumTestingContext(t)

requireT := require.New(t)

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

// Fund account with governance denom
// Use large amount (1 million CORE = 10^12 ucore) to overshadow inflation
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,
})

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

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

// Burn 1 million CORE (10^12 ucore) to overshadow inflation
burnAmount := sdkmath.NewInt(1_000_000_000_000)
burnMsg := &assetfttypes.MsgBurn{
Sender: user.String(),
Coin: sdk.Coin{
Denom: bondDenom,
Amount: burnAmount,
},
}

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

// CORRECTNESS CHECK 1: Account balance decreased by burn amount (within tolerance for tx fees)
// Balance decrease = burn amount + transaction fees, so we use epsilon to account for fee variance
balanceAfter, err := bankClient.Balance(ctx, &banktypes.QueryBalanceRequest{
Address: user.String(),
Denom: bondDenom,
})
requireT.NoError(err)
balAfterAmount := balanceAfter.GetBalance().Amount
actualBalanceDecrease := balBeforeAmount.Sub(balAfterAmount)

// Assert balance decreased by at least burn amount (accounting for tx fees with 0.01% tolerance)
requireT.InEpsilon(
burnAmount.Int64(),
actualBalanceDecrease.Int64(),
0.0001,
"account balance should decrease by burn amount + fees, expected: %s, actual: %s",
burnAmount,
actualBalanceDecrease,
)

// CORRECTNESS CHECK 2: Total supply decreased by burn amount (within 0.01% tolerance)
// Supply changes due to inflation, but burn amount is large enough to overshadow it
supplyResAfter, err := bankClient.SupplyOf(ctx, &banktypes.QuerySupplyOfRequest{Denom: bondDenom})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is failing since there is supply increase in every block due to inflation. you can run integration tests via make integration-tests-modules.
to get around the issue, you can increase the burn amount to much larger value(1 million core or 10^12 ucore) to overshadow the minted value by minting module and instead of doing exact match assert that actual supply decrease is within 0.0001 of the expected decrease (you can utilize require.InEplison for assertion).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, i pushed the fixes. Forgot to retest after fix. Pushed updates your way!

requireT.NoError(err)
supplyAfter := supplyResAfter.GetAmount()

actualSupplyDecrease := supplyBefore.Amount.Sub(supplyAfter.Amount)
expectedSupplyDecrease := burnAmount

// Assert supply decrease is within 0.01% of expected (to account for inflation)
requireT.InEpsilon(
expectedSupplyDecrease.Int64(),
actualSupplyDecrease.Int64(),
0.0001,
"supply decrease should be within 0.01%% of burn amount, expected: %s, actual: %s",
expectedSupplyDecrease,
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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add aditional test to tx_test.go in this folder.

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 @@ -223,6 +223,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
coreumclitestutil.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 := coreumclitestutil.ExecTxCmd(ctx, testNetwork, cli.CmdTxBurn(), args)
requireT.NoError(err)

var balanceAfterRes banktypes.QueryBalanceResponse
coreumclitestutil.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
32 changes: 31 additions & 1 deletion x/asset/ft/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,8 +454,38 @@ 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