package keeper import ( "fmt" errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/kava-labs/kava/x/earn/types" ) // Withdraw removes the amount of supplied tokens from a vault and transfers it // back to the account. func (k *Keeper) Withdraw( ctx sdk.Context, from sdk.AccAddress, wantAmount sdk.Coin, withdrawStrategy types.StrategyType, ) (sdk.Coin, error) { // Get AllowedVault, if not found (not a valid vault), return error allowedVault, found := k.GetAllowedVault(ctx, wantAmount.Denom) if !found { return sdk.Coin{}, types.ErrInvalidVaultDenom } if wantAmount.IsZero() { return sdk.Coin{}, types.ErrInsufficientAmount } // Check if withdraw strategy is supported by vault if !allowedVault.IsStrategyAllowed(withdrawStrategy) { return sdk.Coin{}, types.ErrInvalidVaultStrategy } // Check if VaultRecord exists vaultRecord, found := k.GetVaultRecord(ctx, wantAmount.Denom) if !found { return sdk.Coin{}, types.ErrVaultRecordNotFound } // Get account share record for the vault vaultShareRecord, found := k.GetVaultShareRecord(ctx, from) if !found { return sdk.Coin{}, types.ErrVaultShareRecordNotFound } withdrawShares, err := k.ConvertToShares(ctx, wantAmount) if err != nil { return sdk.Coin{}, fmt.Errorf("failed to convert assets to shares: %w", err) } accCurrentShares := vaultShareRecord.Shares.AmountOf(wantAmount.Denom) // Check if account is not withdrawing more shares than they have if accCurrentShares.LT(withdrawShares.Amount) { return sdk.Coin{}, errorsmod.Wrapf( types.ErrInsufficientValue, "account has less %s vault shares than withdraw shares, %s < %s", wantAmount.Denom, accCurrentShares, withdrawShares.Amount, ) } // Convert shares to amount to get truncated true share value withdrawAmount, err := k.ConvertToAssets(ctx, withdrawShares) if err != nil { return sdk.Coin{}, fmt.Errorf("failed to convert shares to assets: %w", err) } accountValue, err := k.GetVaultAccountValue(ctx, wantAmount.Denom, from) if err != nil { return sdk.Coin{}, fmt.Errorf("failed to get account value: %w", err) } // Check if withdrawAmount > account value if withdrawAmount.Amount.GT(accountValue.Amount) { return sdk.Coin{}, errorsmod.Wrapf( types.ErrInsufficientValue, "account has less %s vault value than withdraw amount, %s < %s", withdrawAmount.Denom, accountValue.Amount, withdrawAmount.Amount, ) } // Get the strategy for the vault strategy, err := k.GetStrategy(allowedVault.Strategies[0]) if err != nil { return sdk.Coin{}, err } // Not necessary to check if amount denom is allowed for the strategy, as // there would be no vault record if it weren't allowed. // Withdraw the withdrawAmount from the strategy if err := strategy.Withdraw(ctx, withdrawAmount); err != nil { return sdk.Coin{}, fmt.Errorf("failed to withdraw from strategy: %w", err) } // Send coins back to account, must withdraw from strategy first or the // module account may not have any funds to send. if err := k.bankKeeper.SendCoinsFromModuleToAccount( ctx, types.ModuleName, from, sdk.NewCoins(withdrawAmount), ); err != nil { return sdk.Coin{}, err } // Check if new account balance of shares results in account share value // of < 1 of a sdk.Coin. This share value is not able to be withdrawn and // should just be removed. isDust, err := k.ShareIsDust( ctx, vaultShareRecord.Shares.GetShare(withdrawAmount.Denom).Sub(withdrawShares), ) if err != nil { return sdk.Coin{}, err } if isDust { // Modify withdrawShares to subtract entire share balance for denom // This does not modify the actual withdraw coin amount as the // difference is < 1coin. withdrawShares = vaultShareRecord.Shares.GetShare(withdrawAmount.Denom) } // Call hook before record is modified with the user's current shares k.BeforeVaultDepositModified(ctx, wantAmount.Denom, from, accCurrentShares) // Decrement VaultRecord and VaultShareRecord supplies - must delete same // amounts vaultShareRecord.Shares = vaultShareRecord.Shares.Sub(withdrawShares) vaultRecord.TotalShares = vaultRecord.TotalShares.Sub(withdrawShares) // Update VaultRecord and VaultShareRecord, deletes if zero supply k.UpdateVaultRecord(ctx, vaultRecord) k.UpdateVaultShareRecord(ctx, vaultShareRecord) ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeVaultWithdraw, sdk.NewAttribute(types.AttributeKeyVaultDenom, withdrawAmount.Denom), sdk.NewAttribute(types.AttributeKeyOwner, from.String()), sdk.NewAttribute(types.AttributeKeyShares, withdrawShares.Amount.String()), sdk.NewAttribute(sdk.AttributeKeyAmount, withdrawAmount.Amount.String()), ), ) return withdrawAmount, nil } // WithdrawFromModuleAccount removes the amount of supplied tokens from a vault and transfers it // back to the module account. The module account must be unblocked from receiving transfers. func (k *Keeper) WithdrawFromModuleAccount( ctx sdk.Context, from string, wantAmount sdk.Coin, withdrawStrategy types.StrategyType, ) (sdk.Coin, error) { // Ensure the module account exists to prevent SendCoins from creating a new non-module account. acc := k.accountKeeper.GetModuleAccount(ctx, from) if acc == nil { return sdk.Coin{}, fmt.Errorf("module account not found: %s", from) } return k.Withdraw(ctx, acc.GetAddress(), wantAmount, withdrawStrategy) }