mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-26 16:25:21 +00:00
1743cf5275
Fix reserve minting an extra coin when the recipient module both carries fractional over to integer balance AND remainder is insufficient. Adjusts fractional carry to simply send from reserve, instead of doing an additional mint. Add invariant to ensure reserve matches exactly with fractional balances + remainder, failing on both insufficient and excess funds.
395 lines
12 KiB
Go
395 lines
12 KiB
Go
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"
|
|
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
|
|
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
|
minttypes "github.com/cosmos/cosmos-sdk/x/mint/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 mintIntegrationTestSuite struct {
|
|
testutil.Suite
|
|
}
|
|
|
|
func (suite *mintIntegrationTestSuite) SetupTest() {
|
|
suite.Suite.SetupTest()
|
|
}
|
|
|
|
func TestMintIntegrationTest(t *testing.T) {
|
|
suite.Run(t, new(mintIntegrationTestSuite))
|
|
}
|
|
|
|
func (suite *mintIntegrationTestSuite) TestBlockedRecipient() {
|
|
// Tests that sending funds to x/precisebank is disallowed.
|
|
// x/precisebank balance is used as the reserve funds and should not be
|
|
// directly interacted with by external modules or users.
|
|
msgServer := bankkeeper.NewMsgServerImpl(suite.BankKeeper)
|
|
|
|
fromAddr := sdk.AccAddress{1}
|
|
|
|
// To x/precisebank
|
|
toAddr := suite.AccountKeeper.GetModuleAddress(types.ModuleName)
|
|
amount := cs(c("ukava", 1000))
|
|
|
|
msg := banktypes.NewMsgSend(fromAddr, toAddr, amount)
|
|
|
|
_, err := msgServer.Send(sdk.WrapSDKContext(suite.Ctx), msg)
|
|
suite.Require().Error(err)
|
|
|
|
suite.Require().EqualError(
|
|
err,
|
|
fmt.Sprintf("%s is not allowed to receive funds: unauthorized", toAddr.String()),
|
|
)
|
|
}
|
|
|
|
func (suite *mintIntegrationTestSuite) TestMintCoins_MatchingErrors() {
|
|
// x/precisebank MintCoins should be identical to x/bank MintCoins to
|
|
// consumers. This test ensures that the panics & errors returned by
|
|
// x/precisebank are identical to x/bank.
|
|
|
|
tests := []struct {
|
|
name string
|
|
recipientModule string
|
|
mintAmount sdk.Coins
|
|
wantErr string
|
|
wantPanic string
|
|
}{
|
|
{
|
|
"invalid module",
|
|
"notamodule",
|
|
cs(c("ukava", 1000)),
|
|
"",
|
|
"module account notamodule does not exist: unknown address",
|
|
},
|
|
{
|
|
"no mint permissions",
|
|
// Check app.go to ensure this module has no mint permissions
|
|
authtypes.FeeCollectorName,
|
|
cs(c("ukava", 1000)),
|
|
"",
|
|
"module account fee_collector does not have permissions to mint tokens: unauthorized",
|
|
},
|
|
{
|
|
"invalid amount",
|
|
minttypes.ModuleName,
|
|
sdk.Coins{sdk.Coin{Denom: "ukava", Amount: sdkmath.NewInt(-100)}},
|
|
"-100ukava: invalid coins",
|
|
"",
|
|
},
|
|
}
|
|
|
|
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 MintCoins for identical error
|
|
bankErr := suite.BankKeeper.MintCoins(suite.Ctx, tt.recipientModule, tt.mintAmount)
|
|
suite.Require().Error(bankErr)
|
|
suite.Require().EqualError(bankErr, tt.wantErr, "expected error should match x/bank MintCoins error")
|
|
|
|
pbankErr := suite.Keeper.MintCoins(suite.Ctx, tt.recipientModule, tt.mintAmount)
|
|
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 MintCoins 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.MintCoins(suite.Ctx, tt.recipientModule, tt.mintAmount)
|
|
}, "expected panic error should match x/bank MintCoins")
|
|
|
|
suite.Require().PanicsWithError(tt.wantPanic, func() {
|
|
_ = suite.Keeper.MintCoins(suite.Ctx, tt.recipientModule, tt.mintAmount)
|
|
}, "x/precisebank panic should match x/bank MintCoins")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func (suite *mintIntegrationTestSuite) TestMintCoins() {
|
|
type mintTest struct {
|
|
mintAmount sdk.Coins
|
|
// Expected **full** balances after MintCoins(mintAmount)
|
|
wantBalance sdk.Coins
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
recipientModule string
|
|
// Instead of having a start balance, we just have a list of mints to
|
|
// both test & get into desired non-default states.
|
|
mints []mintTest
|
|
}{
|
|
{
|
|
"passthrough - unrelated",
|
|
minttypes.ModuleName,
|
|
[]mintTest{
|
|
{
|
|
mintAmount: cs(c("busd", 1000)),
|
|
wantBalance: cs(c("busd", 1000)),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"passthrough - integer denom",
|
|
minttypes.ModuleName,
|
|
[]mintTest{
|
|
{
|
|
mintAmount: cs(c(types.IntegerCoinDenom, 1000)),
|
|
wantBalance: cs(c(types.ExtendedCoinDenom, 1000000000000000)),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"fractional only",
|
|
minttypes.ModuleName,
|
|
[]mintTest{
|
|
{
|
|
mintAmount: cs(c(types.ExtendedCoinDenom, 1000)),
|
|
wantBalance: cs(c(types.ExtendedCoinDenom, 1000)),
|
|
},
|
|
{
|
|
mintAmount: cs(c(types.ExtendedCoinDenom, 1000)),
|
|
wantBalance: cs(c(types.ExtendedCoinDenom, 2000)),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"fractional only with carry",
|
|
minttypes.ModuleName,
|
|
[]mintTest{
|
|
{
|
|
// Start with (1/4 * 3) = 0.75
|
|
mintAmount: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().QuoRaw(4).MulRaw(3))),
|
|
wantBalance: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().QuoRaw(4).MulRaw(3))),
|
|
},
|
|
{
|
|
// Add another 0.50 to incur carry to test reserve on carry
|
|
mintAmount: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().QuoRaw(2))),
|
|
wantBalance: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().QuoRaw(4).MulRaw(5))),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"fractional only, resulting in exact carry and 0 remainder",
|
|
minttypes.ModuleName,
|
|
[]mintTest{
|
|
// mint 0.5, acc = 0.5, reserve = 1
|
|
{
|
|
mintAmount: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().QuoRaw(2))),
|
|
wantBalance: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().QuoRaw(2))),
|
|
},
|
|
// mint another 0.5, acc = 1, reserve = 0
|
|
// Reserve actually goes down by 1 for integer carry
|
|
{
|
|
mintAmount: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().QuoRaw(2))),
|
|
wantBalance: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor())),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"exact carry",
|
|
minttypes.ModuleName,
|
|
[]mintTest{
|
|
{
|
|
mintAmount: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor())),
|
|
wantBalance: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor())),
|
|
},
|
|
// Carry again - exact amount
|
|
{
|
|
mintAmount: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor())),
|
|
wantBalance: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(2))),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"carry with extra",
|
|
minttypes.ModuleName,
|
|
[]mintTest{
|
|
// MintCoins(C + 100)
|
|
{
|
|
mintAmount: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().AddRaw(100))),
|
|
wantBalance: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().AddRaw(100))),
|
|
},
|
|
// MintCoins(C + 5), total = 2C + 105
|
|
{
|
|
mintAmount: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().AddRaw(5))),
|
|
wantBalance: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(2).AddRaw(105))),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"integer with fractional",
|
|
minttypes.ModuleName,
|
|
[]mintTest{
|
|
{
|
|
mintAmount: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(5).AddRaw(100))),
|
|
wantBalance: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(5).AddRaw(100))),
|
|
},
|
|
{
|
|
mintAmount: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(2).AddRaw(5))),
|
|
wantBalance: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(7).AddRaw(105))),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"with passthrough",
|
|
minttypes.ModuleName,
|
|
[]mintTest{
|
|
{
|
|
mintAmount: cs(
|
|
ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(5).AddRaw(100)),
|
|
c("busd", 1000),
|
|
),
|
|
wantBalance: cs(
|
|
ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(5).AddRaw(100)),
|
|
c("busd", 1000),
|
|
),
|
|
},
|
|
{
|
|
mintAmount: cs(
|
|
ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(2).AddRaw(5)),
|
|
c("meow", 40),
|
|
),
|
|
wantBalance: cs(
|
|
ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(7).AddRaw(105)),
|
|
c("busd", 1000),
|
|
c("meow", 40),
|
|
),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
suite.Run(tt.name, func() {
|
|
// Reset
|
|
suite.SetupTest()
|
|
|
|
recipientAddr := suite.AccountKeeper.GetModuleAddress(tt.recipientModule)
|
|
|
|
for _, mt := range tt.mints {
|
|
err := suite.Keeper.MintCoins(suite.Ctx, tt.recipientModule, mt.mintAmount)
|
|
suite.Require().NoError(err)
|
|
|
|
// -------------------------------------------------------------
|
|
// Check FULL balances
|
|
// x/bank balances + x/precisebank balance
|
|
// Exclude "ukava" as x/precisebank balance will include it
|
|
bankCoins := suite.BankKeeper.GetAllBalances(suite.Ctx, recipientAddr)
|
|
|
|
// Only use x/bank balances for non-ukava denoms
|
|
var denoms []string
|
|
for _, coin := range bankCoins {
|
|
// Ignore integer coins, query the extended denom instead
|
|
if coin.Denom == types.IntegerCoinDenom {
|
|
continue
|
|
}
|
|
|
|
denoms = append(denoms, coin.Denom)
|
|
}
|
|
|
|
// Add the extended denom to the list of denoms to balance check
|
|
// Will be included in balance check even if x/bank doesn't have
|
|
// ukava.
|
|
denoms = append(denoms, types.ExtendedCoinDenom)
|
|
|
|
// All balance queries through x/precisebank
|
|
afterBalance := sdk.NewCoins()
|
|
for _, denom := range denoms {
|
|
coin := suite.Keeper.GetBalance(suite.Ctx, recipientAddr, denom)
|
|
afterBalance = afterBalance.Add(coin)
|
|
}
|
|
|
|
suite.Require().Equal(
|
|
mt.wantBalance.String(),
|
|
afterBalance.String(),
|
|
"unexpected balance after minting %s to %s",
|
|
)
|
|
|
|
// Ensure reserve is backing all minted fractions
|
|
allInvariantsFn := keeper.AllInvariants(suite.Keeper)
|
|
msg, stop := allInvariantsFn(suite.Ctx)
|
|
suite.Require().Falsef(stop, "invariant should not be broken: %s", msg)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func FuzzMintCoins(f *testing.F) {
|
|
f.Add(int64(0))
|
|
f.Add(int64(100))
|
|
f.Add(types.ConversionFactor().Int64())
|
|
f.Add(types.ConversionFactor().QuoRaw(2).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(mintIntegrationTestSuite)
|
|
suite.SetT(t)
|
|
suite.SetS(suite)
|
|
suite.SetupTest()
|
|
|
|
mintCount := int64(10)
|
|
|
|
suite.T().Logf("minting %d %d times", amount, mintCount)
|
|
|
|
// Mint 10 times to include mints from non-zero balances
|
|
for i := int64(0); i < mintCount; i++ {
|
|
err := suite.Keeper.MintCoins(
|
|
suite.Ctx,
|
|
minttypes.ModuleName,
|
|
cs(c(types.ExtendedCoinDenom, amount)),
|
|
)
|
|
suite.Require().NoError(err)
|
|
}
|
|
|
|
// Check FULL balances
|
|
recipientAddr := suite.AccountKeeper.GetModuleAddress(minttypes.ModuleName)
|
|
bal := suite.Keeper.GetBalance(suite.Ctx, recipientAddr, types.ExtendedCoinDenom)
|
|
|
|
suite.Require().Equalf(
|
|
amount*mintCount,
|
|
bal.Amount.Int64(),
|
|
"unexpected balance after minting %d %d times",
|
|
amount,
|
|
mintCount,
|
|
)
|
|
|
|
// Run Invariants to ensure remainder is backing all minted fractions
|
|
// and in a valid state
|
|
res, stop := keeper.AllInvariants(suite.Keeper)(suite.Ctx)
|
|
suite.False(stop, "invariant should not be broken")
|
|
suite.Empty(res, "unexpected invariant message")
|
|
})
|
|
}
|