mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-25 15:55:18 +00:00
feat(x/precisebank): Implement BurnCoins (#1934)
Implement & test BurnCoins method
This commit is contained in:
parent
af5eea690b
commit
38230d35e3
1
Makefile
1
Makefile
@ -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
|
||||||
|
169
x/precisebank/keeper/burn.go
Normal file
169
x/precisebank/keeper/burn.go
Normal 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
|
||||||
|
}
|
485
x/precisebank/keeper/burn_integration_test.go
Normal file
485
x/precisebank/keeper/burn_integration_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
161
x/precisebank/keeper/burn_test.go
Normal file
161
x/precisebank/keeper/burn_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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")
|
|
||||||
}
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user