0g-chain/x/precisebank/keeper/invariants.go
drklee3 1743cf5275
fix(x/precisebank): Ensure exact reserve balance on integer carry when minting (#1932)
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.
2024-06-20 15:20:13 -07:00

206 lines
6.0 KiB
Go

package keeper
import (
"fmt"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/precisebank/types"
)
// RegisterInvariants registers the x/precisebank module invariants
func RegisterInvariants(
ir sdk.InvariantRegistry,
k Keeper,
bk types.BankKeeper,
) {
ir.RegisterRoute(types.ModuleName, "reserve-backs-fractions", ReserveBacksFractionsInvariant(k))
ir.RegisterRoute(types.ModuleName, "balance-remainder-total", BalancedFractionalTotalInvariant(k))
ir.RegisterRoute(types.ModuleName, "valid-fractional-balances", ValidFractionalAmountsInvariant(k))
ir.RegisterRoute(types.ModuleName, "valid-remainder-amount", ValidRemainderAmountInvariant(k))
ir.RegisterRoute(types.ModuleName, "fractional-denom-not-in-bank", FractionalDenomNotInBankInvariant(k))
}
// AllInvariants runs all invariants of the X/precisebank module.
func AllInvariants(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
res, stop := ReserveBacksFractionsInvariant(k)(ctx)
if stop {
return res, stop
}
res, stop = BalancedFractionalTotalInvariant(k)(ctx)
if stop {
return res, stop
}
res, stop = ValidFractionalAmountsInvariant(k)(ctx)
if stop {
return res, stop
}
res, stop = ValidRemainderAmountInvariant(k)(ctx)
if stop {
return res, stop
}
res, stop = FractionalDenomNotInBankInvariant(k)(ctx)
if stop {
return res, stop
}
return "", false
}
}
// ReserveBacksFractionsInvariant checks that the total amount of backing
// coins in the reserve is equal to the total amount of fractional balances,
// such that the backing is always available to redeem all fractional balances
// and there are no extra coins in the reserve that are not backing any
// fractional balances.
func ReserveBacksFractionsInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var (
msg string
broken bool
)
fractionalBalSum := k.GetTotalSumFractionalBalances(ctx)
remainderAmount := k.GetRemainderAmount(ctx)
// Get the total amount of backing coins in the reserve
moduleAddr := k.ak.GetModuleAddress(types.ModuleName)
reserveIntegerBalance := k.bk.GetBalance(ctx, moduleAddr, types.IntegerCoinDenom)
reserveExtendedBalance := reserveIntegerBalance.Amount.Mul(types.ConversionFactor())
// The total amount of backing coins in the reserve should be equal to
// fractional balances + remainder amount
totalRequiredBacking := fractionalBalSum.Add(remainderAmount)
broken = !reserveExtendedBalance.Equal(totalRequiredBacking)
msg = fmt.Sprintf(
"%s reserve balance %s mismatches %s (fractional balances %s + remainder %s)\n",
types.ExtendedCoinDenom,
reserveExtendedBalance,
totalRequiredBacking,
fractionalBalSum,
remainderAmount,
)
return sdk.FormatInvariant(
types.ModuleName, "module reserve backing total fractional balances",
msg,
), broken
}
}
// ValidFractionalAmountsInvariant checks that all individual fractional
// balances are valid.
func ValidFractionalAmountsInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var (
msg string
count int
)
k.IterateFractionalBalances(ctx, func(addr sdk.AccAddress, amount sdkmath.Int) bool {
if err := types.NewFractionalAmountFromInt(amount).Validate(); err != nil {
count++
msg += fmt.Sprintf("\t%s has an invalid fractional amount of %s\n", addr, amount)
}
return false
})
broken := count != 0
return sdk.FormatInvariant(
types.ModuleName, "valid-fractional-balances",
fmt.Sprintf("amount of invalid fractional balances found %d\n%s", count, msg),
), broken
}
}
// ValidRemainderAmountInvariant checks that the remainder amount is valid.
func ValidRemainderAmountInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var (
msg string
broken bool
)
remainderAmount := k.GetRemainderAmount(ctx)
if !remainderAmount.IsZero() {
// Only validate if non-zero, as zero is default value
if err := types.NewFractionalAmountFromInt(remainderAmount).Validate(); err != nil {
broken = true
msg = fmt.Sprintf("remainder amount is invalid: %s", err)
}
}
return sdk.FormatInvariant(
types.ModuleName, "valid-remainder-amount",
msg,
), broken
}
}
// BalancedFractionalTotalInvariant checks that the sum of fractional balances
// and the remainder amount is divisible by the conversion factor without any
// leftover amount.
func BalancedFractionalTotalInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
fractionalBalSum := k.GetTotalSumFractionalBalances(ctx)
remainderAmount := k.GetRemainderAmount(ctx)
total := fractionalBalSum.Add(remainderAmount)
fractionalAmount := total.Mod(types.ConversionFactor())
broken := false
msg := ""
if !fractionalAmount.IsZero() {
broken = true
msg = fmt.Sprintf(
"(sum(FractionalBalances) + remainder) %% conversionFactor should be 0 but got %v",
fractionalAmount,
)
}
return sdk.FormatInvariant(
types.ModuleName, "balance-remainder-total",
msg,
), broken
}
}
// FractionalDenomNotInBankInvariant checks that the bank does not hold any
// fractional denoms. These assets, e.g. akava, should only exist in the
// x/precisebank module as this is a decimal extension of ukava that shares
// the same total supply and is effectively the same asset. ukava held by this
// module in x/bank backs all fractional balances in x/precisebank. If akava
// somehow ends up in x/bank, then it would both break all expectations of this
// module as well as be double-counted in the total supply.
func FractionalDenomNotInBankInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
extBankSupply := k.bk.GetSupply(ctx, types.ExtendedCoinDenom)
broken := !extBankSupply.IsZero()
msg := ""
if broken {
msg = fmt.Sprintf(
"x/bank should not hold any %v but has supply of %v",
types.ExtendedCoinDenom,
extBankSupply,
)
}
return sdk.FormatInvariant(
types.ModuleName, "fractional-denom-not-in-bank",
msg,
), broken
}
}