github.com/cosmos/cosmos-sdk@v0.50.10/x/gov/keeper/deposit.go (about)

     1  package keeper
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"cosmossdk.io/collections"
     9  	"cosmossdk.io/errors"
    10  	sdkmath "cosmossdk.io/math"
    11  
    12  	sdk "github.com/cosmos/cosmos-sdk/types"
    13  	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
    14  	disttypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
    15  	"github.com/cosmos/cosmos-sdk/x/gov/types"
    16  	v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
    17  )
    18  
    19  // SetDeposit sets a Deposit to the gov store
    20  func (keeper Keeper) SetDeposit(ctx context.Context, deposit v1.Deposit) error {
    21  	depositor, err := keeper.authKeeper.AddressCodec().StringToBytes(deposit.Depositor)
    22  	if err != nil {
    23  		return err
    24  	}
    25  	return keeper.Deposits.Set(ctx, collections.Join(deposit.ProposalId, sdk.AccAddress(depositor)), deposit)
    26  }
    27  
    28  // GetDeposits returns all the deposits of a proposal
    29  func (keeper Keeper) GetDeposits(ctx context.Context, proposalID uint64) (deposits v1.Deposits, err error) {
    30  	err = keeper.IterateDeposits(ctx, proposalID, func(_ collections.Pair[uint64, sdk.AccAddress], deposit v1.Deposit) (bool, error) {
    31  		deposits = append(deposits, &deposit)
    32  		return false, nil
    33  	})
    34  	return deposits, err
    35  }
    36  
    37  // DeleteAndBurnDeposits deletes and burns all the deposits on a specific proposal.
    38  func (keeper Keeper) DeleteAndBurnDeposits(ctx context.Context, proposalID uint64) error {
    39  	coinsToBurn := sdk.NewCoins()
    40  	err := keeper.IterateDeposits(ctx, proposalID, func(key collections.Pair[uint64, sdk.AccAddress], deposit v1.Deposit) (stop bool, err error) {
    41  		coinsToBurn = coinsToBurn.Add(deposit.Amount...)
    42  		return false, keeper.Deposits.Remove(ctx, key)
    43  	})
    44  	if err != nil {
    45  		return err
    46  	}
    47  
    48  	return keeper.bankKeeper.BurnCoins(ctx, types.ModuleName, coinsToBurn)
    49  }
    50  
    51  // IterateDeposits iterates over all the proposals deposits and performs a callback function
    52  func (keeper Keeper) IterateDeposits(ctx context.Context, proposalID uint64, cb func(key collections.Pair[uint64, sdk.AccAddress], value v1.Deposit) (bool, error)) error {
    53  	rng := collections.NewPrefixedPairRange[uint64, sdk.AccAddress](proposalID)
    54  	err := keeper.Deposits.Walk(ctx, rng, cb)
    55  	if err != nil {
    56  		return err
    57  	}
    58  	return nil
    59  }
    60  
    61  // AddDeposit adds or updates a deposit of a specific depositor on a specific proposal.
    62  // Activates voting period when appropriate and returns true in that case, else returns false.
    63  func (keeper Keeper) AddDeposit(ctx context.Context, proposalID uint64, depositorAddr sdk.AccAddress, depositAmount sdk.Coins) (bool, error) {
    64  	// Checks to see if proposal exists
    65  	proposal, err := keeper.Proposals.Get(ctx, proposalID)
    66  	if err != nil {
    67  		return false, err
    68  	}
    69  
    70  	// Check if proposal is still depositable
    71  	if (proposal.Status != v1.StatusDepositPeriod) && (proposal.Status != v1.StatusVotingPeriod) {
    72  		return false, errors.Wrapf(types.ErrInactiveProposal, "%d", proposalID)
    73  	}
    74  
    75  	// Check coins to be deposited match the proposal's deposit params
    76  	params, err := keeper.Params.Get(ctx)
    77  	if err != nil {
    78  		return false, err
    79  	}
    80  
    81  	minDepositAmount := proposal.GetMinDepositFromParams(params)
    82  	minDepositRatio, err := sdkmath.LegacyNewDecFromStr(params.GetMinDepositRatio())
    83  	if err != nil {
    84  		return false, err
    85  	}
    86  
    87  	// the deposit must only contain valid denoms (listed in the min deposit param)
    88  	if err := keeper.validateDepositDenom(ctx, params, depositAmount); err != nil {
    89  		return false, err
    90  	}
    91  
    92  	// If minDepositRatio is set, the deposit must be equal or greater than minDepositAmount*minDepositRatio
    93  	// for at least one denom. If minDepositRatio is zero we skip this check.
    94  	if !minDepositRatio.IsZero() {
    95  		var (
    96  			depositThresholdMet bool
    97  			thresholds          []string
    98  		)
    99  		for _, minDep := range minDepositAmount {
   100  			// calculate the threshold for this denom, and hold a list to later return a useful error message
   101  			threshold := sdk.NewCoin(minDep.GetDenom(), minDep.Amount.ToLegacyDec().Mul(minDepositRatio).TruncateInt())
   102  			thresholds = append(thresholds, threshold.String())
   103  
   104  			found, deposit := depositAmount.Find(minDep.Denom)
   105  			if !found { // if not found, continue, as we know the deposit contains at least 1 valid denom
   106  				continue
   107  			}
   108  
   109  			// Once we know at least one threshold has been met, we can break. The deposit
   110  			// might contain other denoms but we don't care.
   111  			if deposit.IsGTE(threshold) {
   112  				depositThresholdMet = true
   113  				break
   114  			}
   115  		}
   116  
   117  		// the threshold must be met with at least one denom, if not, return the list of minimum deposits
   118  		if !depositThresholdMet {
   119  			return false, errors.Wrapf(types.ErrMinDepositTooSmall, "received %s but need at least one of the following: %s", depositAmount, strings.Join(thresholds, ","))
   120  		}
   121  	}
   122  
   123  	// update the governance module's account coins pool
   124  	err = keeper.bankKeeper.SendCoinsFromAccountToModule(ctx, depositorAddr, types.ModuleName, depositAmount)
   125  	if err != nil {
   126  		return false, err
   127  	}
   128  
   129  	// Update proposal
   130  	proposal.TotalDeposit = sdk.NewCoins(proposal.TotalDeposit...).Add(depositAmount...)
   131  	err = keeper.SetProposal(ctx, proposal)
   132  	if err != nil {
   133  		return false, err
   134  	}
   135  
   136  	// Check if deposit has provided sufficient total funds to transition the proposal into the voting period
   137  	activatedVotingPeriod := false
   138  	if proposal.Status == v1.StatusDepositPeriod && sdk.NewCoins(proposal.TotalDeposit...).IsAllGTE(minDepositAmount) {
   139  		err = keeper.ActivateVotingPeriod(ctx, proposal)
   140  		if err != nil {
   141  			return false, err
   142  		}
   143  
   144  		activatedVotingPeriod = true
   145  	}
   146  
   147  	// Add or update deposit object
   148  	deposit, err := keeper.Deposits.Get(ctx, collections.Join(proposalID, depositorAddr))
   149  	switch {
   150  	case err == nil:
   151  		// deposit exists
   152  		deposit.Amount = sdk.NewCoins(deposit.Amount...).Add(depositAmount...)
   153  	case errors.IsOf(err, collections.ErrNotFound):
   154  		// deposit doesn't exist
   155  		deposit = v1.NewDeposit(proposalID, depositorAddr, depositAmount)
   156  	default:
   157  		// failed to get deposit
   158  		return false, err
   159  	}
   160  
   161  	// called when deposit has been added to a proposal, however the proposal may not be active
   162  	err = keeper.Hooks().AfterProposalDeposit(ctx, proposalID, depositorAddr)
   163  	if err != nil {
   164  		return false, err
   165  	}
   166  
   167  	sdkCtx := sdk.UnwrapSDKContext(ctx)
   168  	sdkCtx.EventManager().EmitEvent(
   169  		sdk.NewEvent(
   170  			types.EventTypeProposalDeposit,
   171  			sdk.NewAttribute(types.AttributeKeyDepositor, depositorAddr.String()),
   172  			sdk.NewAttribute(sdk.AttributeKeyAmount, depositAmount.String()),
   173  			sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposalID)),
   174  		),
   175  	)
   176  
   177  	err = keeper.SetDeposit(ctx, deposit)
   178  	if err != nil {
   179  		return false, err
   180  	}
   181  
   182  	return activatedVotingPeriod, nil
   183  }
   184  
   185  // ChargeDeposit will charge proposal cancellation fee (deposits * proposal_cancel_burn_rate)  and
   186  // send to a destAddress if defined or burn otherwise.
   187  // Remaining funds are send back to the depositor.
   188  func (keeper Keeper) ChargeDeposit(ctx context.Context, proposalID uint64, destAddress, proposalCancelRate string) error {
   189  	rate := sdkmath.LegacyMustNewDecFromStr(proposalCancelRate)
   190  	var cancellationCharges sdk.Coins
   191  
   192  	deposits, err := keeper.GetDeposits(ctx, proposalID)
   193  	if err != nil {
   194  		return err
   195  	}
   196  
   197  	for _, deposit := range deposits {
   198  		depositerAddress, err := keeper.authKeeper.AddressCodec().StringToBytes(deposit.Depositor)
   199  		if err != nil {
   200  			return err
   201  		}
   202  
   203  		var remainingAmount sdk.Coins
   204  
   205  		for _, coin := range deposit.Amount {
   206  			burnAmount := sdkmath.LegacyNewDecFromInt(coin.Amount).Mul(rate).TruncateInt()
   207  			// remaining amount = deposits amount - burn amount
   208  			remainingAmount = remainingAmount.Add(
   209  				sdk.NewCoin(
   210  					coin.Denom,
   211  					coin.Amount.Sub(burnAmount),
   212  				),
   213  			)
   214  			cancellationCharges = cancellationCharges.Add(
   215  				sdk.NewCoin(
   216  					coin.Denom,
   217  					burnAmount,
   218  				),
   219  			)
   220  		}
   221  
   222  		if !remainingAmount.IsZero() {
   223  			err := keeper.bankKeeper.SendCoinsFromModuleToAccount(
   224  				ctx, types.ModuleName, depositerAddress, remainingAmount,
   225  			)
   226  			if err != nil {
   227  				return err
   228  			}
   229  		}
   230  		err = keeper.Deposits.Remove(ctx, collections.Join(deposit.ProposalId, sdk.AccAddress(depositerAddress)))
   231  		if err != nil {
   232  			return err
   233  		}
   234  	}
   235  
   236  	// burn the cancellation fee or sent the cancellation charges to destination address.
   237  	if !cancellationCharges.IsZero() {
   238  		// get the distribution module account address
   239  		distributionAddress := keeper.authKeeper.GetModuleAddress(disttypes.ModuleName)
   240  		switch {
   241  		case destAddress == "":
   242  			// burn the cancellation charges from deposits
   243  			err := keeper.bankKeeper.BurnCoins(ctx, types.ModuleName, cancellationCharges)
   244  			if err != nil {
   245  				return err
   246  			}
   247  		case distributionAddress.String() == destAddress:
   248  			err := keeper.distrKeeper.FundCommunityPool(ctx, cancellationCharges, keeper.ModuleAccountAddress())
   249  			if err != nil {
   250  				return err
   251  			}
   252  		default:
   253  			destAccAddress, err := keeper.authKeeper.AddressCodec().StringToBytes(destAddress)
   254  			if err != nil {
   255  				return err
   256  			}
   257  			err = keeper.bankKeeper.SendCoinsFromModuleToAccount(
   258  				ctx, types.ModuleName, destAccAddress, cancellationCharges,
   259  			)
   260  			if err != nil {
   261  				return err
   262  			}
   263  		}
   264  	}
   265  
   266  	return nil
   267  }
   268  
   269  // RefundAndDeleteDeposits refunds and deletes all the deposits on a specific proposal.
   270  func (keeper Keeper) RefundAndDeleteDeposits(ctx context.Context, proposalID uint64) error {
   271  	return keeper.IterateDeposits(ctx, proposalID, func(key collections.Pair[uint64, sdk.AccAddress], deposit v1.Deposit) (bool, error) {
   272  		depositor := key.K2()
   273  		err := keeper.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, depositor, deposit.Amount)
   274  		if err != nil {
   275  			return false, err
   276  		}
   277  		err = keeper.Deposits.Remove(ctx, key)
   278  		return false, err
   279  	})
   280  }
   281  
   282  // validateInitialDeposit validates if initial deposit is greater than or equal to the minimum
   283  // required at the time of proposal submission. This threshold amount is determined by
   284  // the deposit parameters. Returns nil on success, error otherwise.
   285  func (keeper Keeper) validateInitialDeposit(ctx context.Context, params v1.Params, initialDeposit sdk.Coins, expedited bool) error {
   286  	if !initialDeposit.IsValid() || initialDeposit.IsAnyNegative() {
   287  		return errors.Wrap(sdkerrors.ErrInvalidCoins, initialDeposit.String())
   288  	}
   289  
   290  	minInitialDepositRatio, err := sdkmath.LegacyNewDecFromStr(params.MinInitialDepositRatio)
   291  	if err != nil {
   292  		return err
   293  	}
   294  	if minInitialDepositRatio.IsZero() {
   295  		return nil
   296  	}
   297  
   298  	var minDepositCoins sdk.Coins
   299  	if expedited {
   300  		minDepositCoins = params.ExpeditedMinDeposit
   301  	} else {
   302  		minDepositCoins = params.MinDeposit
   303  	}
   304  
   305  	for i := range minDepositCoins {
   306  		minDepositCoins[i].Amount = sdkmath.LegacyNewDecFromInt(minDepositCoins[i].Amount).Mul(minInitialDepositRatio).RoundInt()
   307  	}
   308  	if !initialDeposit.IsAllGTE(minDepositCoins) {
   309  		return errors.Wrapf(types.ErrMinDepositTooSmall, "was (%s), need (%s)", initialDeposit, minDepositCoins)
   310  	}
   311  	return nil
   312  }
   313  
   314  // validateDepositDenom validates if the deposit denom is accepted by the governance module.
   315  func (keeper Keeper) validateDepositDenom(ctx context.Context, params v1.Params, depositAmount sdk.Coins) error {
   316  	denoms := []string{}
   317  	acceptedDenoms := make(map[string]bool, len(params.MinDeposit))
   318  	for _, coin := range params.MinDeposit {
   319  		acceptedDenoms[coin.Denom] = true
   320  		denoms = append(denoms, coin.Denom)
   321  	}
   322  
   323  	for _, coin := range depositAmount {
   324  		if _, ok := acceptedDenoms[coin.Denom]; !ok {
   325  			return errors.Wrapf(types.ErrInvalidDepositDenom, "deposited %s, but gov accepts only the following denom(s): %v", depositAmount, denoms)
   326  		}
   327  	}
   328  
   329  	return nil
   330  }