feat(x/precisebank): Implement BurnCoins (#1934)

Implement & test BurnCoins method
This commit is contained in:
drklee3 2024-06-20 15:02:23 -07:00 committed by GitHub
parent af5eea690b
commit 38230d35e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 866 additions and 5 deletions

View File

@ -334,6 +334,7 @@ endif
test-fuzz: test-fuzz:
@$(GO_BIN) test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzMintCoins ./x/precisebank/keeper @$(GO_BIN) test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzMintCoins ./x/precisebank/keeper
@$(GO_BIN) test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzBurnCoins ./x/precisebank/keeper
@$(GO_BIN) test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzSendCoins ./x/precisebank/keeper @$(GO_BIN) test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzSendCoins ./x/precisebank/keeper
@$(GO_BIN) test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzGenesisStateValidate_NonZeroRemainder ./x/precisebank/types @$(GO_BIN) test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzGenesisStateValidate_NonZeroRemainder ./x/precisebank/types
@$(GO_BIN) test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzGenesisStateValidate_ZeroRemainder ./x/precisebank/types @$(GO_BIN) test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzGenesisStateValidate_ZeroRemainder ./x/precisebank/types

View File

@ -0,0 +1,169 @@
package keeper
import (
"fmt"
errorsmod "cosmossdk.io/errors"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/kava-labs/kava/x/precisebank/types"
)
// BurnCoins burns coins deletes coins from the balance of the module account.
// It will panic if the module account does not exist or is unauthorized.
func (k Keeper) BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error {
// Custom protection for x/precisebank, no external module should be able to
// affect reserves.
if moduleName == types.ModuleName {
panic(errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "module account %s cannot be burned from", moduleName))
}
// Panic errors are identical to x/bank for consistency.
acc := k.ak.GetModuleAccount(ctx, moduleName)
if acc == nil {
panic(errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "module account %s does not exist", moduleName))
}
if !acc.HasPermission(authtypes.Burner) {
panic(errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "module account %s does not have permissions to burn tokens", moduleName))
}
// Ensure the coins are valid before burning
if !amt.IsValid() {
return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, amt.String())
}
// Get non-ExtendedCoinDenom coins
passthroughCoins := amt
extendedAmount := amt.AmountOf(types.ExtendedCoinDenom)
if extendedAmount.IsPositive() {
// Remove ExtendedCoinDenom from the coins as it is managed by x/precisebank
removeCoin := sdk.NewCoin(types.ExtendedCoinDenom, extendedAmount)
passthroughCoins = amt.Sub(removeCoin)
}
// Coins unmanaged by x/precisebank are passed through to x/bank
if !passthroughCoins.Empty() {
if err := k.bk.BurnCoins(ctx, moduleName, passthroughCoins); err != nil {
return err
}
}
// No more processing required if no ExtendedCoinDenom
if extendedAmount.IsZero() {
return nil
}
return k.burnExtendedCoin(ctx, moduleName, extendedAmount)
}
// burnExtendedCoin burns the fractional amount of the ExtendedCoinDenom from the module account.
func (k Keeper) burnExtendedCoin(
ctx sdk.Context,
moduleName string,
amt sdkmath.Int,
) error {
// Get the module address
moduleAddr := k.ak.GetModuleAddress(moduleName)
// We only need the fractional balance to burn coins, as integer burns will
// return errors on insufficient funds.
prevFractionalBalance := k.GetFractionalBalance(ctx, moduleAddr)
// Get remainder amount first to optimize direct burn.
prevRemainder := k.GetRemainderAmount(ctx)
// -------------------------------------------------------------------------
// Pure stateless calculations
integerBurnAmount := amt.Quo(types.ConversionFactor())
fractionalBurnAmount := amt.Mod(types.ConversionFactor())
// newFractionalBalance can be negative if fractional balance is insufficient.
newFractionalBalance := prevFractionalBalance.Sub(fractionalBurnAmount)
// If true, fractional balance is insufficient and will require an integer
// borrow.
requiresBorrow := newFractionalBalance.IsNegative()
// Add to new remainder with burned fractional amount.
newRemainder := prevRemainder.Add(fractionalBurnAmount)
// If true, remainder has accumulated enough fractional amounts to burn 1
// integer coin.
overflowingRemainder := newRemainder.GTE(types.ConversionFactor())
// -------------------------------------------------------------------------
// Stateful operations for burn
// Not enough fractional balance:
// 1. If the new remainder incurs an additional reserve burn, we can just
// burn an additional integer coin from the account directly instead as
// an optimization.
// 2. If the new remainder is still under conversion factor (no extra
// reserve burn) then we need to transfer 1 integer coin to the reserve
// for the integer borrow.
// Case #1: (optimization) direct burn instead of borrow (reserve transfer)
// & reserve burn. No additional reserve burn would be necessary after this.
if requiresBorrow && overflowingRemainder {
newFractionalBalance = newFractionalBalance.Add(types.ConversionFactor())
newRemainder = newRemainder.Sub(types.ConversionFactor())
integerBurnAmount = integerBurnAmount.AddRaw(1)
}
// Case #2: Transfer 1 integer coin to reserve for integer borrow to ensure
// reserve fully backs the fractional amount.
if requiresBorrow && !overflowingRemainder {
newFractionalBalance = newFractionalBalance.Add(types.ConversionFactor())
// Transfer 1 integer coin to reserve to cover the borrowed fractional
// amount. SendCoinsFromModuleToModule will return an error if the
// module account has insufficient funds and an error with the full
// extended balance will be returned.
borrowCoin := sdk.NewCoin(types.IntegerCoinDenom, sdkmath.OneInt())
if err := k.bk.SendCoinsFromModuleToModule(
ctx,
moduleName,
types.ModuleName, // borrowed integer is transferred to reserve
sdk.NewCoins(borrowCoin),
); err != nil {
return k.updateInsufficientFundsError(ctx, moduleAddr, amt, err)
}
}
// Case #3: Does not require borrow, but remainder has accumulated enough
// fractional amounts to burn 1 integer coin.
if !requiresBorrow && overflowingRemainder {
reserveBurnCoins := sdk.NewCoins(sdk.NewCoin(types.IntegerCoinDenom, sdkmath.OneInt()))
if err := k.bk.BurnCoins(ctx, types.ModuleName, reserveBurnCoins); err != nil {
return fmt.Errorf("failed to burn %s for reserve: %w", reserveBurnCoins, err)
}
newRemainder = newRemainder.Sub(types.ConversionFactor())
}
// Case #4: No additional work required, no borrow needed and no additional
// reserve burn
// Burn the integer amount - this may include the extra optimization burn
// from case #1
if !integerBurnAmount.IsZero() {
coin := sdk.NewCoin(types.IntegerCoinDenom, integerBurnAmount)
if err := k.bk.BurnCoins(ctx, moduleName, sdk.NewCoins(coin)); err != nil {
return k.updateInsufficientFundsError(ctx, moduleAddr, amt, err)
}
}
// Assign new fractional balance in x/precisebank
k.SetFractionalBalance(ctx, moduleAddr, newFractionalBalance)
// Update remainder for burned fractional coins
k.SetRemainderAmount(ctx, newRemainder)
return nil
}

View File

@ -0,0 +1,485 @@
package keeper_test
import (
"fmt"
"testing"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
ibctransfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types"
"github.com/kava-labs/kava/x/precisebank/keeper"
"github.com/kava-labs/kava/x/precisebank/testutil"
"github.com/kava-labs/kava/x/precisebank/types"
"github.com/stretchr/testify/suite"
)
type burnIntegrationTestSuite struct {
testutil.Suite
}
func (suite *burnIntegrationTestSuite) SetupTest() {
suite.Suite.SetupTest()
}
func TestBurnIntegrationTest(t *testing.T) {
suite.Run(t, new(burnIntegrationTestSuite))
}
func (suite *burnIntegrationTestSuite) TestBurnCoins_MatchingErrors() {
// x/precisebank BurnCoins should be identical to x/bank BurnCoins to
// consumers. This test ensures that the panics & errors returned by
// x/precisebank are identical to x/bank.
tests := []struct {
name string
recipientModule string
setupFn func()
burnAmount sdk.Coins
wantErr string
wantPanic string
}{
{
"invalid module",
"notamodule",
func() {},
cs(c("ukava", 1000)),
"",
"module account notamodule does not exist: unknown address",
},
{
"no burn permissions",
// Check app.go to ensure this module has no burn permissions
authtypes.FeeCollectorName,
func() {},
cs(c("ukava", 1000)),
"",
"module account fee_collector does not have permissions to burn tokens: unauthorized",
},
{
"invalid amount",
// Has burn permissions so it goes to the amt check
ibctransfertypes.ModuleName,
func() {},
sdk.Coins{sdk.Coin{Denom: "ukava", Amount: sdkmath.NewInt(-100)}},
"-100ukava: invalid coins",
"",
},
{
"insufficient balance - empty",
ibctransfertypes.ModuleName,
func() {},
cs(c("ukava", 1000)),
"spendable balance is smaller than 1000ukava: insufficient funds",
"",
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
// Reset
suite.SetupTest()
if tt.wantErr == "" && tt.wantPanic == "" {
suite.Fail("test must specify either wantErr or wantPanic")
}
if tt.wantErr != "" {
// Check x/bank BurnCoins for identical error
bankErr := suite.BankKeeper.BurnCoins(suite.Ctx, tt.recipientModule, tt.burnAmount)
suite.Require().Error(bankErr)
suite.Require().EqualError(bankErr, tt.wantErr, "expected error should match x/bank BurnCoins error")
pbankErr := suite.Keeper.BurnCoins(suite.Ctx, tt.recipientModule, tt.burnAmount)
suite.Require().Error(pbankErr)
// Compare strings instead of errors, as error stack is still different
suite.Require().Equal(
bankErr.Error(),
pbankErr.Error(),
"x/precisebank error should match x/bank BurnCoins error",
)
}
if tt.wantPanic != "" {
// First check the wantPanic string is correct.
// Actually specify the panic string in the test since it makes
// it more clear we are testing specific and different cases.
suite.Require().PanicsWithError(tt.wantPanic, func() {
_ = suite.BankKeeper.BurnCoins(suite.Ctx, tt.recipientModule, tt.burnAmount)
}, "expected panic error should match x/bank BurnCoins")
suite.Require().PanicsWithError(tt.wantPanic, func() {
_ = suite.Keeper.BurnCoins(suite.Ctx, tt.recipientModule, tt.burnAmount)
}, "x/precisebank panic should match x/bank BurnCoins")
}
})
}
}
func (suite *burnIntegrationTestSuite) TestBurnCoins() {
tests := []struct {
name string
startBalance sdk.Coins
burnCoins sdk.Coins
wantBalance sdk.Coins
wantErr string
}{
{
"passthrough - unrelated",
cs(c("meow", 1000)),
cs(c("meow", 1000)),
cs(),
"",
},
{
"passthrough - integer denom",
cs(c(types.IntegerCoinDenom, 2000)),
cs(c(types.IntegerCoinDenom, 1000)),
cs(c(types.ExtendedCoinDenom, 1000000000000000)),
"",
},
{
"fractional only - no borrow",
cs(c(types.ExtendedCoinDenom, 1000)),
cs(c(types.ExtendedCoinDenom, 500)),
cs(c(types.ExtendedCoinDenom, 500)),
"",
},
{
"fractional burn - borrows",
cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().AddRaw(100))),
cs(c(types.ExtendedCoinDenom, 500)),
cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(400))),
"",
},
{
"error - insufficient integer balance",
cs(ci(types.ExtendedCoinDenom, types.ConversionFactor())),
cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(2))),
cs(),
// Returns correct error with akava balance (rewrites Bank BurnCoins err)
"spendable balance 1000000000000akava is smaller than 2000000000000akava: insufficient funds",
},
{
"error - insufficient fractional, borrow",
cs(c(types.ExtendedCoinDenom, 1000)),
cs(c(types.ExtendedCoinDenom, 2000)),
cs(),
// Error from SendCoins to reserve
"spendable balance 1000akava is smaller than 2000akava: insufficient funds",
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
// Reset
suite.SetupTest()
moduleName := ibctransfertypes.ModuleName
recipientAddr := suite.AccountKeeper.GetModuleAddress(moduleName)
// Start balance
err := suite.Keeper.MintCoins(suite.Ctx, moduleName, tt.startBalance)
suite.Require().NoError(err)
// Burn
err = suite.Keeper.BurnCoins(suite.Ctx, moduleName, tt.burnCoins)
if tt.wantErr != "" {
suite.Require().Error(err)
suite.Require().EqualError(err, tt.wantErr)
return
}
suite.Require().NoError(err)
// -------------------------------------------------------------
// Check FULL balances
// x/bank balances + x/precisebank balance
// Exclude "ukava" as x/precisebank balance will include it
afterBalance := suite.GetAllBalances(recipientAddr)
suite.Require().Equal(
tt.wantBalance.String(),
afterBalance.String(),
"unexpected balance after minting %s to %s",
)
// Ensure reserve is backing all minted fractions
allInvariantsFn := keeper.AllInvariants(suite.Keeper)
res, stop := allInvariantsFn(suite.Ctx)
suite.Require().False(stop, "invariant should not be broken")
suite.Require().Empty(res, "unexpected invariant message: %s", res)
})
}
}
func (suite *burnIntegrationTestSuite) TestBurnCoins_Remainder() {
// This tests a series of small burns to ensure the remainder is both
// updated correctly and reserve is correctly updated. This only burns from
// 1 single account.
reserveAddr := suite.AccountKeeper.GetModuleAddress(types.ModuleName)
moduleName := ibctransfertypes.ModuleName
moduleAddr := suite.AccountKeeper.GetModuleAddress(moduleName)
startCoins := cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(5)))
// Start balance
err := suite.Keeper.MintCoins(
suite.Ctx,
moduleName,
startCoins,
)
suite.Require().NoError(err)
burnAmt := types.ConversionFactor().QuoRaw(10)
burnCoins := cs(ci(types.ExtendedCoinDenom, burnAmt))
// Burn 0.1 until balance is 0
for {
reserveBalBefore := suite.Keeper.GetBalance(
suite.Ctx,
reserveAddr,
types.IntegerCoinDenom,
)
balBefore := suite.Keeper.GetBalance(
suite.Ctx,
moduleAddr,
types.ExtendedCoinDenom,
)
remainderBefore := suite.Keeper.GetRemainderAmount(suite.Ctx)
// ----------------------------------------
// Burn
err := suite.Keeper.BurnCoins(
suite.Ctx,
moduleName,
burnCoins,
)
suite.Require().NoError(err)
// ----------------------------------------
// Checks
remainderAfter := suite.Keeper.GetRemainderAmount(suite.Ctx)
balAfter := suite.Keeper.GetBalance(
suite.Ctx,
moduleAddr,
types.ExtendedCoinDenom,
)
reserveBalAfter := suite.Keeper.GetBalance(
suite.Ctx,
reserveAddr,
types.IntegerCoinDenom,
)
suite.Require().Equal(
balBefore.Amount.Sub(burnAmt).String(),
balAfter.Amount.String(),
"balance should decrease by burn amount",
)
// Remainder should be updated correctly
suite.Require().Equal(
remainderBefore.Add(burnAmt).Mod(types.ConversionFactor()),
remainderAfter,
)
// If remainder has exceeded (then rolled over), reserve should be updated
if remainderAfter.LT(remainderBefore) {
suite.Require().Equal(
reserveBalBefore.Amount.SubRaw(1).String(),
reserveBalAfter.Amount.String(),
"reserve should decrease by 1 if remainder exceeds ConversionFactor",
)
}
// No more to burn
if balAfter.Amount.IsZero() {
break
}
}
// Run Invariants to ensure remainder is backing all fractions correctly
res, stop := keeper.AllInvariants(suite.Keeper)(suite.Ctx)
suite.Require().False(stop, "invariant should not be broken")
suite.Require().Empty(res, "unexpected invariant message: %s", res)
}
func (suite *burnIntegrationTestSuite) TestBurnCoins_Spread_Remainder() {
// This tests a series of small burns to ensure the remainder is both
// updated correctly and reserve is correctly updated. This burns from
// a series of multiple accounts, to test when the remainder is modified
// by multiple accounts.
reserveAddr := suite.AccountKeeper.GetModuleAddress(types.ModuleName)
burnerModuleName := ibctransfertypes.ModuleName
burnerAddr := suite.AccountKeeper.GetModuleAddress(burnerModuleName)
accCount := 20
startCoins := cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(5)))
addrs := []sdk.AccAddress{}
for i := 0; i < accCount; i++ {
addr := sdk.AccAddress(fmt.Sprintf("addr%d", i))
suite.MintToAccount(addr, startCoins)
addrs = append(addrs, addr)
}
burnAmt := types.ConversionFactor().QuoRaw(10)
burnCoins := cs(ci(types.ExtendedCoinDenom, burnAmt))
// Burn 0.1 from each account
for _, addr := range addrs {
reserveBalBefore := suite.Keeper.GetBalance(
suite.Ctx,
reserveAddr,
types.IntegerCoinDenom,
)
balBefore := suite.Keeper.GetBalance(
suite.Ctx,
addr,
types.ExtendedCoinDenom,
)
remainderBefore := suite.Keeper.GetRemainderAmount(suite.Ctx)
// ----------------------------------------
// Send & Burn
err := suite.Keeper.SendCoins(
suite.Ctx,
addr,
burnerAddr,
burnCoins,
)
suite.Require().NoError(err)
err = suite.Keeper.BurnCoins(
suite.Ctx,
burnerModuleName,
burnCoins,
)
suite.Require().NoError(err)
// ----------------------------------------
// Checks
remainderAfter := suite.Keeper.GetRemainderAmount(suite.Ctx)
balAfter := suite.Keeper.GetBalance(
suite.Ctx,
addr,
types.ExtendedCoinDenom,
)
reserveBalAfter := suite.Keeper.GetBalance(
suite.Ctx,
reserveAddr,
types.IntegerCoinDenom,
)
suite.Require().Equal(
balBefore.Amount.Sub(burnAmt).String(),
balAfter.Amount.String(),
"balance should decrease by burn amount",
)
// Remainder should be updated correctly
suite.Require().Equal(
remainderBefore.Add(burnAmt).Mod(types.ConversionFactor()),
remainderAfter,
)
suite.T().Logf("acc: %s", string(addr.Bytes()))
suite.T().Logf("acc bal: %s -> %s", balBefore, balAfter)
suite.T().Logf("remainder: %s -> %s", remainderBefore, remainderAfter)
suite.T().Logf("reserve: %v -> %v", reserveBalBefore, reserveBalAfter)
// Reserve will change when:
// 1. Account needs to borrow from integer (transfers to reserve)
// 2. Remainder meets or exceeds conversion factor (burn 1 from reserve)
reserveIncrease := sdkmath.ZeroInt()
// Does account need to borrow from integer?
if balBefore.Amount.Mod(types.ConversionFactor()).LT(burnAmt) {
reserveIncrease = reserveIncrease.AddRaw(1)
}
// If remainder has exceeded (then rolled over), burn additional 1
if remainderBefore.Add(burnAmt).GTE(types.ConversionFactor()) {
reserveIncrease = reserveIncrease.SubRaw(1)
}
suite.Require().Equal(
reserveBalBefore.Amount.Add(reserveIncrease).String(),
reserveBalAfter.Amount.String(),
"reserve should be updated by remainder and borrowing",
)
// Run Invariants to ensure remainder is backing all fractions correctly
res, stop := keeper.AllInvariants(suite.Keeper)(suite.Ctx)
suite.Require().False(stop, "invariant should not be broken")
suite.Require().Empty(res, "unexpected invariant message: %s", res)
}
}
func FuzzBurnCoins(f *testing.F) {
f.Add(int64(0))
f.Add(int64(100))
f.Add(types.ConversionFactor().Int64())
f.Add(types.ConversionFactor().MulRaw(5).Int64())
f.Add(types.ConversionFactor().MulRaw(2).AddRaw(123948723).Int64())
f.Fuzz(func(t *testing.T, amount int64) {
// No negative amounts
if amount < 0 {
amount = -amount
}
// Manually setup test suite since no direct Fuzz support in test suites
suite := new(burnIntegrationTestSuite)
suite.SetT(t)
suite.SetS(suite)
suite.SetupTest()
burnCount := int64(10)
// Has both mint & burn permissions
moduleName := ibctransfertypes.ModuleName
recipientAddr := suite.AccountKeeper.GetModuleAddress(moduleName)
// Start balance
err := suite.Keeper.MintCoins(
suite.Ctx,
moduleName,
cs(ci(types.ExtendedCoinDenom, sdkmath.NewInt(amount).MulRaw(burnCount))),
)
suite.Require().NoError(err)
// Burn multiple times to ensure different balance scenarios
for i := int64(0); i < burnCount; i++ {
err := suite.Keeper.BurnCoins(
suite.Ctx,
moduleName,
cs(c(types.ExtendedCoinDenom, amount)),
)
suite.Require().NoError(err)
}
// Check FULL balances
balAfter := suite.Keeper.GetBalance(suite.Ctx, recipientAddr, types.ExtendedCoinDenom)
suite.Require().Equalf(
int64(0),
balAfter.Amount.Int64(),
"all coins should be burned, got %d",
balAfter.Amount.Int64(),
)
// Run Invariants to ensure remainder is backing all fractions correctly
allInvariantsFn := keeper.AllInvariants(suite.Keeper)
res, stop := allInvariantsFn(suite.Ctx)
suite.Require().False(stop, "invariant should not be broken")
suite.Require().Empty(res, "unexpected invariant message: %s", res)
})
}

View File

@ -0,0 +1,161 @@
package keeper_test
import (
"fmt"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/kava-labs/kava/x/precisebank/types"
"github.com/stretchr/testify/require"
)
// Testing module name for mocked GetModuleAccount()
const burnerModuleName = "burner-module"
func TestBurnCoins_PanicValidations(t *testing.T) {
// panic tests for invalid inputs
tests := []struct {
name string
recipientModule string
setupFn func(td testData)
burnAmount sdk.Coins
wantPanic string
}{
{
"invalid module",
"notamodule",
func(td testData) {
// Make module not found
td.ak.EXPECT().
GetModuleAccount(td.ctx, "notamodule").
Return(nil).
Once()
},
cs(c("ukava", 1000)),
"module account notamodule does not exist: unknown address",
},
{
"no permission",
burnerModuleName,
func(td testData) {
td.ak.EXPECT().
GetModuleAccount(td.ctx, burnerModuleName).
Return(authtypes.NewModuleAccount(
authtypes.NewBaseAccountWithAddress(sdk.AccAddress{1}),
burnerModuleName,
// no burn permission
)).
Once()
},
cs(c("ukava", 1000)),
fmt.Sprintf("module account %s does not have permissions to burn tokens: unauthorized", burnerModuleName),
},
{
"has burn permission",
burnerModuleName,
func(td testData) {
td.ak.EXPECT().
GetModuleAccount(td.ctx, burnerModuleName).
Return(authtypes.NewModuleAccount(
authtypes.NewBaseAccountWithAddress(sdk.AccAddress{1}),
burnerModuleName,
// includes burner permission
authtypes.Burner,
)).
Once()
// Will call x/bank BurnCoins coins
td.bk.EXPECT().
BurnCoins(td.ctx, burnerModuleName, cs(c("ukava", 1000))).
Return(nil).
Once()
},
cs(c("ukava", 1000)),
"",
},
{
"disallow burning from x/precisebank",
types.ModuleName,
func(td testData) {
// No mock setup needed since this is checked before module
// account checks
},
cs(c("ukava", 1000)),
"module account precisebank cannot be burned from: unauthorized",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
td := NewMockedTestData(t)
tt.setupFn(td)
if tt.wantPanic != "" {
require.PanicsWithError(t, tt.wantPanic, func() {
_ = td.keeper.BurnCoins(td.ctx, tt.recipientModule, tt.burnAmount)
})
return
}
require.NotPanics(t, func() {
// Not testing errors, only panics for this test
_ = td.keeper.BurnCoins(td.ctx, tt.recipientModule, tt.burnAmount)
})
})
}
}
func TestBurnCoins_Errors(t *testing.T) {
// returned errors, not panics
tests := []struct {
name string
recipientModule string
setupFn func(td testData)
burnAmount sdk.Coins
wantError string
}{
{
"invalid coins",
burnerModuleName,
func(td testData) {
// Valid module account burner
td.ak.EXPECT().
GetModuleAccount(td.ctx, burnerModuleName).
Return(authtypes.NewModuleAccount(
authtypes.NewBaseAccountWithAddress(sdk.AccAddress{1}),
burnerModuleName,
// includes burner permission
authtypes.Burner,
)).
Once()
},
sdk.Coins{sdk.Coin{
Denom: "ukava",
Amount: sdk.NewInt(-1000),
}},
"-1000ukava: invalid coins",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
td := NewMockedTestData(t)
tt.setupFn(td)
require.NotPanics(t, func() {
err := td.keeper.BurnCoins(td.ctx, tt.recipientModule, tt.burnAmount)
if tt.wantError != "" {
require.Error(t, err)
require.EqualError(t, err, tt.wantError)
return
}
require.NoError(t, err)
})
})
}
}

View File

@ -3,7 +3,6 @@ package keeper
import ( import (
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
storetypes "github.com/cosmos/cosmos-sdk/store/types" storetypes "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
evmtypes "github.com/evmos/ethermint/x/evm/types" evmtypes "github.com/evmos/ethermint/x/evm/types"
@ -36,7 +35,3 @@ func NewKeeper(
ak: ak, ak: ak,
} }
} }
func (k Keeper) BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error {
panic("unimplemented")
}

View File

@ -24,6 +24,7 @@ type BankKeeper interface {
SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
SendCoinsFromModuleToModule(ctx sdk.Context, senderModule string, recipientModule string, amt sdk.Coins) error
MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error
BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error

View File

@ -466,6 +466,55 @@ func (_c *MockBankKeeper_SendCoinsFromModuleToAccount_Call) RunAndReturn(run fun
return _c return _c
} }
// SendCoinsFromModuleToModule provides a mock function with given fields: ctx, senderModule, recipientModule, amt
func (_m *MockBankKeeper) SendCoinsFromModuleToModule(ctx types.Context, senderModule string, recipientModule string, amt types.Coins) error {
ret := _m.Called(ctx, senderModule, recipientModule, amt)
if len(ret) == 0 {
panic("no return value specified for SendCoinsFromModuleToModule")
}
var r0 error
if rf, ok := ret.Get(0).(func(types.Context, string, string, types.Coins) error); ok {
r0 = rf(ctx, senderModule, recipientModule, amt)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockBankKeeper_SendCoinsFromModuleToModule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendCoinsFromModuleToModule'
type MockBankKeeper_SendCoinsFromModuleToModule_Call struct {
*mock.Call
}
// SendCoinsFromModuleToModule is a helper method to define mock.On call
// - ctx types.Context
// - senderModule string
// - recipientModule string
// - amt types.Coins
func (_e *MockBankKeeper_Expecter) SendCoinsFromModuleToModule(ctx interface{}, senderModule interface{}, recipientModule interface{}, amt interface{}) *MockBankKeeper_SendCoinsFromModuleToModule_Call {
return &MockBankKeeper_SendCoinsFromModuleToModule_Call{Call: _e.mock.On("SendCoinsFromModuleToModule", ctx, senderModule, recipientModule, amt)}
}
func (_c *MockBankKeeper_SendCoinsFromModuleToModule_Call) Run(run func(ctx types.Context, senderModule string, recipientModule string, amt types.Coins)) *MockBankKeeper_SendCoinsFromModuleToModule_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(types.Context), args[1].(string), args[2].(string), args[3].(types.Coins))
})
return _c
}
func (_c *MockBankKeeper_SendCoinsFromModuleToModule_Call) Return(_a0 error) *MockBankKeeper_SendCoinsFromModuleToModule_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockBankKeeper_SendCoinsFromModuleToModule_Call) RunAndReturn(run func(types.Context, string, string, types.Coins) error) *MockBankKeeper_SendCoinsFromModuleToModule_Call {
_c.Call.Return(run)
return _c
}
// SpendableCoins provides a mock function with given fields: ctx, addr // SpendableCoins provides a mock function with given fields: ctx, addr
func (_m *MockBankKeeper) SpendableCoins(ctx types.Context, addr types.AccAddress) types.Coins { func (_m *MockBankKeeper) SpendableCoins(ctx types.Context, addr types.AccAddress) types.Coins {
ret := _m.Called(ctx, addr) ret := _m.Called(ctx, addr)