mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-25 15:55:18 +00:00
feat(x/precisebank): Implement SendCoins (#1923)
Implements methods SendCoins, SendCoinsFromModuleToAccount, SendCoinsFromAccountToModule
This commit is contained in:
parent
4c3f6533a0
commit
409841c79c
15
.github/workflows/ci-default.yml
vendored
15
.github/workflows/ci-default.yml
vendored
@ -35,6 +35,21 @@ jobs:
|
|||||||
run: make test
|
run: make test
|
||||||
- name: run e2e tests
|
- name: run e2e tests
|
||||||
run: make docker-build test-e2e
|
run: make docker-build test-e2e
|
||||||
|
fuzz:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout repo from current commit
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache-dependency-path: |
|
||||||
|
go.sum
|
||||||
|
- name: run fuzz tests
|
||||||
|
run: make test-fuzz
|
||||||
ibc-test:
|
ibc-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
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=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
|
||||||
|
|
||||||
|
@ -142,6 +142,10 @@ func (tApp TestApp) GetKVStoreKey(key string) *storetypes.KVStoreKey {
|
|||||||
return tApp.keys[key]
|
return tApp.keys[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tApp TestApp) GetBlockedMaccAddrs() map[string]bool {
|
||||||
|
return tApp.loadBlockedMaccAddrs()
|
||||||
|
}
|
||||||
|
|
||||||
// LegacyAmino returns the app's amino codec.
|
// LegacyAmino returns the app's amino codec.
|
||||||
func (app *App) LegacyAmino() *codec.LegacyAmino {
|
func (app *App) LegacyAmino() *codec.LegacyAmino {
|
||||||
return app.legacyAmino
|
return app.legacyAmino
|
||||||
|
@ -1,16 +1,29 @@
|
|||||||
package keeper
|
package keeper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
errorsmod "cosmossdk.io/errors"
|
||||||
|
sdkmath "cosmossdk.io/math"
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
"github.com/kava-labs/kava/x/precisebank/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsSendEnabledCoins uses the parent x/bank keeper to check the coins provided
|
// IsSendEnabledCoins uses the parent x/bank keeper to check the coins provided
|
||||||
// and returns an ErrSendDisabled if any of the coins are not configured for
|
// and returns an ErrSendDisabled if any of the coins are not configured for
|
||||||
// sending. Returns nil if sending is enabled for all provided coin
|
// sending. Returns nil if sending is enabled for all provided coin
|
||||||
func (k Keeper) IsSendEnabledCoins(ctx sdk.Context, coins ...sdk.Coin) error {
|
func (k Keeper) IsSendEnabledCoins(ctx sdk.Context, coins ...sdk.Coin) error {
|
||||||
panic("unimplemented")
|
// TODO: This does not actually seem to be used by x/evm, so it should be
|
||||||
|
// removed from the expected_interface in x/evm.
|
||||||
|
return k.bk.IsSendEnabledCoins(ctx, coins...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendCoins transfers amt coins from a sending account to a receiving account.
|
||||||
|
// An error is returned upon failure. This handles transfers including
|
||||||
|
// ExtendedCoinDenom and supports non-ExtendedCoinDenom transfers by passing
|
||||||
|
// through to x/bank.
|
||||||
func (k Keeper) SendCoins(
|
func (k Keeper) SendCoins(
|
||||||
ctx sdk.Context,
|
ctx sdk.Context,
|
||||||
from, to sdk.AccAddress,
|
from, to sdk.AccAddress,
|
||||||
@ -19,23 +32,298 @@ func (k Keeper) SendCoins(
|
|||||||
// IsSendEnabledCoins() is only used in x/bank in msg server, not in keeper,
|
// IsSendEnabledCoins() is only used in x/bank in msg server, not in keeper,
|
||||||
// so we should also not use it here to align with x/bank behavior.
|
// so we should also not use it here to align with x/bank behavior.
|
||||||
|
|
||||||
panic("unimplemented")
|
if !amt.IsValid() {
|
||||||
|
return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, amt.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
passthroughCoins := amt
|
||||||
|
extendedCoinAmount := amt.AmountOf(types.ExtendedCoinDenom)
|
||||||
|
|
||||||
|
// Remove the extended coin amount from the passthrough coins
|
||||||
|
if extendedCoinAmount.IsPositive() {
|
||||||
|
subCoin := sdk.NewCoin(types.ExtendedCoinDenom, extendedCoinAmount)
|
||||||
|
passthroughCoins = amt.Sub(subCoin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the passthrough coins through x/bank
|
||||||
|
if passthroughCoins.IsAllPositive() {
|
||||||
|
if err := k.bk.SendCoins(ctx, from, to, passthroughCoins); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no extended coin amount, we are done
|
||||||
|
if extendedCoinAmount.IsZero() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the extended coin amount through x/precisebank
|
||||||
|
return k.sendExtendedCoins(ctx, from, to, extendedCoinAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendExtendedCoins transfers amt extended coins from a sending account to a
|
||||||
|
// receiving account. An error is returned upon failure. This function is
|
||||||
|
// called by SendCoins() and should not be called directly.
|
||||||
|
//
|
||||||
|
// This method covers 4 cases between two accounts - sender and receiver.
|
||||||
|
// Depending on the fractional balance and the amount being transferred:
|
||||||
|
// Sender account:
|
||||||
|
// 1. Arithmetic borrow 1 integer equivalent amount of fractional coins if the
|
||||||
|
// fractional balance is insufficient.
|
||||||
|
// 2. No borrow if fractional balance is sufficient.
|
||||||
|
//
|
||||||
|
// Receiver account:
|
||||||
|
// 1. Arithmetic carry 1 integer equivalent amount of fractional coins if the
|
||||||
|
// received amount exceeds max fractional balance.
|
||||||
|
// 2. No carry if received amount does not exceed max fractional balance.
|
||||||
|
//
|
||||||
|
// The 4 cases are:
|
||||||
|
// 1. Sender borrow, receiver carry
|
||||||
|
// 2. Sender borrow, NO receiver carry
|
||||||
|
// 3. NO sender borrow, receiver carry
|
||||||
|
// 4. NO sender borrow, NO receiver carry
|
||||||
|
//
|
||||||
|
// Truth table:
|
||||||
|
// | Sender Borrow | Receiver Carry | Direct Transfer |
|
||||||
|
// | --------------|----------------|-----------------|
|
||||||
|
// | T | T | T |
|
||||||
|
// | T | F | F |
|
||||||
|
// | F | T | F |
|
||||||
|
// | F | F | F |
|
||||||
|
func (k Keeper) sendExtendedCoins(
|
||||||
|
ctx sdk.Context,
|
||||||
|
from, to sdk.AccAddress,
|
||||||
|
amt sdkmath.Int,
|
||||||
|
) error {
|
||||||
|
// Sufficient balance check is done by bankkeeper.SendCoins(), for both
|
||||||
|
// integer and fractional-only sends. E.g. If fractional balance is
|
||||||
|
// insufficient, it will still incur a integer borrow which will fail if the
|
||||||
|
// sender does not have sufficient integer balance.
|
||||||
|
|
||||||
|
// Load required state: Account old balances
|
||||||
|
senderFracBal := k.GetFractionalBalance(ctx, from)
|
||||||
|
recipientFracBal := k.GetFractionalBalance(ctx, to)
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Pure stateless calculations
|
||||||
|
integerAmt := amt.Quo(types.ConversionFactor())
|
||||||
|
fractionalAmt := amt.Mod(types.ConversionFactor())
|
||||||
|
|
||||||
|
// Account new fractional balances
|
||||||
|
senderNewFracBal, senderNeedsBorrow := subFromFractionalBalance(senderFracBal, fractionalAmt)
|
||||||
|
recipientNewFracBal, recipientNeedsCarry := addToFractionalBalance(recipientFracBal, fractionalAmt)
|
||||||
|
|
||||||
|
// Case #1: Sender borrow, recipient carry
|
||||||
|
if senderNeedsBorrow && recipientNeedsCarry {
|
||||||
|
// Can directly transfer borrow/carry - increase the direct transfer by 1
|
||||||
|
integerAmt = integerAmt.AddRaw(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Stateful operations for transfers
|
||||||
|
|
||||||
|
// This includes ALL transfers of >= conversionFactor AND Case #1
|
||||||
|
// Full integer amount transfer, including direct transfer of borrow/carry
|
||||||
|
// if any.
|
||||||
|
if integerAmt.IsPositive() {
|
||||||
|
transferCoin := sdk.NewCoin(types.IntegerCoinDenom, integerAmt)
|
||||||
|
if err := k.bk.SendCoins(ctx, from, to, sdk.NewCoins(transferCoin)); err != nil {
|
||||||
|
return k.updateInsufficientFundsError(ctx, from, amt, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case #2: Sender borrow, NO recipient carry
|
||||||
|
// Sender borrows by transferring 1 integer amount to reserve to account for
|
||||||
|
// lack of fractional balance.
|
||||||
|
if senderNeedsBorrow && !recipientNeedsCarry {
|
||||||
|
borrowCoin := sdk.NewCoin(types.IntegerCoinDenom, sdk.NewInt(1))
|
||||||
|
if err := k.bk.SendCoinsFromAccountToModule(
|
||||||
|
ctx,
|
||||||
|
from, // sender borrowing
|
||||||
|
types.ModuleName,
|
||||||
|
sdk.NewCoins(borrowCoin),
|
||||||
|
); err != nil {
|
||||||
|
return k.updateInsufficientFundsError(ctx, from, amt, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case #3: NO sender borrow, recipient carry.
|
||||||
|
// Recipient's fractional balance carries over to integer balance by 1.
|
||||||
|
// Always send carry from reserve before receiving borrow from sender to
|
||||||
|
// ensure reserve always has sufficient balance starting from 0.
|
||||||
|
if !senderNeedsBorrow && recipientNeedsCarry {
|
||||||
|
carryCoin := sdk.NewCoin(types.IntegerCoinDenom, sdk.NewInt(1))
|
||||||
|
if err := k.bk.SendCoinsFromModuleToAccount(
|
||||||
|
ctx,
|
||||||
|
types.ModuleName,
|
||||||
|
to, // recipient carrying
|
||||||
|
sdk.NewCoins(carryCoin),
|
||||||
|
); err != nil {
|
||||||
|
// Panic instead of returning error, as this will only error
|
||||||
|
// with invalid state or logic. Reserve should always have
|
||||||
|
// sufficient balance to carry fractional coins.
|
||||||
|
panic(fmt.Errorf("failed to carry fractional coins to %s: %w", to, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case #4: NO sender borrow, NO recipient carry
|
||||||
|
// No additional operations required, as the transfer of fractional coins
|
||||||
|
// does not incur any integer borrow or carry. New fractional balances
|
||||||
|
// already calculated and just need to be set.
|
||||||
|
|
||||||
|
// Persist new fractional balances to store.
|
||||||
|
k.SetFractionalBalance(ctx, from, senderNewFracBal)
|
||||||
|
k.SetFractionalBalance(ctx, to, recipientNewFracBal)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// subFromFractionalBalance subtracts a fractional amount from the provided
|
||||||
|
// current fractional balance, returning the new fractional balance and true if
|
||||||
|
// an integer borrow is required.
|
||||||
|
func subFromFractionalBalance(
|
||||||
|
currentFractionalBalance sdkmath.Int,
|
||||||
|
amountToSub sdkmath.Int,
|
||||||
|
) (sdkmath.Int, bool) {
|
||||||
|
// Enforce that currentFractionalBalance is not a full balance.
|
||||||
|
if currentFractionalBalance.GTE(types.ConversionFactor()) {
|
||||||
|
panic("currentFractionalBalance must be less than ConversionFactor")
|
||||||
|
}
|
||||||
|
|
||||||
|
if amountToSub.GTE(types.ConversionFactor()) {
|
||||||
|
panic("amountToSub must be less than ConversionFactor")
|
||||||
|
}
|
||||||
|
|
||||||
|
newFractionalBalance := currentFractionalBalance.Sub(amountToSub)
|
||||||
|
|
||||||
|
// Insufficient fractional balance, so we need to borrow.
|
||||||
|
borrowRequired := newFractionalBalance.IsNegative()
|
||||||
|
|
||||||
|
if borrowRequired {
|
||||||
|
// Borrowing 1 integer equivalent amount of fractional coins. We need to
|
||||||
|
// add 1 integer equivalent amount to the fractional balance otherwise
|
||||||
|
// the new fractional balance will be negative.
|
||||||
|
newFractionalBalance = newFractionalBalance.Add(types.ConversionFactor())
|
||||||
|
}
|
||||||
|
|
||||||
|
return newFractionalBalance, borrowRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
// addToFractionalBalance adds a fractional amount to the provided current
|
||||||
|
// fractional balance, returning the new fractional balance and true if a carry
|
||||||
|
// is required.
|
||||||
|
func addToFractionalBalance(
|
||||||
|
currentFractionalBalance sdkmath.Int,
|
||||||
|
amountToAdd sdkmath.Int,
|
||||||
|
) (sdkmath.Int, bool) {
|
||||||
|
// Enforce that currentFractionalBalance is not a full balance.
|
||||||
|
if currentFractionalBalance.GTE(types.ConversionFactor()) {
|
||||||
|
panic("currentFractionalBalance must be less than ConversionFactor")
|
||||||
|
}
|
||||||
|
|
||||||
|
if amountToAdd.GTE(types.ConversionFactor()) {
|
||||||
|
panic("amountToAdd must be less than ConversionFactor")
|
||||||
|
}
|
||||||
|
|
||||||
|
newFractionalBalance := currentFractionalBalance.Add(amountToAdd)
|
||||||
|
|
||||||
|
// New balance exceeds max fractional balance, so we need to carry it over
|
||||||
|
// to the integer balance.
|
||||||
|
carryRequired := newFractionalBalance.GTE(types.ConversionFactor())
|
||||||
|
|
||||||
|
if carryRequired {
|
||||||
|
// Carry over to integer amount
|
||||||
|
newFractionalBalance = newFractionalBalance.Sub(types.ConversionFactor())
|
||||||
|
}
|
||||||
|
|
||||||
|
return newFractionalBalance, carryRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendCoinsFromModuleToModule transfers coins from a ModuleAccount to another.
|
||||||
|
// It will panic if either module account does not exist. An error is returned
|
||||||
|
// if the recipient module is the x/precisebank module account or if sending the
|
||||||
|
// tokens fails.
|
||||||
func (k Keeper) SendCoinsFromAccountToModule(
|
func (k Keeper) SendCoinsFromAccountToModule(
|
||||||
ctx sdk.Context,
|
ctx sdk.Context,
|
||||||
senderAddr sdk.AccAddress,
|
senderAddr sdk.AccAddress,
|
||||||
recipientModule string,
|
recipientModule string,
|
||||||
amt sdk.Coins,
|
amt sdk.Coins,
|
||||||
) error {
|
) error {
|
||||||
panic("unimplemented")
|
recipientAcc := k.ak.GetModuleAccount(ctx, recipientModule)
|
||||||
|
if recipientAcc == nil {
|
||||||
|
panic(errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "module account %s does not exist", recipientModule))
|
||||||
|
}
|
||||||
|
|
||||||
|
if recipientModule == types.ModuleName {
|
||||||
|
return errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "module account %s is not allowed to receive funds", types.ModuleName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return k.SendCoins(ctx, senderAddr, recipientAcc.GetAddress(), amt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendCoinsFromModuleToAccount transfers coins from a ModuleAccount to an AccAddress.
|
||||||
|
// It will panic if the module account does not exist. An error is returned if
|
||||||
|
// the recipient address is blocked, if the sender is the x/precisebank module
|
||||||
|
// account, or if sending the tokens fails.
|
||||||
func (k Keeper) SendCoinsFromModuleToAccount(
|
func (k Keeper) SendCoinsFromModuleToAccount(
|
||||||
ctx sdk.Context,
|
ctx sdk.Context,
|
||||||
senderModule string,
|
senderModule string,
|
||||||
recipientAddr sdk.AccAddress,
|
recipientAddr sdk.AccAddress,
|
||||||
amt sdk.Coins,
|
amt sdk.Coins,
|
||||||
) error {
|
) error {
|
||||||
panic("unimplemented")
|
// Identical panics to x/bank
|
||||||
|
senderAddr := k.ak.GetModuleAddress(senderModule)
|
||||||
|
if senderAddr == nil {
|
||||||
|
panic(errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "module account %s does not exist", senderModule))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom error to prevent external modules from modifying x/precisebank
|
||||||
|
// balances. x/precisebank module account balance is for internal reserve
|
||||||
|
// use only.
|
||||||
|
if senderModule == types.ModuleName {
|
||||||
|
return errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "module account %s is not allowed to send funds", types.ModuleName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uses x/bank BlockedAddr, no need to modify. x/precisebank should be
|
||||||
|
// blocked.
|
||||||
|
if k.bk.BlockedAddr(recipientAddr) {
|
||||||
|
return errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", recipientAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return k.SendCoins(ctx, senderAddr, recipientAddr, amt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateInsufficientFundsError returns a modified ErrInsufficientFunds with
|
||||||
|
// extended coin amounts if the error is due to insufficient funds. Otherwise,
|
||||||
|
// it returns the original error. This is used since x/bank transfers will
|
||||||
|
// return errors with integer coins, but we want the more accurate error that
|
||||||
|
// contains the full extended coin balance and send amounts.
|
||||||
|
func (k Keeper) updateInsufficientFundsError(
|
||||||
|
ctx sdk.Context,
|
||||||
|
addr sdk.AccAddress,
|
||||||
|
amt sdkmath.Int,
|
||||||
|
err error,
|
||||||
|
) error {
|
||||||
|
if !errors.Is(err, sdkerrors.ErrInsufficientFunds) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check balance is sufficient
|
||||||
|
bal := k.GetBalance(ctx, addr, types.ExtendedCoinDenom)
|
||||||
|
coin := sdk.NewCoin(types.ExtendedCoinDenom, amt)
|
||||||
|
|
||||||
|
// TODO: This checks spendable coins and returns error with spendable
|
||||||
|
// coins, not full balance. If GetBalance() is modified to return the
|
||||||
|
// full, including locked, balance then this should be updated to deduct
|
||||||
|
// locked coins.
|
||||||
|
|
||||||
|
// Use sdk.NewCoins() so that it removes empty balances - ie. prints
|
||||||
|
// empty string if balance is 0. This is to match x/bank behavior.
|
||||||
|
spendable := sdk.NewCoins(bal)
|
||||||
|
|
||||||
|
return errorsmod.Wrapf(
|
||||||
|
sdkerrors.ErrInsufficientFunds,
|
||||||
|
"spendable balance %s is smaller than %s",
|
||||||
|
spendable, coin,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
683
x/precisebank/keeper/send_integration_test.go
Normal file
683
x/precisebank/keeper/send_integration_test.go
Normal file
@ -0,0 +1,683 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
|
||||||
|
"github.com/kava-labs/kava/app"
|
||||||
|
"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 sendIntegrationTestSuite struct {
|
||||||
|
testutil.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *sendIntegrationTestSuite) SetupTest() {
|
||||||
|
suite.Suite.SetupTest()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendIntegrationTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(sendIntegrationTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *sendIntegrationTestSuite) TestSendCoinsFromAccountToModule_MatchingErrors() {
|
||||||
|
// No specific errors for SendCoinsFromAccountToModule, only 1 panic if
|
||||||
|
// the module account does not exist
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sender sdk.AccAddress
|
||||||
|
recipientModule string
|
||||||
|
sendAmount sdk.Coins
|
||||||
|
wantPanic string
|
||||||
|
}{
|
||||||
|
// SendCoinsFromAccountToModule specific errors/panics
|
||||||
|
{
|
||||||
|
"missing module account - passthrough",
|
||||||
|
sdk.AccAddress([]byte{2}),
|
||||||
|
"cat",
|
||||||
|
cs(c("usdc", 1000)),
|
||||||
|
"module account cat does not exist: unknown address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"missing module account - extended",
|
||||||
|
sdk.AccAddress([]byte{2}),
|
||||||
|
"cat",
|
||||||
|
cs(c(types.ExtendedCoinDenom, 1000)),
|
||||||
|
"module account cat does not exist: unknown address",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
suite.Run(tt.name, func() {
|
||||||
|
// Reset
|
||||||
|
suite.SetupTest()
|
||||||
|
|
||||||
|
suite.Require().NotEmpty(tt.wantPanic, "test case must have a wantPanic")
|
||||||
|
|
||||||
|
suite.Require().PanicsWithError(tt.wantPanic, func() {
|
||||||
|
suite.BankKeeper.SendCoinsFromAccountToModule(suite.Ctx, tt.sender, tt.recipientModule, tt.sendAmount)
|
||||||
|
}, "wantPanic should match x/bank SendCoinsFromAccountToModule panic")
|
||||||
|
|
||||||
|
suite.Require().PanicsWithError(tt.wantPanic, func() {
|
||||||
|
suite.Keeper.SendCoinsFromAccountToModule(suite.Ctx, tt.sender, tt.recipientModule, tt.sendAmount)
|
||||||
|
}, "x/precisebank panic should match x/bank SendCoinsFromAccountToModule panic")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *sendIntegrationTestSuite) TestSendCoinsFromModuleToAccount_MatchingErrors() {
|
||||||
|
// Ensure errors match x/bank errors AND panics. This needs to be well
|
||||||
|
// tested before SendCoins as all send tests rely on this to initialize
|
||||||
|
// account balances.
|
||||||
|
// No unit test with mock x/bank for SendCoinsFromModuleToAccount since
|
||||||
|
// we only are testing the errors/panics specific to the method and
|
||||||
|
// remaining logic is the same as SendCoins.
|
||||||
|
|
||||||
|
blockedMacAddrs := suite.App.GetBlockedMaccAddrs()
|
||||||
|
precisebankAddr := suite.AccountKeeper.GetModuleAddress(types.ModuleName)
|
||||||
|
|
||||||
|
var blockedAddr sdk.AccAddress
|
||||||
|
// Get the first blocked address
|
||||||
|
for addr, isBlocked := range blockedMacAddrs {
|
||||||
|
// Skip x/precisebank module account
|
||||||
|
if addr == precisebankAddr.String() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isBlocked {
|
||||||
|
blockedAddr = sdk.MustAccAddressFromBech32(addr)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need a ModuleName of another module account to send funds from.
|
||||||
|
// x/precisebank is blocked from use with SendCoinsFromModuleToAccount as we
|
||||||
|
// don't want external modules to modify x/precisebank balances.
|
||||||
|
var senderModuleName string
|
||||||
|
macPerms := app.GetMaccPerms()
|
||||||
|
for moduleName := range macPerms {
|
||||||
|
if moduleName != types.ModuleName {
|
||||||
|
senderModuleName = moduleName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Require().NotEmpty(blockedAddr, "no blocked addresses found")
|
||||||
|
suite.Require().NotEmpty(senderModuleName, "no sender module name found")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
senderModule string
|
||||||
|
recipient sdk.AccAddress
|
||||||
|
sendAmount sdk.Coins
|
||||||
|
wantErr string
|
||||||
|
wantPanic string
|
||||||
|
}{
|
||||||
|
// SendCoinsFromModuleToAccount specific errors/panics
|
||||||
|
{
|
||||||
|
"missing module account - passthrough",
|
||||||
|
"cat",
|
||||||
|
sdk.AccAddress([]byte{2}),
|
||||||
|
cs(c("usdc", 1000)),
|
||||||
|
"",
|
||||||
|
"module account cat does not exist: unknown address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"missing module account - extended",
|
||||||
|
"cat",
|
||||||
|
sdk.AccAddress([]byte{2}),
|
||||||
|
cs(c(types.ExtendedCoinDenom, 1000)),
|
||||||
|
"",
|
||||||
|
"module account cat does not exist: unknown address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"blocked recipient address - passthrough",
|
||||||
|
senderModuleName,
|
||||||
|
blockedAddr,
|
||||||
|
cs(c("usdc", 1000)),
|
||||||
|
fmt.Sprintf("%s is not allowed to receive funds: unauthorized", blockedAddr.String()),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"blocked recipient address - extended",
|
||||||
|
senderModuleName,
|
||||||
|
blockedAddr,
|
||||||
|
cs(c(types.ExtendedCoinDenom, 1000)),
|
||||||
|
fmt.Sprintf("%s is not allowed to receive funds: unauthorized", blockedAddr.String()),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
// SendCoins specific errors/panics
|
||||||
|
{
|
||||||
|
"invalid coins",
|
||||||
|
senderModuleName,
|
||||||
|
sdk.AccAddress([]byte{2}),
|
||||||
|
sdk.Coins{sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(-1)}},
|
||||||
|
"-1ukava: invalid coins",
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insufficient balance - passthrough",
|
||||||
|
senderModuleName,
|
||||||
|
sdk.AccAddress([]byte{2}),
|
||||||
|
cs(c(types.IntegerCoinDenom, 1000)),
|
||||||
|
"spendable balance is smaller than 1000ukava: insufficient funds",
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insufficient balance - extended",
|
||||||
|
senderModuleName,
|
||||||
|
sdk.AccAddress([]byte{2}),
|
||||||
|
// We can still test insufficient bal errors with "akava" since
|
||||||
|
// we also expect it to not exist in x/bank
|
||||||
|
cs(c(types.ExtendedCoinDenom, 1000)),
|
||||||
|
"spendable balance is smaller than 1000akava: insufficient funds",
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
suite.Run(tt.name, func() {
|
||||||
|
// Reset
|
||||||
|
suite.SetupTest()
|
||||||
|
|
||||||
|
if tt.wantPanic == "" && tt.wantErr == "" {
|
||||||
|
suite.FailNow("test case must have a wantErr or wantPanic")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantPanic != "" {
|
||||||
|
suite.Require().Empty(tt.wantErr, "test case must not have a wantErr if wantPanic is set")
|
||||||
|
|
||||||
|
suite.Require().PanicsWithError(tt.wantPanic, func() {
|
||||||
|
suite.BankKeeper.SendCoinsFromModuleToAccount(suite.Ctx, tt.senderModule, tt.recipient, tt.sendAmount)
|
||||||
|
}, "wantPanic should match x/bank SendCoinsFromModuleToAccount panic")
|
||||||
|
|
||||||
|
suite.Require().PanicsWithError(tt.wantPanic, func() {
|
||||||
|
suite.Keeper.SendCoinsFromModuleToAccount(suite.Ctx, tt.senderModule, tt.recipient, tt.sendAmount)
|
||||||
|
}, "x/precisebank panic should match x/bank SendCoinsFromModuleToAccount panic")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
bankErr := suite.BankKeeper.SendCoinsFromModuleToAccount(suite.Ctx, tt.senderModule, tt.recipient, tt.sendAmount)
|
||||||
|
suite.Require().Error(bankErr)
|
||||||
|
suite.Require().EqualError(bankErr, tt.wantErr, "expected error should match x/bank SendCoins error")
|
||||||
|
|
||||||
|
pbankErr := suite.Keeper.SendCoinsFromModuleToAccount(suite.Ctx, tt.senderModule, tt.recipient, tt.sendAmount)
|
||||||
|
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 SendCoins error",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *sendIntegrationTestSuite) TestSendCoins_MatchingErrors() {
|
||||||
|
// Ensure errors match x/bank errors
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
initialAmount sdk.Coins
|
||||||
|
sendAmount sdk.Coins
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"invalid coins",
|
||||||
|
cs(),
|
||||||
|
sdk.Coins{sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(-1)}},
|
||||||
|
"-1ukava: invalid coins",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insufficient empty balance - passthrough",
|
||||||
|
cs(),
|
||||||
|
cs(c(types.IntegerCoinDenom, 1000)),
|
||||||
|
"spendable balance is smaller than 1000ukava: insufficient funds",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insufficient empty balance - extended",
|
||||||
|
cs(),
|
||||||
|
// We can still test insufficient bal errors with "akava" since
|
||||||
|
// we also expect it to not exist in x/bank
|
||||||
|
cs(c(types.ExtendedCoinDenom, 1000)),
|
||||||
|
"spendable balance is smaller than 1000akava: insufficient funds",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insufficient non-empty balance - passthrough",
|
||||||
|
cs(c(types.IntegerCoinDenom, 100), c("usdc", 1000)),
|
||||||
|
cs(c(types.IntegerCoinDenom, 1000)),
|
||||||
|
"spendable balance 100ukava is smaller than 1000ukava: insufficient funds",
|
||||||
|
},
|
||||||
|
// non-empty akava transfer error is tested in SendCoins, not here since
|
||||||
|
// x/bank doesn't hold akava
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
suite.Run(tt.name, func() {
|
||||||
|
// Reset
|
||||||
|
suite.SetupTest()
|
||||||
|
sender := sdk.AccAddress([]byte{1})
|
||||||
|
recipient := sdk.AccAddress([]byte{2})
|
||||||
|
|
||||||
|
suite.Require().NotEmpty(tt.wantErr, "test case must have a wantErr")
|
||||||
|
|
||||||
|
suite.MintToAccount(sender, tt.initialAmount)
|
||||||
|
|
||||||
|
bankErr := suite.BankKeeper.SendCoins(suite.Ctx, sender, recipient, tt.sendAmount)
|
||||||
|
suite.Require().Error(bankErr)
|
||||||
|
suite.Require().EqualError(bankErr, tt.wantErr, "expected error should match x/bank SendCoins error")
|
||||||
|
|
||||||
|
pbankErr := suite.Keeper.SendCoins(suite.Ctx, sender, recipient, tt.sendAmount)
|
||||||
|
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 SendCoins error",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *sendIntegrationTestSuite) TestSendCoins() {
|
||||||
|
// SendCoins is tested mostly in this integration test, as a unit test with
|
||||||
|
// mocked BankKeeper overcomplicates expected keepers and makes initializing
|
||||||
|
// balances very complex.
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
giveStartBalSender sdk.Coins
|
||||||
|
giveStartBalRecipient sdk.Coins
|
||||||
|
giveAmt sdk.Coins
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"insufficient balance error denom matches",
|
||||||
|
cs(c(types.ExtendedCoinDenom, 10), c("usdc", 1000)),
|
||||||
|
cs(),
|
||||||
|
cs(c(types.ExtendedCoinDenom, 1000)),
|
||||||
|
"spendable balance 10akava is smaller than 1000akava: insufficient funds",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"passthrough - unrelated",
|
||||||
|
cs(c("cats", 1000)),
|
||||||
|
cs(),
|
||||||
|
cs(c("cats", 1000)),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"passthrough - integer denom",
|
||||||
|
cs(c(types.IntegerCoinDenom, 1000)),
|
||||||
|
cs(),
|
||||||
|
cs(c(types.IntegerCoinDenom, 1000)),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"passthrough & extended",
|
||||||
|
cs(c(types.IntegerCoinDenom, 1000)),
|
||||||
|
cs(),
|
||||||
|
cs(c(types.IntegerCoinDenom, 10), c(types.ExtendedCoinDenom, 1)),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"akava send - 1akava to 0 balance",
|
||||||
|
// Starting balances
|
||||||
|
cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(5))),
|
||||||
|
cs(),
|
||||||
|
// Send amount
|
||||||
|
cs(c(types.ExtendedCoinDenom, 1)), // akava
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sender borrow from integer",
|
||||||
|
// 1ukava, 0 fractional
|
||||||
|
cs(ci(types.ExtendedCoinDenom, types.ConversionFactor())),
|
||||||
|
cs(),
|
||||||
|
// Send 1 with 0 fractional balance
|
||||||
|
cs(c(types.ExtendedCoinDenom, 1)),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sender borrow from integer - max fractional amount",
|
||||||
|
// 1ukava, 0 fractional
|
||||||
|
cs(ci(types.ExtendedCoinDenom, types.ConversionFactor())),
|
||||||
|
cs(),
|
||||||
|
// Max fractional amount
|
||||||
|
cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1))),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"receiver carry",
|
||||||
|
cs(c(types.ExtendedCoinDenom, 1000)),
|
||||||
|
// max fractional amount, carries over to integer
|
||||||
|
cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1))),
|
||||||
|
cs(c(types.ExtendedCoinDenom, 1)),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"receiver carry - max fractional amount",
|
||||||
|
cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(5))),
|
||||||
|
// max fractional amount, carries over to integer
|
||||||
|
cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1))),
|
||||||
|
cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1))),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
suite.Run(tt.name, func() {
|
||||||
|
suite.SetupTest()
|
||||||
|
|
||||||
|
sender := sdk.AccAddress([]byte{1})
|
||||||
|
recipient := sdk.AccAddress([]byte{2})
|
||||||
|
|
||||||
|
// Initialize balances
|
||||||
|
suite.MintToAccount(sender, tt.giveStartBalSender)
|
||||||
|
suite.MintToAccount(recipient, tt.giveStartBalRecipient)
|
||||||
|
|
||||||
|
senderBalBefore := suite.GetAllBalances(sender)
|
||||||
|
recipientBalBefore := suite.GetAllBalances(recipient)
|
||||||
|
|
||||||
|
err := suite.Keeper.SendCoins(suite.Ctx, sender, recipient, tt.giveAmt)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
suite.Require().Error(err)
|
||||||
|
suite.Require().EqualError(err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Check balances
|
||||||
|
senderBalAfter := suite.GetAllBalances(sender)
|
||||||
|
recipientBalAfter := suite.GetAllBalances(recipient)
|
||||||
|
|
||||||
|
// Convert send amount coins to extended coins. i.e. if send coins
|
||||||
|
// includes ukava, convert it so that its the equivalent akava
|
||||||
|
// amount so its easier to compare. Compare extended coins only.
|
||||||
|
sendAmountExtended := tt.giveAmt
|
||||||
|
sendAmountInteger := tt.giveAmt.AmountOf(types.IntegerCoinDenom)
|
||||||
|
if !sendAmountInteger.IsZero() {
|
||||||
|
integerCoin := sdk.NewCoin(types.IntegerCoinDenom, sendAmountInteger)
|
||||||
|
sendAmountExtended = sendAmountExtended.Sub(integerCoin)
|
||||||
|
|
||||||
|
// Add equivalent extended coin
|
||||||
|
extendedCoinAmount := sendAmountInteger.Mul(types.ConversionFactor())
|
||||||
|
extendedCoin := sdk.NewCoin(types.ExtendedCoinDenom, extendedCoinAmount)
|
||||||
|
sendAmountExtended = sendAmountExtended.Add(extendedCoin)
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Require().Equal(
|
||||||
|
senderBalBefore.Sub(sendAmountExtended...),
|
||||||
|
senderBalAfter,
|
||||||
|
)
|
||||||
|
|
||||||
|
suite.Require().Equal(
|
||||||
|
recipientBalBefore.Add(sendAmountExtended...),
|
||||||
|
recipientBalAfter,
|
||||||
|
)
|
||||||
|
|
||||||
|
invariantFn := keeper.AllInvariants(suite.Keeper)
|
||||||
|
res, stop := invariantFn(suite.Ctx)
|
||||||
|
suite.Require().False(stop, "invariants should not stop")
|
||||||
|
suite.Require().Empty(res, "invariants should not return any messages")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *sendIntegrationTestSuite) TestSendCoins_Matrix() {
|
||||||
|
// SendCoins is tested mostly in this integration test, as a unit test with
|
||||||
|
// mocked BankKeeper overcomplicates expected keepers and makes initializing
|
||||||
|
// balances very complex.
|
||||||
|
|
||||||
|
type startBalance struct {
|
||||||
|
name string
|
||||||
|
bal sdk.Coins
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run through each combination of start sender/recipient balance & send amt
|
||||||
|
// Test matrix fields:
|
||||||
|
startBalances := []startBalance{
|
||||||
|
{"empty", cs()},
|
||||||
|
{"integer only", cs(c(types.IntegerCoinDenom, 1000))},
|
||||||
|
{"extended only", cs(c(types.ExtendedCoinDenom, 1000))},
|
||||||
|
{"integer & extended", cs(c(types.IntegerCoinDenom, 1000), c(types.ExtendedCoinDenom, 1000))},
|
||||||
|
{"integer & extended - max fractional", cs(c(types.IntegerCoinDenom, 1000), ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1)))},
|
||||||
|
{"integer & extended - min fractional", cs(c(types.IntegerCoinDenom, 1000), c(types.ExtendedCoinDenom, 1))},
|
||||||
|
}
|
||||||
|
|
||||||
|
sendAmts := []struct {
|
||||||
|
name string
|
||||||
|
amt sdk.Coins
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"empty",
|
||||||
|
cs(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"integer only",
|
||||||
|
cs(c(types.IntegerCoinDenom, 10)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"extended only",
|
||||||
|
cs(c(types.ExtendedCoinDenom, 10)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"integer & extended",
|
||||||
|
cs(c(types.IntegerCoinDenom, 10), c(types.ExtendedCoinDenom, 1000)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"integer & extended - max fractional",
|
||||||
|
cs(c(types.IntegerCoinDenom, 10), ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"integer & extended - min fractional",
|
||||||
|
cs(c(types.IntegerCoinDenom, 10), c(types.ExtendedCoinDenom, 1)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, senderStartBal := range startBalances {
|
||||||
|
for _, recipientStartBal := range startBalances {
|
||||||
|
for _, sendAmt := range sendAmts {
|
||||||
|
testName := fmt.Sprintf(
|
||||||
|
"%s -> %s (%s -> %s), send %s (%s)",
|
||||||
|
senderStartBal.name, senderStartBal.bal,
|
||||||
|
recipientStartBal.name, recipientStartBal.bal,
|
||||||
|
sendAmt.name, sendAmt.amt,
|
||||||
|
)
|
||||||
|
|
||||||
|
suite.Run(testName, func() {
|
||||||
|
suite.SetupTest()
|
||||||
|
|
||||||
|
sender := sdk.AccAddress([]byte{1})
|
||||||
|
recipient := sdk.AccAddress([]byte{2})
|
||||||
|
|
||||||
|
// Initialize balances
|
||||||
|
suite.MintToAccount(sender, senderStartBal.bal)
|
||||||
|
suite.MintToAccount(recipient, recipientStartBal.bal)
|
||||||
|
|
||||||
|
// balances & send amount will only contain total equivalent
|
||||||
|
// extended coins and no integer coins so its easier to compare
|
||||||
|
senderBalBefore := suite.GetAllBalances(sender)
|
||||||
|
recipientBalBefore := suite.GetAllBalances(recipient)
|
||||||
|
|
||||||
|
sendAmtNormalized := testutil.ConvertCoinsToExtendedCoinDenom(sendAmt.amt)
|
||||||
|
|
||||||
|
err := suite.Keeper.SendCoins(suite.Ctx, sender, recipient, sendAmt.amt)
|
||||||
|
|
||||||
|
hasSufficientBal := senderBalBefore.IsAllGTE(sendAmtNormalized)
|
||||||
|
|
||||||
|
if hasSufficientBal {
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
} else {
|
||||||
|
suite.Require().Error(err, "expected insufficient funds error")
|
||||||
|
// No balance checks if insufficient funds
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check balances
|
||||||
|
senderBalAfter := suite.GetAllBalances(sender)
|
||||||
|
recipientBalAfter := suite.GetAllBalances(recipient)
|
||||||
|
|
||||||
|
// Convert send amount coins to extended coins. i.e. if send coins
|
||||||
|
// includes ukava, convert it so that its the equivalent akava
|
||||||
|
// amount so its easier to compare. Compare extended coins only.
|
||||||
|
|
||||||
|
suite.Require().Equal(
|
||||||
|
senderBalBefore.Sub(sendAmtNormalized...),
|
||||||
|
senderBalAfter,
|
||||||
|
)
|
||||||
|
|
||||||
|
suite.Require().Equal(
|
||||||
|
recipientBalBefore.Add(sendAmtNormalized...),
|
||||||
|
recipientBalAfter,
|
||||||
|
)
|
||||||
|
|
||||||
|
invariantFn := keeper.AllInvariants(suite.Keeper)
|
||||||
|
res, stop := invariantFn(suite.Ctx)
|
||||||
|
suite.Require().False(stop, "invariants should not stop")
|
||||||
|
suite.Require().Empty(res, "invariants should not return any messages")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *sendIntegrationTestSuite) TestSendCoinsFromAccountToModule() {
|
||||||
|
// Ensure recipient correctly matches the specified module account. Specific
|
||||||
|
// send amount and cases are handled by SendCoins() tests, so we are only
|
||||||
|
// checking SendCoinsFromAccountToModule specific behavior here.
|
||||||
|
|
||||||
|
sender := sdk.AccAddress([]byte{1})
|
||||||
|
recipientModule := minttypes.ModuleName
|
||||||
|
recipientAddr := suite.AccountKeeper.GetModuleAddress(recipientModule)
|
||||||
|
|
||||||
|
sendAmt := cs(c(types.ExtendedCoinDenom, 1000))
|
||||||
|
|
||||||
|
suite.MintToAccount(sender, sendAmt)
|
||||||
|
|
||||||
|
err := suite.Keeper.SendCoinsFromAccountToModule(
|
||||||
|
suite.Ctx,
|
||||||
|
sender,
|
||||||
|
recipientModule,
|
||||||
|
sendAmt,
|
||||||
|
)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Check balances
|
||||||
|
senderBalAfter := suite.GetAllBalances(sender)
|
||||||
|
recipientBalAfter := suite.GetAllBalances(recipientAddr)
|
||||||
|
|
||||||
|
suite.Require().Equal(
|
||||||
|
cs(),
|
||||||
|
senderBalAfter,
|
||||||
|
)
|
||||||
|
|
||||||
|
suite.Require().Equal(
|
||||||
|
sendAmt,
|
||||||
|
recipientBalAfter,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *sendIntegrationTestSuite) TestSendCoinsFromModuleToAccount() {
|
||||||
|
// Ensure sender correctly matches the specified module account. Opposite
|
||||||
|
// of SendCoinsFromAccountToModule, so we are only checking the correct
|
||||||
|
// addresses are being used.
|
||||||
|
|
||||||
|
senderModule := "community"
|
||||||
|
senderAddr := suite.AccountKeeper.GetModuleAddress(senderModule)
|
||||||
|
|
||||||
|
recipient := sdk.AccAddress([]byte{1})
|
||||||
|
|
||||||
|
sendAmt := cs(c(types.ExtendedCoinDenom, 1000))
|
||||||
|
|
||||||
|
suite.MintToAccount(senderAddr, sendAmt)
|
||||||
|
|
||||||
|
err := suite.Keeper.SendCoinsFromModuleToAccount(
|
||||||
|
suite.Ctx,
|
||||||
|
senderModule,
|
||||||
|
recipient,
|
||||||
|
sendAmt,
|
||||||
|
)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Check balances
|
||||||
|
senderBalAfter := suite.GetAllBalances(senderAddr)
|
||||||
|
recipientBalAfter := suite.GetAllBalances(recipient)
|
||||||
|
|
||||||
|
suite.Require().Equal(
|
||||||
|
cs(),
|
||||||
|
senderBalAfter,
|
||||||
|
)
|
||||||
|
|
||||||
|
suite.Require().Equal(
|
||||||
|
sendAmt,
|
||||||
|
recipientBalAfter,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzSendCoins(f *testing.F) {
|
||||||
|
f.Add(uint64(100), uint64(0), uint64(2))
|
||||||
|
f.Add(uint64(100), uint64(100), uint64(5))
|
||||||
|
f.Add(types.ConversionFactor().Uint64(), uint64(0), uint64(500))
|
||||||
|
f.Add(
|
||||||
|
types.ConversionFactor().MulRaw(2).AddRaw(123948723).Uint64(),
|
||||||
|
types.ConversionFactor().MulRaw(2).Uint64(),
|
||||||
|
types.ConversionFactor().Uint64(),
|
||||||
|
)
|
||||||
|
|
||||||
|
f.Fuzz(func(
|
||||||
|
t *testing.T,
|
||||||
|
startBalSender uint64,
|
||||||
|
startBalReceiver uint64,
|
||||||
|
sendAmount uint64,
|
||||||
|
) {
|
||||||
|
// Manually setup test suite since no direct Fuzz support in test suites
|
||||||
|
suite := new(sendIntegrationTestSuite)
|
||||||
|
suite.SetT(t)
|
||||||
|
suite.SetS(suite)
|
||||||
|
suite.SetupTest()
|
||||||
|
|
||||||
|
sender := sdk.AccAddress([]byte{1})
|
||||||
|
recipient := sdk.AccAddress([]byte{2})
|
||||||
|
|
||||||
|
// Initial balances
|
||||||
|
suite.MintToAccount(sender, cs(c(types.ExtendedCoinDenom, int64(startBalSender))))
|
||||||
|
suite.MintToAccount(recipient, cs(c(types.ExtendedCoinDenom, int64(startBalReceiver))))
|
||||||
|
|
||||||
|
// Send amount
|
||||||
|
sendCoins := cs(c(types.ExtendedCoinDenom, int64(sendAmount)))
|
||||||
|
err := suite.Keeper.SendCoins(suite.Ctx, sender, recipient, sendCoins)
|
||||||
|
if startBalSender < sendAmount {
|
||||||
|
suite.Require().Error(err, "expected insufficient funds error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Check FULL balances
|
||||||
|
balSender := suite.GetAllBalances(sender)
|
||||||
|
balReceiver := suite.GetAllBalances(recipient)
|
||||||
|
|
||||||
|
suite.Require().Equal(
|
||||||
|
startBalSender-sendAmount,
|
||||||
|
balSender.AmountOf(types.ExtendedCoinDenom).Uint64(),
|
||||||
|
)
|
||||||
|
suite.Require().Equal(
|
||||||
|
startBalReceiver+sendAmount,
|
||||||
|
balReceiver.AmountOf(types.ExtendedCoinDenom).Uint64(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run Invariants to ensure remainder is backing all minted fractions
|
||||||
|
// and in a valid state
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
47
x/precisebank/keeper/send_test.go
Normal file
47
x/precisebank/keeper/send_test.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSendCoinsFromAccountToModule_BlockedReserve(t *testing.T) {
|
||||||
|
// Other modules shouldn't be able to send x/precisebank coins as the module
|
||||||
|
// account balance is for internal reserve use only.
|
||||||
|
|
||||||
|
td := NewMockedTestData(t)
|
||||||
|
td.ak.EXPECT().
|
||||||
|
GetModuleAccount(td.ctx, types.ModuleName).
|
||||||
|
Return(authtypes.NewModuleAccount(
|
||||||
|
authtypes.NewBaseAccountWithAddress(sdk.AccAddress{100}),
|
||||||
|
types.ModuleName,
|
||||||
|
)).
|
||||||
|
Once()
|
||||||
|
|
||||||
|
fromAddr := sdk.AccAddress([]byte{1})
|
||||||
|
err := td.keeper.SendCoinsFromAccountToModule(td.ctx, fromAddr, types.ModuleName, cs(c("busd", 1000)))
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, err, "module account precisebank is not allowed to receive funds: unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendCoinsFromModuleToAccount_BlockedReserve(t *testing.T) {
|
||||||
|
// Other modules shouldn't be able to send x/precisebank module account
|
||||||
|
// funds.
|
||||||
|
|
||||||
|
td := NewMockedTestData(t)
|
||||||
|
td.ak.EXPECT().
|
||||||
|
GetModuleAddress(types.ModuleName).
|
||||||
|
Return(sdk.AccAddress{100}).
|
||||||
|
Once()
|
||||||
|
|
||||||
|
toAddr := sdk.AccAddress([]byte{1})
|
||||||
|
err := td.keeper.SendCoinsFromModuleToAccount(td.ctx, types.ModuleName, toAddr, cs(c("busd", 1000)))
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, err, "module account precisebank is not allowed to send funds: unauthorized")
|
||||||
|
}
|
@ -12,11 +12,13 @@ import (
|
|||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
|
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
|
||||||
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
|
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
|
||||||
|
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
|
||||||
"github.com/evmos/ethermint/crypto/ethsecp256k1"
|
"github.com/evmos/ethermint/crypto/ethsecp256k1"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
"github.com/kava-labs/kava/app"
|
"github.com/kava-labs/kava/app"
|
||||||
"github.com/kava-labs/kava/x/precisebank/keeper"
|
"github.com/kava-labs/kava/x/precisebank/keeper"
|
||||||
|
"github.com/kava-labs/kava/x/precisebank/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Suite struct {
|
type Suite struct {
|
||||||
@ -87,3 +89,63 @@ func (suite *Suite) Commit() {
|
|||||||
// update ctx
|
// update ctx
|
||||||
suite.Ctx = suite.App.NewContext(false, header)
|
suite.Ctx = suite.App.NewContext(false, header)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MintToAccount mints coins to an account with the x/precisebank methods. This
|
||||||
|
// must be used when minting extended coins, ie. akava coins. This depends on
|
||||||
|
// the methods to be properly tested to be implemented correctly.
|
||||||
|
func (suite *Suite) MintToAccount(addr sdk.AccAddress, amt sdk.Coins) {
|
||||||
|
accBalancesBefore := suite.GetAllBalances(addr)
|
||||||
|
|
||||||
|
err := suite.Keeper.MintCoins(suite.Ctx, minttypes.ModuleName, amt)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
err = suite.Keeper.SendCoinsFromModuleToAccount(suite.Ctx, minttypes.ModuleName, addr, amt)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Double check balances are correctly minted and sent to account
|
||||||
|
accBalancesAfter := suite.GetAllBalances(addr)
|
||||||
|
|
||||||
|
netIncrease := accBalancesAfter.Sub(accBalancesBefore...)
|
||||||
|
suite.Require().Equal(ConvertCoinsToExtendedCoinDenom(amt), netIncrease)
|
||||||
|
|
||||||
|
suite.T().Logf("minted %s to %s", amt, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllBalances returns all the account balances for the given account address.
|
||||||
|
// This returns the extended coin balance if the account has a non-zero balance,
|
||||||
|
// WITHOUT the integer coin balance.
|
||||||
|
func (suite *Suite) GetAllBalances(addr sdk.AccAddress) sdk.Coins {
|
||||||
|
// Get all balances for an account
|
||||||
|
bankBalances := suite.BankKeeper.GetAllBalances(suite.Ctx, addr)
|
||||||
|
|
||||||
|
// Remove integer coins from the balance
|
||||||
|
for _, coin := range bankBalances {
|
||||||
|
if coin.Denom == types.IntegerCoinDenom {
|
||||||
|
bankBalances = bankBalances.Sub(coin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the integer coin with the extended coin, from x/precisebank
|
||||||
|
extendedBal := suite.Keeper.GetBalance(suite.Ctx, addr, types.ExtendedCoinDenom)
|
||||||
|
|
||||||
|
return bankBalances.Add(extendedBal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertCoinsToExtendedCoinDenom converts sdk.Coins that includes Integer denoms
|
||||||
|
// to sdk.Coins that includes Extended denoms of the same amount. This is useful
|
||||||
|
// for testing to make sure only extended amounts are compared instead of double
|
||||||
|
// counting balances.
|
||||||
|
func ConvertCoinsToExtendedCoinDenom(coins sdk.Coins) sdk.Coins {
|
||||||
|
integerCoinAmt := coins.AmountOf(types.IntegerCoinDenom)
|
||||||
|
if integerCoinAmt.IsZero() {
|
||||||
|
return coins
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the integer coin from the coins
|
||||||
|
integerCoin := sdk.NewCoin(types.IntegerCoinDenom, integerCoinAmt)
|
||||||
|
|
||||||
|
// Add the equivalent extended coin to the coins
|
||||||
|
extendedCoin := sdk.NewCoin(types.ExtendedCoinDenom, integerCoinAmt.Mul(types.ConversionFactor()))
|
||||||
|
|
||||||
|
return coins.Sub(integerCoin).Add(extendedCoin)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user