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 }