-
Notifications
You must be signed in to change notification settings - Fork 332
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(x/precisebank): Implement BurnCoins (#1934)
Implement & test BurnCoins method
- Loading branch information
Showing
7 changed files
with
866 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.