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

     1  package gov
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"time"
     7  
     8  	"cosmossdk.io/collections"
     9  	"cosmossdk.io/log"
    10  
    11  	"github.com/cosmos/cosmos-sdk/baseapp"
    12  	"github.com/cosmos/cosmos-sdk/telemetry"
    13  	sdk "github.com/cosmos/cosmos-sdk/types"
    14  	"github.com/cosmos/cosmos-sdk/x/gov/keeper"
    15  	"github.com/cosmos/cosmos-sdk/x/gov/types"
    16  	v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
    17  )
    18  
    19  // EndBlocker called every block, process inflation, update validator set.
    20  func EndBlocker(ctx sdk.Context, keeper *keeper.Keeper) error {
    21  	defer telemetry.ModuleMeasureSince(types.ModuleName, telemetry.Now(), telemetry.MetricKeyEndBlocker)
    22  
    23  	logger := ctx.Logger().With("module", "x/"+types.ModuleName)
    24  	// delete dead proposals from store and returns theirs deposits.
    25  	// A proposal is dead when it's inactive and didn't get enough deposit on time to get into voting phase.
    26  	rng := collections.NewPrefixUntilPairRange[time.Time, uint64](ctx.BlockTime())
    27  	err := keeper.InactiveProposalsQueue.Walk(ctx, rng, func(key collections.Pair[time.Time, uint64], _ uint64) (bool, error) {
    28  		proposal, err := keeper.Proposals.Get(ctx, key.K2())
    29  		if err != nil {
    30  			// if the proposal has an encoding error, this means it cannot be processed by x/gov
    31  			// this could be due to some types missing their registration
    32  			// instead of returning an error (i.e, halting the chain), we fail the proposal
    33  			if errors.Is(err, collections.ErrEncoding) {
    34  				proposal.Id = key.K2()
    35  				if err := failUnsupportedProposal(logger, ctx, keeper, proposal, err.Error(), false); err != nil {
    36  					return false, err
    37  				}
    38  
    39  				if err = keeper.DeleteProposal(ctx, proposal.Id); err != nil {
    40  					return false, err
    41  				}
    42  
    43  				return false, nil
    44  			}
    45  
    46  			return false, err
    47  		}
    48  
    49  		if err = keeper.DeleteProposal(ctx, proposal.Id); err != nil {
    50  			return false, err
    51  		}
    52  
    53  		params, err := keeper.Params.Get(ctx)
    54  		if err != nil {
    55  			return false, err
    56  		}
    57  		if !params.BurnProposalDepositPrevote {
    58  			err = keeper.RefundAndDeleteDeposits(ctx, proposal.Id) // refund deposit if proposal got removed without getting 100% of the proposal
    59  		} else {
    60  			err = keeper.DeleteAndBurnDeposits(ctx, proposal.Id) // burn the deposit if proposal got removed without getting 100% of the proposal
    61  		}
    62  
    63  		if err != nil {
    64  			return false, err
    65  		}
    66  
    67  		// called when proposal become inactive
    68  		cacheCtx, writeCache := ctx.CacheContext()
    69  		err = keeper.Hooks().AfterProposalFailedMinDeposit(cacheCtx, proposal.Id)
    70  		if err == nil { // purposely ignoring the error here not to halt the chain if the hook fails
    71  			writeCache()
    72  		} else {
    73  			keeper.Logger(ctx).Error("failed to execute AfterProposalFailedMinDeposit hook", "error", err)
    74  		}
    75  
    76  		ctx.EventManager().EmitEvent(
    77  			sdk.NewEvent(
    78  				types.EventTypeInactiveProposal,
    79  				sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.Id)),
    80  				sdk.NewAttribute(types.AttributeKeyProposalResult, types.AttributeValueProposalDropped),
    81  			),
    82  		)
    83  
    84  		logger.Info(
    85  			"proposal did not meet minimum deposit; deleted",
    86  			"proposal", proposal.Id,
    87  			"expedited", proposal.Expedited,
    88  			"title", proposal.Title,
    89  			"min_deposit", sdk.NewCoins(proposal.GetMinDepositFromParams(params)...).String(),
    90  			"total_deposit", sdk.NewCoins(proposal.TotalDeposit...).String(),
    91  		)
    92  
    93  		return false, nil
    94  	})
    95  	if err != nil {
    96  		return err
    97  	}
    98  
    99  	// fetch active proposals whose voting periods have ended (are passed the block time)
   100  	rng = collections.NewPrefixUntilPairRange[time.Time, uint64](ctx.BlockTime())
   101  	err = keeper.ActiveProposalsQueue.Walk(ctx, rng, func(key collections.Pair[time.Time, uint64], _ uint64) (bool, error) {
   102  		proposal, err := keeper.Proposals.Get(ctx, key.K2())
   103  		if err != nil {
   104  			// if the proposal has an encoding error, this means it cannot be processed by x/gov
   105  			// this could be due to some types missing their registration
   106  			// instead of returning an error (i.e, halting the chain), we fail the proposal
   107  			if errors.Is(err, collections.ErrEncoding) {
   108  				proposal.Id = key.K2()
   109  				if err := failUnsupportedProposal(logger, ctx, keeper, proposal, err.Error(), true); err != nil {
   110  					return false, err
   111  				}
   112  
   113  				if err = keeper.ActiveProposalsQueue.Remove(ctx, collections.Join(*proposal.VotingEndTime, proposal.Id)); err != nil {
   114  					return false, err
   115  				}
   116  
   117  				return false, nil
   118  			}
   119  
   120  			return false, err
   121  		}
   122  
   123  		var tagValue, logMsg string
   124  
   125  		passes, burnDeposits, tallyResults, err := keeper.Tally(ctx, proposal)
   126  		if err != nil {
   127  			return false, err
   128  		}
   129  
   130  		// If an expedited proposal fails, we do not want to update
   131  		// the deposit at this point since the proposal is converted to regular.
   132  		// As a result, the deposits are either deleted or refunded in all cases
   133  		// EXCEPT when an expedited proposal fails.
   134  		if !(proposal.Expedited && !passes) {
   135  			if burnDeposits {
   136  				err = keeper.DeleteAndBurnDeposits(ctx, proposal.Id)
   137  			} else {
   138  				err = keeper.RefundAndDeleteDeposits(ctx, proposal.Id)
   139  			}
   140  			if err != nil {
   141  				return false, err
   142  			}
   143  		}
   144  
   145  		if err = keeper.ActiveProposalsQueue.Remove(ctx, collections.Join(*proposal.VotingEndTime, proposal.Id)); err != nil {
   146  			return false, err
   147  		}
   148  
   149  		switch {
   150  		case passes:
   151  			var (
   152  				idx    int
   153  				events sdk.Events
   154  				msg    sdk.Msg
   155  			)
   156  
   157  			// attempt to execute all messages within the passed proposal
   158  			// Messages may mutate state thus we use a cached context. If one of
   159  			// the handlers fails, no state mutation is written and the error
   160  			// message is logged.
   161  			cacheCtx, writeCache := ctx.CacheContext()
   162  			messages, err := proposal.GetMsgs()
   163  			if err != nil {
   164  				proposal.Status = v1.StatusFailed
   165  				proposal.FailedReason = err.Error()
   166  				tagValue = types.AttributeValueProposalFailed
   167  				logMsg = fmt.Sprintf("passed proposal (%v) failed to execute; msgs: %s", proposal, err)
   168  
   169  				break
   170  			}
   171  
   172  			// execute all messages
   173  			for idx, msg = range messages {
   174  				handler := keeper.Router().Handler(msg)
   175  				var res *sdk.Result
   176  				res, err = safeExecuteHandler(cacheCtx, msg, handler)
   177  				if err != nil {
   178  					break
   179  				}
   180  
   181  				events = append(events, res.GetEvents()...)
   182  			}
   183  
   184  			// `err == nil` when all handlers passed.
   185  			// Or else, `idx` and `err` are populated with the msg index and error.
   186  			if err == nil {
   187  				proposal.Status = v1.StatusPassed
   188  				tagValue = types.AttributeValueProposalPassed
   189  				logMsg = "passed"
   190  
   191  				// write state to the underlying multi-store
   192  				writeCache()
   193  
   194  				// propagate the msg events to the current context
   195  				ctx.EventManager().EmitEvents(events)
   196  			} else {
   197  				proposal.Status = v1.StatusFailed
   198  				proposal.FailedReason = err.Error()
   199  				tagValue = types.AttributeValueProposalFailed
   200  				logMsg = fmt.Sprintf("passed, but msg %d (%s) failed on execution: %s", idx, sdk.MsgTypeURL(msg), err)
   201  			}
   202  		case proposal.Expedited:
   203  			// When expedited proposal fails, it is converted
   204  			// to a regular proposal. As a result, the voting period is extended, and,
   205  			// once the regular voting period expires again, the tally is repeated
   206  			// according to the regular proposal rules.
   207  			proposal.Expedited = false
   208  			params, err := keeper.Params.Get(ctx)
   209  			if err != nil {
   210  				return false, err
   211  			}
   212  			endTime := proposal.VotingStartTime.Add(*params.VotingPeriod)
   213  			proposal.VotingEndTime = &endTime
   214  
   215  			err = keeper.ActiveProposalsQueue.Set(ctx, collections.Join(*proposal.VotingEndTime, proposal.Id), proposal.Id)
   216  			if err != nil {
   217  				return false, err
   218  			}
   219  
   220  			tagValue = types.AttributeValueExpeditedProposalRejected
   221  			logMsg = "expedited proposal converted to regular"
   222  		default:
   223  			proposal.Status = v1.StatusRejected
   224  			proposal.FailedReason = "proposal did not get enough votes to pass"
   225  			tagValue = types.AttributeValueProposalRejected
   226  			logMsg = "rejected"
   227  		}
   228  
   229  		proposal.FinalTallyResult = &tallyResults
   230  
   231  		err = keeper.SetProposal(ctx, proposal)
   232  		if err != nil {
   233  			return false, err
   234  		}
   235  
   236  		// when proposal become active
   237  		cacheCtx, writeCache := ctx.CacheContext()
   238  		err = keeper.Hooks().AfterProposalVotingPeriodEnded(cacheCtx, proposal.Id)
   239  		if err == nil { // purposely ignoring the error here not to halt the chain if the hook fails
   240  			writeCache()
   241  		} else {
   242  			keeper.Logger(ctx).Error("failed to execute AfterProposalVotingPeriodEnded hook", "error", err)
   243  		}
   244  
   245  		logger.Info(
   246  			"proposal tallied",
   247  			"proposal", proposal.Id,
   248  			"status", proposal.Status.String(),
   249  			"expedited", proposal.Expedited,
   250  			"title", proposal.Title,
   251  			"results", logMsg,
   252  		)
   253  
   254  		ctx.EventManager().EmitEvent(
   255  			sdk.NewEvent(
   256  				types.EventTypeActiveProposal,
   257  				sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.Id)),
   258  				sdk.NewAttribute(types.AttributeKeyProposalResult, tagValue),
   259  				sdk.NewAttribute(types.AttributeKeyProposalLog, logMsg),
   260  			),
   261  		)
   262  
   263  		return false, nil
   264  	})
   265  	if err != nil {
   266  		return err
   267  	}
   268  	return nil
   269  }
   270  
   271  // executes handle(msg) and recovers from panic.
   272  func safeExecuteHandler(ctx sdk.Context, msg sdk.Msg, handler baseapp.MsgServiceHandler,
   273  ) (res *sdk.Result, err error) {
   274  	defer func() {
   275  		if r := recover(); r != nil {
   276  			err = fmt.Errorf("handling x/gov proposal msg [%s] PANICKED: %v", msg, r)
   277  		}
   278  	}()
   279  	res, err = handler(ctx, msg)
   280  	return
   281  }
   282  
   283  // failUnsupportedProposal fails a proposal that cannot be processed by gov
   284  func failUnsupportedProposal(
   285  	logger log.Logger,
   286  	ctx sdk.Context,
   287  	keeper *keeper.Keeper,
   288  	proposal v1.Proposal,
   289  	errMsg string,
   290  	active bool,
   291  ) error {
   292  	proposal.Status = v1.StatusFailed
   293  	proposal.FailedReason = fmt.Sprintf("proposal failed because it cannot be processed by gov: %s", errMsg)
   294  	proposal.Messages = nil // clear out the messages
   295  
   296  	if err := keeper.SetProposal(ctx, proposal); err != nil {
   297  		return err
   298  	}
   299  
   300  	if err := keeper.RefundAndDeleteDeposits(ctx, proposal.Id); err != nil {
   301  		return err
   302  	}
   303  
   304  	eventType := types.EventTypeInactiveProposal
   305  	if active {
   306  		eventType = types.EventTypeActiveProposal
   307  	}
   308  
   309  	ctx.EventManager().EmitEvent(
   310  		sdk.NewEvent(
   311  			eventType,
   312  			sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.Id)),
   313  			sdk.NewAttribute(types.AttributeKeyProposalResult, types.AttributeValueProposalFailed),
   314  		),
   315  	)
   316  
   317  	logger.Info(
   318  		"proposal failed to decode; deleted",
   319  		"proposal", proposal.Id,
   320  		"expedited", proposal.Expedited,
   321  		"title", proposal.Title,
   322  		"results", errMsg,
   323  	)
   324  
   325  	return nil
   326  }