339 lines
11 KiB
Go
339 lines
11 KiB
Go
package keeper
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"cosmossdk.io/collections"
|
|
"cosmossdk.io/core/event"
|
|
"cosmossdk.io/errors"
|
|
sdkmath "cosmossdk.io/math"
|
|
"cosmossdk.io/x/gov/types"
|
|
v1 "cosmossdk.io/x/gov/types/v1"
|
|
pooltypes "cosmossdk.io/x/protocolpool/types"
|
|
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
|
)
|
|
|
|
// SetDeposit sets a Deposit to the gov store
|
|
func (k Keeper) SetDeposit(ctx context.Context, deposit v1.Deposit) error {
|
|
depositor, err := k.authKeeper.AddressCodec().StringToBytes(deposit.Depositor)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return k.Deposits.Set(ctx, collections.Join(deposit.ProposalId, sdk.AccAddress(depositor)), deposit)
|
|
}
|
|
|
|
// GetDeposits returns all the deposits of a proposal
|
|
func (k Keeper) GetDeposits(ctx context.Context, proposalID uint64) (deposits v1.Deposits, err error) {
|
|
err = k.IterateDeposits(ctx, proposalID, func(_ collections.Pair[uint64, sdk.AccAddress], deposit v1.Deposit) (bool, error) {
|
|
deposits = append(deposits, &deposit)
|
|
return false, nil
|
|
})
|
|
return deposits, err
|
|
}
|
|
|
|
// DeleteAndBurnDeposits deletes and burns all the deposits on a specific proposal.
|
|
func (k Keeper) DeleteAndBurnDeposits(ctx context.Context, proposalID uint64) error {
|
|
coinsToBurn := sdk.NewCoins()
|
|
err := k.IterateDeposits(ctx, proposalID, func(key collections.Pair[uint64, sdk.AccAddress], deposit v1.Deposit) (stop bool, err error) {
|
|
coinsToBurn = coinsToBurn.Add(deposit.Amount...)
|
|
return false, k.Deposits.Remove(ctx, key)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return k.bankKeeper.BurnCoins(ctx, k.authKeeper.GetModuleAddress(types.ModuleName), coinsToBurn)
|
|
}
|
|
|
|
// RefundAndDeleteDeposits refunds and deletes all the deposits on a specific proposal.
|
|
func (k Keeper) RefundAndDeleteDeposits(ctx context.Context, proposalID uint64) error {
|
|
return k.IterateDeposits(ctx, proposalID, func(key collections.Pair[uint64, sdk.AccAddress], deposit v1.Deposit) (bool, error) {
|
|
depositor := key.K2()
|
|
err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, depositor, deposit.Amount)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
err = k.Deposits.Remove(ctx, key)
|
|
return false, err
|
|
})
|
|
}
|
|
|
|
// IterateDeposits iterates over all the proposals deposits and performs a callback function
|
|
func (k Keeper) IterateDeposits(ctx context.Context, proposalID uint64, cb func(key collections.Pair[uint64, sdk.AccAddress], value v1.Deposit) (bool, error)) error {
|
|
rng := collections.NewPrefixedPairRange[uint64, sdk.AccAddress](proposalID)
|
|
if err := k.Deposits.Walk(ctx, rng, cb); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddDeposit adds or updates a deposit of a specific depositor on a specific proposal.
|
|
// Activates voting period when appropriate and returns true in that case, else returns false.
|
|
func (k Keeper) AddDeposit(ctx context.Context, proposalID uint64, depositorAddr sdk.AccAddress, depositAmount sdk.Coins) (bool, error) {
|
|
// Checks to see if proposal exists
|
|
proposal, err := k.Proposals.Get(ctx, proposalID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Check if proposal is still depositable
|
|
if (proposal.Status != v1.StatusDepositPeriod) && (proposal.Status != v1.StatusVotingPeriod) {
|
|
return false, errors.Wrapf(types.ErrInactiveProposal, "%d", proposalID)
|
|
}
|
|
|
|
// Check coins to be deposited match the proposal's deposit params
|
|
params, err := k.Params.Get(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
minDepositAmount := proposal.GetMinDepositFromParams(params)
|
|
minDepositRatio, err := sdkmath.LegacyNewDecFromStr(params.GetMinDepositRatio())
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// the deposit must only contain valid denoms (listed in the min deposit param)
|
|
if err := k.validateDepositDenom(params, depositAmount); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// If minDepositRatio is set, the deposit must be equal or greater than minDepositAmount*minDepositRatio
|
|
// for at least one denom. If minDepositRatio is zero we skip this check.
|
|
if !minDepositRatio.IsZero() {
|
|
var (
|
|
depositThresholdMet bool
|
|
thresholds []string
|
|
)
|
|
for _, minDep := range minDepositAmount {
|
|
// calculate the threshold for this denom, and hold a list to later return a useful error message
|
|
threshold := sdk.NewCoin(minDep.GetDenom(), minDep.Amount.ToLegacyDec().Mul(minDepositRatio).TruncateInt())
|
|
thresholds = append(thresholds, threshold.String())
|
|
|
|
found, deposit := depositAmount.Find(minDep.Denom)
|
|
if !found { // if not found, continue, as we know the deposit contains at least 1 valid denom
|
|
continue
|
|
}
|
|
|
|
// Once we know at least one threshold has been met, we can break. The deposit
|
|
// might contain other denoms but we don't care.
|
|
if deposit.IsGTE(threshold) {
|
|
depositThresholdMet = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// the threshold must be met with at least one denom, if not, return the list of minimum deposits
|
|
if !depositThresholdMet {
|
|
return false, errors.Wrapf(types.ErrMinDepositTooSmall, "received %s but need at least one of the following: %s", depositAmount, strings.Join(thresholds, ","))
|
|
}
|
|
}
|
|
|
|
// update the governance module's account coins pool
|
|
err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, depositorAddr, types.ModuleName, depositAmount)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Update proposal
|
|
proposal.TotalDeposit = sdk.NewCoins(proposal.TotalDeposit...).Add(depositAmount...)
|
|
if err = k.Proposals.Set(ctx, proposal.Id, proposal); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Check if deposit has provided sufficient total funds to transition the proposal into the voting period
|
|
activatedVotingPeriod := false
|
|
if proposal.Status == v1.StatusDepositPeriod && sdk.NewCoins(proposal.TotalDeposit...).IsAllGTE(minDepositAmount) {
|
|
err = k.ActivateVotingPeriod(ctx, proposal)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
activatedVotingPeriod = true
|
|
}
|
|
|
|
addr, err := k.authKeeper.AddressCodec().BytesToString(depositorAddr)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Add or update deposit object
|
|
deposit, err := k.Deposits.Get(ctx, collections.Join(proposalID, depositorAddr))
|
|
switch {
|
|
case err == nil:
|
|
// deposit exists
|
|
deposit.Amount = sdk.NewCoins(deposit.Amount...).Add(depositAmount...)
|
|
case errors.IsOf(err, collections.ErrNotFound):
|
|
// deposit doesn't exist
|
|
deposit = v1.NewDeposit(proposalID, addr, depositAmount)
|
|
default:
|
|
// failed to get deposit
|
|
return false, err
|
|
}
|
|
|
|
// called when deposit has been added to a proposal, however the proposal may not be active
|
|
err = k.Hooks().AfterProposalDeposit(ctx, proposalID, depositorAddr)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if err := k.EventService.EventManager(ctx).EmitKV(
|
|
types.EventTypeProposalDeposit,
|
|
event.NewAttribute(types.AttributeKeyDepositor, addr),
|
|
event.NewAttribute(sdk.AttributeKeyAmount, depositAmount.String()),
|
|
event.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposalID)),
|
|
); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
err = k.SetDeposit(ctx, deposit)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return activatedVotingPeriod, nil
|
|
}
|
|
|
|
// ChargeDeposit will charge proposal cancellation fee (deposits * proposal_cancel_burn_rate) and
|
|
// send to a destAddress if defined or burn otherwise.
|
|
// Remaining funds are send back to the depositor.
|
|
func (k Keeper) ChargeDeposit(ctx context.Context, proposalID uint64, destAddress, proposalCancelRate string) error {
|
|
rate := sdkmath.LegacyMustNewDecFromStr(proposalCancelRate)
|
|
var cancellationCharges sdk.Coins
|
|
|
|
deposits, err := k.GetDeposits(ctx, proposalID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, deposit := range deposits {
|
|
depositorAddress, err := k.authKeeper.AddressCodec().StringToBytes(deposit.Depositor)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var remainingAmount sdk.Coins
|
|
|
|
for _, coin := range deposit.Amount {
|
|
burnAmount := sdkmath.LegacyNewDecFromInt(coin.Amount).Mul(rate).TruncateInt()
|
|
// remaining amount = deposits amount - burn amount
|
|
remainingAmount = remainingAmount.Add(
|
|
sdk.NewCoin(
|
|
coin.Denom,
|
|
coin.Amount.Sub(burnAmount),
|
|
),
|
|
)
|
|
cancellationCharges = cancellationCharges.Add(
|
|
sdk.NewCoin(
|
|
coin.Denom,
|
|
burnAmount,
|
|
),
|
|
)
|
|
}
|
|
|
|
if !remainingAmount.IsZero() {
|
|
err := k.bankKeeper.SendCoinsFromModuleToAccount(
|
|
ctx, types.ModuleName, depositorAddress, remainingAmount,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
err = k.Deposits.Remove(ctx, collections.Join(deposit.ProposalId, sdk.AccAddress(depositorAddress)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// burn the cancellation fee or send the cancellation charges to destination address.
|
|
if !cancellationCharges.IsZero() {
|
|
// get the pool module account address
|
|
poolAddress, err := k.authKeeper.AddressCodec().BytesToString(k.authKeeper.GetModuleAddress(pooltypes.ModuleName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch {
|
|
case destAddress == "":
|
|
// burn the cancellation charges from deposits
|
|
err := k.bankKeeper.BurnCoins(ctx, k.authKeeper.GetModuleAddress(types.ModuleName), cancellationCharges)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case poolAddress == destAddress:
|
|
err := k.poolKeeper.FundCommunityPool(ctx, cancellationCharges, k.ModuleAccountAddress())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
destAccAddress, err := k.authKeeper.AddressCodec().StringToBytes(destAddress)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = k.bankKeeper.SendCoinsFromModuleToAccount(
|
|
ctx, types.ModuleName, destAccAddress, cancellationCharges,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateInitialDeposit validates if initial deposit is greater than or equal to the minimum
|
|
// required at the time of proposal submission. This threshold amount is determined by
|
|
// the deposit parameters. Returns nil on success, error otherwise.
|
|
func (k Keeper) validateInitialDeposit(params v1.Params, initialDeposit sdk.Coins, proposalType v1.ProposalType) error {
|
|
if !initialDeposit.IsValid() || initialDeposit.IsAnyNegative() {
|
|
return errors.Wrap(sdkerrors.ErrInvalidCoins, initialDeposit.String())
|
|
}
|
|
|
|
minInitialDepositRatio, err := sdkmath.LegacyNewDecFromStr(params.MinInitialDepositRatio)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if minInitialDepositRatio.IsZero() {
|
|
return nil
|
|
}
|
|
|
|
var minDepositCoins sdk.Coins
|
|
switch proposalType {
|
|
case v1.ProposalType_PROPOSAL_TYPE_EXPEDITED:
|
|
minDepositCoins = params.ExpeditedMinDeposit
|
|
default:
|
|
minDepositCoins = params.MinDeposit
|
|
}
|
|
|
|
for i := range minDepositCoins {
|
|
minDepositCoins[i].Amount = sdkmath.LegacyNewDecFromInt(minDepositCoins[i].Amount).Mul(minInitialDepositRatio).RoundInt()
|
|
}
|
|
if !initialDeposit.IsAllGTE(minDepositCoins) {
|
|
return errors.Wrapf(types.ErrMinDepositTooSmall, "was (%s), need (%s)", initialDeposit, minDepositCoins)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateDepositDenom validates if the deposit denom is accepted by the governance module.
|
|
func (k Keeper) validateDepositDenom(params v1.Params, depositAmount sdk.Coins) error {
|
|
denoms := make([]string, 0, len(params.MinDeposit))
|
|
acceptedDenoms := make(map[string]bool, len(params.MinDeposit))
|
|
for _, coin := range params.MinDeposit {
|
|
acceptedDenoms[coin.Denom] = true
|
|
denoms = append(denoms, coin.Denom)
|
|
}
|
|
|
|
for _, coin := range depositAmount {
|
|
if _, ok := acceptedDenoms[coin.Denom]; !ok {
|
|
return errors.Wrapf(types.ErrInvalidDepositDenom, "deposited %s, but gov accepts only the following denom(s): %v", depositAmount, denoms)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|