Skip to content

Commit

Permalink
feat(x/precisebank): Implement BurnCoins (#1934)
Browse files Browse the repository at this point in the history
Implement & test BurnCoins method
  • Loading branch information
drklee3 authored Jun 20, 2024
1 parent af5eea6 commit 38230d3
Show file tree
Hide file tree
Showing 7 changed files with 866 additions and 5 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ endif

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=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=FuzzGenesisStateValidate_NonZeroRemainder ./x/precisebank/types
@$(GO_BIN) test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzGenesisStateValidate_ZeroRemainder ./x/precisebank/types
Expand Down
169 changes: 169 additions & 0 deletions x/precisebank/keeper/burn.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 38230d3

Please sign in to comment.