0g-chain/x/precisebank/keeper/mint.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

200 lines
7.7 KiB
Go

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"
)
// MintCoins creates new coins from thin air and adds it to the module account.
// If ExtendedCoinDenom is provided, the corresponding fractional amount is
// added to the module state.
// It will panic if the module account does not exist or is unauthorized.
func (k Keeper) MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error {
// Disallow minting to x/precisebank module
if moduleName == types.ModuleName {
panic(errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "module account %s cannot be minted to", moduleName))
}
// Note: MintingRestrictionFn is not used in x/precisebank
// 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.Minter) {
panic(errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "module account %s does not have permissions to mint tokens", moduleName))
}
// Ensure the coins are valid before minting
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.MintCoins(ctx, moduleName, passthroughCoins); err != nil {
return err
}
}
// No more processing required if no ExtendedCoinDenom
if extendedAmount.IsZero() {
return nil
}
return k.mintExtendedCoin(ctx, moduleName, extendedAmount)
}
// mintExtendedCoin manages the minting of only extended coins. This also
// handles integer carry over from fractional balance to integer balance if
// necessary depending on the fractional balance and minting amount. Ensures
// that the reserve fully backs the additional minted amount, minting any extra
// reserve integer coins if necessary.
// 4 Cases:
// 1. NO integer carry over, >= 0 remainder - no reserve mint
// 2. NO integer carry over, negative remainder - mint 1 to reserve
// 3. Integer carry over, >= 0 remainder
// - Transfer 1 integer from reserve -> account
//
// 4. Integer carry over, negative remainder
// - Transfer 1 integer from reserve -> account
// - Mint 1 to reserve
// Optimization:
// - Increase direct account mint amount by 1, no extra reserve mint
func (k Keeper) mintExtendedCoin(
ctx sdk.Context,
recipientModuleName string,
amt sdkmath.Int,
) error {
moduleAddr := k.ak.GetModuleAddress(recipientModuleName)
// Get current module account fractional balance - 0 if not found
fractionalAmount := k.GetFractionalBalance(ctx, moduleAddr)
// Get separated mint amounts
integerMintAmount := amt.Quo(types.ConversionFactor())
fractionalMintAmount := amt.Mod(types.ConversionFactor())
// Get previous remainder amount, as we need to it before carry calculation
// for the optimization path.
prevRemainder := k.GetRemainderAmount(ctx)
// Deduct new remainder with minted fractional amount. This will result in
// two cases:
// 1. Zero or positive remainder: remainder is sufficient to back the minted
// fractional amount. Reserve is also sufficient to back the minted amount
// so no additional reserve integer coin is needed.
// 2. Negative remainder: remainder is insufficient to back the minted
// fractional amount. Reserve will need to be increased to back the mint
// amount.
newRemainder := prevRemainder.Sub(fractionalMintAmount)
// Get new fractional balance after minting, this could be greater than
// the conversion factor and must be checked for carry over to integer mint
// amount as being set as-is may cause fractional balance exceeding max.
newFractionalBalance := fractionalAmount.Add(fractionalMintAmount)
// Case #3 - Integer carry, remainder is sufficient (0 or positive)
if newFractionalBalance.GTE(types.ConversionFactor()) && newRemainder.GTE(sdkmath.ZeroInt()) {
// Carry should send from reserve -> account, instead of minting an
// extra integer coin. Otherwise doing an extra mint will require a burn
// from reserves to maintain exact backing.
carryCoin := sdk.NewCoin(types.IntegerCoinDenom, sdkmath.OneInt())
// SendCoinsFromModuleToModule allows for sending coins even if the
// recipient module account is blocked.
if err := k.bk.SendCoinsFromModuleToModule(
ctx,
types.ModuleName,
recipientModuleName,
sdk.NewCoins(carryCoin),
); err != nil {
return err
}
}
// Case #4 - Integer carry, remainder is insufficient
// This is the optimization path where the integer mint amount is increased
// by 1, instead of doing both a reserve -> account transfer and reserve mint.
if newFractionalBalance.GTE(types.ConversionFactor()) && newRemainder.IsNegative() {
integerMintAmount = integerMintAmount.AddRaw(1)
}
// If it carries over, adjust the fractional balance to account for the
// previously added 1 integer amount.
// fractional amounts x and y where both x and y < ConversionFactor
// x + y < (2 * ConversionFactor) - 2
// x + y < 1 integer amount + fractional amount
if newFractionalBalance.GTE(types.ConversionFactor()) {
// Subtract 1 integer equivalent amount of fractional balance. Same
// behavior as using .Mod() in this case.
newFractionalBalance = newFractionalBalance.Sub(types.ConversionFactor())
}
// Mint new integer amounts in x/bank - including carry over from fractional
// amount if any.
if integerMintAmount.IsPositive() {
integerMintCoin := sdk.NewCoin(types.IntegerCoinDenom, integerMintAmount)
if err := k.bk.MintCoins(
ctx,
recipientModuleName,
sdk.NewCoins(integerMintCoin),
); err != nil {
return err
}
}
// Assign new fractional balance in x/precisebank
k.SetFractionalBalance(ctx, moduleAddr, newFractionalBalance)
// ----------------------------------------
// Update remainder & reserves to back minted fractional coins
// Mint an additional reserve integer coin if remainder is insufficient.
// The remainder is the amount of fractional coins that can be minted and
// still be fully backed by reserve. If the remainder is less than the
// minted fractional amount, then the reserve needs to be increased to
// back the additional fractional amount.
// Optimization: This is only done when the integer amount does NOT carry,
// as a direct account mint is done instead of integer carry transfer +
// insufficient remainder reserve mint.
wasCarried := fractionalAmount.Add(fractionalMintAmount).GTE(types.ConversionFactor())
if prevRemainder.LT(fractionalMintAmount) && !wasCarried {
// Always only 1 integer coin, as fractionalMintAmount < ConversionFactor
reserveMintCoins := sdk.NewCoins(sdk.NewCoin(types.IntegerCoinDenom, sdkmath.OneInt()))
if err := k.bk.MintCoins(ctx, types.ModuleName, reserveMintCoins); err != nil {
return fmt.Errorf("failed to mint %s for reserve: %w", reserveMintCoins, err)
}
}
// newRemainder will be negative if prevRemainder < fractionalMintAmount.
// This needs to be adjusted back to the corresponding positive value. The
// remainder will be always < conversionFactor after add if it is negative.
if newRemainder.IsNegative() {
newRemainder = newRemainder.Add(types.ConversionFactor())
}
k.SetRemainderAmount(ctx, newRemainder)
return nil
}