code.vegaprotocol.io/vega@v0.79.0/core/governance/engine.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package governance
    17  
    18  import (
    19  	"context"
    20  	"encoding/binary"
    21  	"encoding/hex"
    22  	"fmt"
    23  	"sort"
    24  	"strings"
    25  	"time"
    26  
    27  	"code.vegaprotocol.io/vega/core/assets"
    28  	"code.vegaprotocol.io/vega/core/events"
    29  	"code.vegaprotocol.io/vega/core/netparams"
    30  	"code.vegaprotocol.io/vega/core/types"
    31  	"code.vegaprotocol.io/vega/core/validators"
    32  	vgcrypto "code.vegaprotocol.io/vega/libs/crypto"
    33  	"code.vegaprotocol.io/vega/libs/num"
    34  	"code.vegaprotocol.io/vega/logging"
    35  
    36  	"github.com/pkg/errors"
    37  )
    38  
    39  var (
    40  	ErrProposalDoesNotExist                      = errors.New("proposal does not exist")
    41  	ErrMarketDoesNotExist                        = errors.New("market does not exist")
    42  	ErrMarketStateUpdateNotAllowed               = errors.New("market state does not allow for state update")
    43  	ErrMarketProposalStillOpen                   = errors.New("original market proposal is still open")
    44  	ErrProposalNotOpenForVotes                   = errors.New("proposal is not open for votes")
    45  	ErrProposalIsDuplicate                       = errors.New("proposal with given ID already exists")
    46  	ErrVoterInsufficientTokensAndEquityLikeShare = errors.New("vote requires tokens or equity-like share")
    47  	ErrVoterInsufficientTokens                   = errors.New("vote requires more tokens than the party has")
    48  	ErrUnsupportedProposalType                   = errors.New("unsupported proposal type")
    49  	ErrUnsupportedAssetSourceType                = errors.New("unsupported asset source type")
    50  	ErrExpectedERC20Asset                        = errors.New("expected an ERC20 asset but was not")
    51  	ErrErc20AddressAlreadyInUse                  = errors.New("erc20 address already in use")
    52  	ErrParentMarketDoesNotExist                  = errors.New("market to succeed does not exist")
    53  	ErrParentMarketAlreadySucceeded              = errors.New("the parent market was already succeeded by a prior proposal")
    54  	ErrParentMarketSucceededByCompeting          = errors.New("the parent market has been succeeded by a competing propsal")
    55  	ErrSettlementDataOutOfRange                  = errors.New("the settlement data is invalid")
    56  )
    57  
    58  //go:generate go run github.com/golang/mock/mockgen -destination mocks/mocks.go -package mocks code.vegaprotocol.io/vega/core/governance Markets,StakingAccounts,Assets,TimeService,Witness,NetParams,Banking
    59  
    60  // Broker - event bus.
    61  type Broker interface {
    62  	Send(e events.Event)
    63  	SendBatch(es []events.Event)
    64  }
    65  
    66  // Markets allows to get the market data for use in the market update proposal
    67  // computation.
    68  type Markets interface {
    69  	MarketExists(market string) bool
    70  	GetMarket(market string, settled bool) (types.Market, bool)
    71  	GetMarketState(market string) (types.MarketState, error)
    72  	GetEquityLikeShareForMarketAndParty(market, party string) (num.Decimal, bool)
    73  	RestoreMarket(ctx context.Context, marketConfig *types.Market) error
    74  	StartOpeningAuction(ctx context.Context, marketID string) error
    75  	UpdateMarket(ctx context.Context, marketConfig *types.Market) error
    76  	IsSucceeded(mktID string) bool
    77  	ValidateSettlementData(mktID string, data *num.Uint) bool
    78  	MarketHasActivePAP(market string) (bool, error)
    79  }
    80  
    81  // StakingAccounts ...
    82  type StakingAccounts interface {
    83  	GetAvailableBalance(party string) (*num.Uint, error)
    84  	GetStakingAssetTotalSupply() *num.Uint
    85  }
    86  
    87  type Assets interface {
    88  	NewAsset(ctx context.Context, ref string, assetDetails *types.AssetDetails) (string, error)
    89  	Get(assetID string) (*assets.Asset, error)
    90  	IsEnabled(string) bool
    91  	SetRejected(ctx context.Context, assetID string) error
    92  	SetPendingListing(ctx context.Context, assetID string) error
    93  	ValidateAsset(assetID string) error
    94  	ValidateEthereumAddress(address, chainID string) error
    95  }
    96  
    97  type Banking interface {
    98  	VerifyGovernanceTransfer(transfer *types.NewTransferConfiguration) error
    99  	VerifyCancelGovernanceTransfer(transferID string) error
   100  }
   101  
   102  // TimeService ...
   103  type TimeService interface {
   104  	GetTimeNow() time.Time
   105  }
   106  
   107  // Witness ...
   108  type Witness interface {
   109  	StartCheck(validators.Resource, func(interface{}, bool), time.Time) error
   110  	RestoreResource(validators.Resource, func(interface{}, bool)) error
   111  }
   112  
   113  type NetParams interface {
   114  	Validate(string, string) error
   115  	Update(context.Context, string, string) error
   116  	GetDecimal(string) (num.Decimal, error)
   117  	GetInt(string) (int64, error)
   118  	GetUint(string) (*num.Uint, error)
   119  	GetDuration(string) (time.Duration, error)
   120  	GetJSONStruct(string, netparams.Reset) error
   121  	Get(string) (string, error)
   122  }
   123  
   124  // Engine is the governance engine that handles proposal and vote lifecycle.
   125  type Engine struct {
   126  	Config
   127  	log *logging.Logger
   128  
   129  	nodeProposalValidation *NodeValidation
   130  	accs                   StakingAccounts
   131  	markets                Markets
   132  	timeService            TimeService
   133  	broker                 Broker
   134  	assets                 Assets
   135  	netp                   NetParams
   136  	banking                Banking
   137  
   138  	// we store proposals in slice
   139  	// not as easy to access them directly, but by doing this we can keep
   140  	// them in order of arrival, which makes their processing deterministic
   141  	activeBatchProposals map[string]*batchProposal
   142  	activeProposals      []*proposal
   143  	enactedProposals     []*proposal
   144  
   145  	// snapshot state
   146  	gss *governanceSnapshotState
   147  	// main chain ID
   148  	chainID uint64
   149  }
   150  
   151  func NewEngine(
   152  	log *logging.Logger,
   153  	cfg Config,
   154  	accs StakingAccounts,
   155  	tm TimeService,
   156  	broker Broker,
   157  	assets Assets,
   158  	witness Witness,
   159  	markets Markets,
   160  	netp NetParams,
   161  	banking Banking,
   162  ) *Engine {
   163  	log = log.Named(namedLogger)
   164  	log.SetLevel(cfg.Level.Level)
   165  
   166  	e := &Engine{
   167  		Config:                 cfg,
   168  		accs:                   accs,
   169  		log:                    log,
   170  		activeProposals:        []*proposal{},
   171  		enactedProposals:       []*proposal{},
   172  		activeBatchProposals:   map[string]*batchProposal{},
   173  		nodeProposalValidation: NewNodeValidation(log, assets, tm.GetTimeNow(), witness),
   174  		timeService:            tm,
   175  		broker:                 broker,
   176  		assets:                 assets,
   177  		markets:                markets,
   178  		netp:                   netp,
   179  		gss:                    &governanceSnapshotState{},
   180  		banking:                banking,
   181  	}
   182  	return e
   183  }
   184  
   185  func (e *Engine) Hash() []byte {
   186  	// get the node proposal hash first
   187  	npHash := e.nodeProposalValidation.Hash()
   188  
   189  	// Create the slice for this state
   190  	// 32 -> len(proposal.ID) = 32 bytes pubkey
   191  	// vote counts = 3*uint64
   192  	// 32 -> len of enactedProposal.ID
   193  	// len of the np hash
   194  	output := make(
   195  		[]byte,
   196  		len(e.activeProposals)*(32+8*3)+len(e.enactedProposals)*32+len(npHash),
   197  	)
   198  
   199  	var i int
   200  
   201  	for _, k := range e.activeProposals {
   202  		idbytes := []byte(k.ID)
   203  		copy(output[i:], idbytes[:])
   204  		i += 32
   205  		binary.BigEndian.PutUint64(output[i:], uint64(len(k.yes)))
   206  		i += 8
   207  		binary.BigEndian.PutUint64(output[i:], uint64(len(k.no)))
   208  		i += 8
   209  		binary.BigEndian.PutUint64(output[i:], uint64(len(k.invalidVotes)))
   210  		i += 8
   211  	}
   212  	for _, k := range e.enactedProposals {
   213  		idbytes := []byte(k.ID)
   214  		copy(output[i:], idbytes[:])
   215  		i += 32
   216  	}
   217  	// now add the hash of the nodeProposals
   218  	copy(output[i:], npHash[:])
   219  	h := vgcrypto.Hash(output)
   220  	e.log.Debug("governance state hash", logging.String("hash", hex.EncodeToString(h)))
   221  	return h
   222  }
   223  
   224  // ReloadConf updates the internal configuration of the governance engine.
   225  func (e *Engine) ReloadConf(cfg Config) {
   226  	e.log.Info("reloading configuration")
   227  	if e.log.GetLevel() != cfg.Level.Get() {
   228  		e.log.Info("updating log level",
   229  			logging.String("old", e.log.GetLevel().String()),
   230  			logging.String("new", cfg.Level.String()),
   231  		)
   232  		e.log.SetLevel(cfg.Level.Get())
   233  	}
   234  
   235  	e.Config = cfg
   236  }
   237  
   238  func (e *Engine) preEnactProposal(ctx context.Context, p *proposal) (te *ToEnact, perr types.ProposalError, err error) {
   239  	te = &ToEnact{
   240  		p: p,
   241  	}
   242  	defer func() {
   243  		if err != nil {
   244  			p.FailWithErr(perr, err)
   245  		}
   246  	}()
   247  
   248  	switch p.Terms.Change.GetTermType() {
   249  	case types.ProposalTermsTypeNewMarket:
   250  		te.m = &ToEnactNewMarket{}
   251  	case types.ProposalTermsTypeNewSpotMarket:
   252  		te.s = &ToEnactNewSpotMarket{}
   253  	case types.ProposalTermsTypeUpdateMarket:
   254  		mkt, perr, err := e.updatedMarketFromProposal(p)
   255  		if err != nil {
   256  			return nil, perr, err
   257  		}
   258  		te.updatedMarket = mkt
   259  	case types.ProposalTermsTypeUpdateSpotMarket:
   260  		mkt, perr, err := e.updatedSpotMarketFromProposal(p)
   261  		if err != nil {
   262  			return nil, perr, err
   263  		}
   264  		te.updatedSpotMarket = mkt
   265  	case types.ProposalTermsTypeUpdateNetworkParameter:
   266  		unp := p.Terms.GetUpdateNetworkParameter()
   267  		if unp != nil {
   268  			te.n = unp.Changes
   269  		}
   270  		if err := e.netp.Validate(unp.Changes.Key, unp.Changes.Value); err != nil {
   271  			return nil, types.ProposalErrorNetworkParameterInvalidValue, err
   272  		}
   273  	case types.ProposalTermsTypeNewAsset:
   274  		asset, err := e.assets.Get(p.ID)
   275  		if err != nil {
   276  			return nil, types.ProposalErrorUnspecified, err
   277  		}
   278  		te.newAsset = asset.Type()
   279  		// notify the asset engine that the proposal was passed
   280  		// and the asset is not pending for listing on the bridge
   281  		e.assets.SetPendingListing(ctx, p.ID)
   282  	case types.ProposalTermsTypeUpdateAsset:
   283  		asset, perr, err := e.updatedAssetFromProposal(p)
   284  		if err != nil {
   285  			return nil, perr, err
   286  		}
   287  		te.updatedAsset = asset
   288  	case types.ProposalTermsTypeNewFreeform:
   289  		te.f = &ToEnactFreeform{}
   290  	case types.ProposalTermsTypeNewTransfer:
   291  		te.t = &ToEnactTransfer{}
   292  	case types.ProposalTermsTypeCancelTransfer:
   293  		te.c = &ToEnactCancelTransfer{}
   294  	case types.ProposalTermsTypeUpdateMarketState:
   295  		te.msu = &ToEnactMarketStateUpdate{}
   296  	case types.ProposalTermsTypeUpdateReferralProgram:
   297  		te.referralProgramChanges = updatedReferralProgramFromProposal(p)
   298  	case types.ProposalTermsTypeUpdateVolumeDiscountProgram:
   299  		te.volumeDiscountProgram = updatedVolumeDiscountProgramFromProposal(p)
   300  	case types.ProposalTermsTypeUpdateVolumeRebateProgram:
   301  		te.volumeRebateProgram = updatedVolumeRebateProgramFromProposal(p)
   302  	case types.ProposalTermsTypeNewProtocolAutomatedPurchase:
   303  		te.automaticPurchase = &ToEnactAutomatedPurchase{}
   304  	}
   305  	return //nolint:nakedret
   306  }
   307  
   308  func (e *Engine) preVoteClosedProposal(p *proposal) *VoteClosed {
   309  	vc := &VoteClosed{
   310  		p: p.Proposal,
   311  	}
   312  	switch p.Terms.Change.GetTermType() {
   313  	case types.ProposalTermsTypeNewMarket, types.ProposalTermsTypeNewSpotMarket:
   314  		startAuction := true
   315  		if p.State != types.ProposalStatePassed {
   316  			startAuction = false
   317  		}
   318  
   319  		vc.m = &NewMarketVoteClosed{
   320  			startAuction: startAuction,
   321  		}
   322  	}
   323  	return vc
   324  }
   325  
   326  func (e *Engine) removeActiveProposalByID(ctx context.Context, id string) {
   327  	for i, p := range e.activeProposals {
   328  		if p.ID == id {
   329  			copy(e.activeProposals[i:], e.activeProposals[i+1:])
   330  			e.activeProposals[len(e.activeProposals)-1] = nil
   331  			e.activeProposals = e.activeProposals[:len(e.activeProposals)-1]
   332  
   333  			if p.State == types.ProposalStateDeclined || p.State == types.ProposalStateFailed || p.State == types.ProposalStateRejected {
   334  				// if it's an asset proposal we need to update it's
   335  				// state in the asset engine
   336  				switch p.Terms.Change.GetTermType() {
   337  				case types.ProposalTermsTypeNewAsset:
   338  					e.assets.SetRejected(ctx, p.ID)
   339  				}
   340  			}
   341  			return
   342  		}
   343  	}
   344  }
   345  
   346  func (e *Engine) OnTick(ctx context.Context, t time.Time) ([]*ToEnact, []*VoteClosed) {
   347  	now := t.Unix()
   348  
   349  	voteClosed, addToActiveProposals := e.evaluateBatchProposals(ctx, now)
   350  
   351  	var (
   352  		preparedToEnact []*ToEnact
   353  		toBeRemoved     []string // ids
   354  	)
   355  
   356  	for _, proposal := range e.activeProposals {
   357  		// check if the market for successor proposals still exists, if not, reject the proposal
   358  		if nm := proposal.Terms.GetNewMarket(); nm != nil && nm.Successor() != nil {
   359  			if _, err := e.markets.GetMarketState(proposal.ID); err != nil {
   360  				proposal.RejectWithErr(types.ProposalErrorInvalidSuccessorMarket, ErrParentMarketSucceededByCompeting)
   361  				e.broker.Send(events.NewProposalEvent(ctx, *proposal.Proposal))
   362  				toBeRemoved = append(toBeRemoved, proposal.ID)
   363  				continue
   364  			}
   365  		}
   366  
   367  		// do not check parent market, the market was either rejected when the parent was succeeded
   368  		// or, if the parent market state is gone (ie succession window has expired), the proposal simply
   369  		// loses its parent market reference
   370  		if proposal.ShouldClose(now) {
   371  			e.closeProposal(ctx, proposal)
   372  			voteClosed = append(voteClosed, e.preVoteClosedProposal(proposal))
   373  		}
   374  
   375  		if !proposal.IsOpen() && !proposal.IsPassed() {
   376  			toBeRemoved = append(toBeRemoved, proposal.ID)
   377  		} else if proposal.IsPassed() && (e.isAutoEnactableProposal(proposal.Proposal) || proposal.IsTimeToEnact(now)) {
   378  			enact, perr, err := e.preEnactProposal(ctx, proposal)
   379  			if err != nil {
   380  				e.broker.Send(events.NewProposalEvent(ctx, *proposal.Proposal))
   381  				toBeRemoved = append(toBeRemoved, proposal.ID)
   382  				e.log.Error("proposal enactment has failed",
   383  					logging.String("proposal-id", proposal.ID),
   384  					logging.String("proposal-error", perr.String()),
   385  					logging.Error(err))
   386  			} else {
   387  				toBeRemoved = append(toBeRemoved, proposal.ID)
   388  				preparedToEnact = append(preparedToEnact, enact)
   389  			}
   390  		}
   391  	}
   392  
   393  	// then get all proposal accepted through node validation, and start their vote time.
   394  	accepted, rejected := e.nodeProposalValidation.OnTick(t)
   395  	for _, p := range accepted {
   396  		e.log.Info("proposal has been validated by nodes, starting now",
   397  			logging.String("proposal-id", p.ID))
   398  		p.State = types.ProposalStateOpen
   399  		e.broker.Send(events.NewProposalEvent(ctx, *p.Proposal))
   400  		e.startValidatedProposal(p) // can't fail, and proposal has been validated at an ulterior time
   401  	}
   402  	for _, p := range rejected {
   403  		e.log.Info("proposal has not been validated by nodes",
   404  			logging.String("proposal-id", p.ID))
   405  		p.Reject(types.ProposalErrorNodeValidationFailed)
   406  		e.broker.Send(events.NewProposalEvent(ctx, *p.Proposal))
   407  
   408  		// if it's an asset proposal we need to update it's
   409  		// state in the asset engine
   410  		switch p.Terms.Change.GetTermType() {
   411  		case types.ProposalTermsTypeNewAsset:
   412  			e.assets.SetRejected(ctx, p.ID)
   413  		}
   414  	}
   415  
   416  	// then do the same thing for batches
   417  	acceptedBatches, rejectedBatches := e.nodeProposalValidation.OnTickBatch(t)
   418  	for _, p := range acceptedBatches {
   419  		e.log.Info("proposal has been validated by nodes, starting now",
   420  			logging.String("proposal-id", p.ID))
   421  		p.Open()
   422  
   423  		e.broker.Send(events.NewProposalEventFromProto(ctx, p.ToProto()))
   424  		proposalsEvents := []events.Event{}
   425  		for _, v := range p.Proposals {
   426  			proposalsEvents = append(proposalsEvents, events.NewProposalEvent(ctx, *v))
   427  		}
   428  		e.broker.SendBatch(proposalsEvents)
   429  
   430  		e.startValidatedBatchProposal(p) // can't fail, and proposal has been validated at an ulterior time
   431  	}
   432  
   433  	for _, p := range rejectedBatches {
   434  		e.log.Info("proposal has not been validated by nodes",
   435  			logging.String("proposal-id", p.ID))
   436  		p.Reject(types.ProposalErrorNodeValidationFailed)
   437  		e.broker.Send(events.NewProposalEventFromProto(ctx, p.ToProto()))
   438  		proposalsEvents := []events.Event{}
   439  		for _, v := range p.Proposals {
   440  			proposalsEvents = append(proposalsEvents, events.NewProposalEvent(ctx, *v))
   441  		}
   442  		e.broker.SendBatch(proposalsEvents)
   443  
   444  		for _, v := range p.Proposals {
   445  			// if it's an asset proposal we need to update it's
   446  			// state in the asset engine
   447  			switch v.Terms.Change.GetTermType() {
   448  			case types.ProposalTermsTypeNewAsset:
   449  				e.assets.SetRejected(ctx, v.ID)
   450  			}
   451  		}
   452  	}
   453  
   454  	toBeEnacted := []*ToEnact{}
   455  	for i, ep := range preparedToEnact {
   456  		// this is the new market proposal, and should already be in the slice
   457  		prop := *ep.ProposalData()
   458  
   459  		propType := prop.Terms.Change.GetTermType()
   460  		id := prop.ID
   461  		if propType == types.ProposalTermsTypeNewMarket || propType == types.ProposalTermsTypeUpdateMarket {
   462  			if propType == types.ProposalTermsTypeUpdateMarket {
   463  				id = prop.Terms.GetUpdateMarket().MarketID
   464  			}
   465  
   466  			// before trying to enact a successor market proposal, check if the market wasn't rejected due
   467  			// to another successor leaving opening auction
   468  			if _, err := e.markets.GetMarketState(id); err != nil {
   469  				if nm := prop.Terms.GetNewMarket(); nm != nil && nm.Successor() != nil {
   470  					prop.RejectWithErr(types.ProposalErrorInvalidSuccessorMarket, ErrParentMarketSucceededByCompeting)
   471  					e.broker.Send(events.NewProposalEvent(ctx, *prop.Proposal))
   472  					continue
   473  				}
   474  				e.log.Error("could not get state of market %s", logging.String("market-id", id))
   475  				continue
   476  			}
   477  		}
   478  
   479  		e.enactedProposals = append(e.enactedProposals, &prop)
   480  		toBeEnacted = append(toBeEnacted, preparedToEnact[i])
   481  	}
   482  
   483  	// now we iterate over all proposal ids to remove them from the list
   484  	for _, id := range toBeRemoved {
   485  		e.removeActiveProposalByID(ctx, id)
   486  	}
   487  
   488  	// add these passed proposals from batch to prepare them for enactment
   489  	e.activeProposals = append(e.activeProposals, addToActiveProposals...)
   490  
   491  	// flush here for now
   492  	return toBeEnacted, voteClosed
   493  }
   494  
   495  func (e *Engine) getProposal(id string) (*proposal, bool) {
   496  	for _, v := range e.activeProposals {
   497  		if v.ID == id {
   498  			return v, true
   499  		}
   500  	}
   501  
   502  	p, ok := e.nodeProposalValidation.getProposal(id)
   503  	if !ok {
   504  		return nil, false
   505  	}
   506  
   507  	return p.proposal, true
   508  }
   509  
   510  // SubmitProposal submits new proposal to the governance engine so it can be voted on, passed and enacted.
   511  // Only open can be submitted and validated at this point. No further validation happens.
   512  func (e *Engine) SubmitProposal(
   513  	ctx context.Context,
   514  	psub types.ProposalSubmission,
   515  	id, party string,
   516  ) (ts *ToSubmit, err error) {
   517  	if _, ok := e.getProposal(id); ok {
   518  		return nil, ErrProposalIsDuplicate // state is not allowed to change externally
   519  	}
   520  
   521  	p := &types.Proposal{
   522  		ID:                      id,
   523  		Timestamp:               e.timeService.GetTimeNow().UnixNano(),
   524  		Party:                   party,
   525  		State:                   types.ProposalStateOpen,
   526  		Terms:                   psub.Terms,
   527  		Reference:               psub.Reference,
   528  		Rationale:               psub.Rationale,
   529  		RequiredMajority:        num.DecimalZero(),
   530  		RequiredParticipation:   num.DecimalZero(),
   531  		RequiredLPMajority:      num.DecimalZero(),
   532  		RequiredLPParticipation: num.DecimalZero(),
   533  	}
   534  
   535  	defer func() {
   536  		e.broker.Send(events.NewProposalEvent(ctx, *p))
   537  	}()
   538  
   539  	params, err := e.getProposalParams(p.Terms.Change)
   540  	if err != nil {
   541  		p.RejectWithErr(types.ProposalErrorUnknownType, err)
   542  		return nil, err
   543  	}
   544  
   545  	if perr, err := e.validateOpenProposal(p, params); err != nil {
   546  		p.RejectWithErr(perr, err)
   547  		if e.log.IsDebug() {
   548  			e.log.Debug("Proposal rejected",
   549  				logging.String("proposal-id", p.ID),
   550  				logging.String("proposal details", p.String()),
   551  			)
   552  		}
   553  		return nil, err
   554  	}
   555  
   556  	// now if it's a 2 steps proposal, start the node votes
   557  	if e.isTwoStepsProposal(p) {
   558  		p.WaitForNodeVote()
   559  		if err := e.startTwoStepsProposal(ctx, p); err != nil {
   560  			p.RejectWithErr(types.ProposalErrorNodeValidationFailed, err)
   561  			if e.log.IsDebug() {
   562  				e.log.Debug("Proposal rejected",
   563  					logging.String("proposal-id", p.ID),
   564  					logging.String("proposal details", p.String()),
   565  				)
   566  			}
   567  			return nil, err
   568  		}
   569  	} else {
   570  		e.startProposal(p)
   571  	}
   572  
   573  	return e.intoToSubmit(ctx, p, &enactmentTime{current: p.Terms.EnactmentTimestamp}, false)
   574  }
   575  
   576  func (e *Engine) RejectProposal(
   577  	ctx context.Context, p *types.Proposal, r types.ProposalError, errorDetails error,
   578  ) error {
   579  	if _, ok := e.getProposal(p.ID); !ok {
   580  		return ErrProposalDoesNotExist
   581  	}
   582  
   583  	e.rejectProposal(ctx, p, r, errorDetails)
   584  	e.broker.Send(events.NewProposalEvent(ctx, *p))
   585  	return nil
   586  }
   587  
   588  // FinaliseEnactment receives the enact proposal and updates the state in our enactedProposal
   589  // list to have the current state of the proposals. This is entirely so that when we restore
   590  // from a snapshot we can propagate the proposal with the latest state back into the API service.
   591  func (e *Engine) FinaliseEnactment(ctx context.Context, prop *types.Proposal) {
   592  	// find the proposal so we can update the state after enactment
   593  	for _, enacted := range e.enactedProposals {
   594  		if enacted.ID == prop.ID {
   595  			enacted.State = prop.State
   596  			break
   597  		}
   598  	}
   599  	e.broker.Send(events.NewProposalEvent(ctx, *prop))
   600  }
   601  
   602  func (e *Engine) rejectProposal(ctx context.Context, p *types.Proposal, r types.ProposalError, errorDetails error) {
   603  	e.removeActiveProposalByID(ctx, p.ID)
   604  	p.RejectWithErr(r, errorDetails)
   605  }
   606  
   607  // toSubmit build the return response for the SubmitProposal
   608  // method.
   609  func (e *Engine) intoToSubmit(ctx context.Context, p *types.Proposal, enct *enactmentTime, restore bool) (*ToSubmit, error) {
   610  	tsb := &ToSubmit{p: p}
   611  
   612  	switch p.Terms.Change.GetTermType() {
   613  	case types.ProposalTermsTypeNewMarket:
   614  		// use to calculate the auction duration
   615  		// which is basically enacttime - closetime
   616  		// FIXME(): normally we should use the closetime
   617  		// but this would not play well with the MarketAuctionState stuff
   618  		// for now we start the auction as of now.
   619  		newMarket := p.Terms.GetNewMarket()
   620  		var parent *types.Market
   621  		if suc := newMarket.Successor(); suc != nil {
   622  			pm, ok := e.markets.GetMarket(suc.ParentID, true)
   623  			if !ok {
   624  				if !restore {
   625  					e.rejectProposal(ctx, p, types.ProposalErrorInvalidSuccessorMarket, ErrParentMarketDoesNotExist)
   626  					return nil, fmt.Errorf("%w, %v", ErrParentMarketDoesNotExist, types.ProposalErrorInvalidSuccessorMarket)
   627  				}
   628  			} else {
   629  				parent = &pm
   630  			}
   631  			// proposal to succeed a market that was already succeeded
   632  			// on restore, the parent market may be succeeded and the restored market may have own state (ie was the successor)
   633  			// So skip this check when restoring markets from checkpoints
   634  			if !restore && e.markets.IsSucceeded(suc.ParentID) {
   635  				e.rejectProposal(ctx, p, types.ProposalErrorInvalidSuccessorMarket, ErrParentMarketAlreadySucceeded)
   636  				return nil, fmt.Errorf("%w, %v", ErrParentMarketAlreadySucceeded, types.ProposalErrorInvalidSuccessorMarket)
   637  			}
   638  			// restore can be true while parent == nil. This is fine, though
   639  		}
   640  		now := e.timeService.GetTimeNow()
   641  		closeTime := time.Unix(p.Terms.ClosingTimestamp, 0)
   642  		enactTime := time.Unix(p.Terms.EnactmentTimestamp, 0)
   643  		auctionDuration := enactTime.Sub(closeTime)
   644  		if perr, err := validateNewMarketChange(newMarket, e.assets, true, e.netp, auctionDuration, enct, parent, now, restore); err != nil {
   645  			e.rejectProposal(ctx, p, perr, err)
   646  			return nil, fmt.Errorf("%w, %v", err, perr)
   647  		}
   648  		// closeTime = e.timeService.GetTimeNow().Round(time.Second)
   649  		// auctionDuration = enactTime.Sub(closeTime)
   650  		mkt, perr, err := buildMarketFromProposal(p.ID, newMarket, e.netp, auctionDuration)
   651  		if err != nil {
   652  			e.rejectProposal(ctx, p, perr, err)
   653  			return nil, fmt.Errorf("%w, %v", err, perr)
   654  		}
   655  		tsb.m = &ToSubmitNewMarket{
   656  			m: mkt,
   657  		}
   658  	case types.ProposalTermsTypeNewSpotMarket:
   659  		closeTime := time.Unix(p.Terms.ClosingTimestamp, 0)
   660  		enactTime := time.Unix(p.Terms.EnactmentTimestamp, 0)
   661  		newMarket := p.Terms.GetNewSpotMarket()
   662  		auctionDuration := enactTime.Sub(closeTime)
   663  		if perr, err := validateNewSpotMarketChange(newMarket, e.assets, true, e.netp, auctionDuration, enct); err != nil {
   664  			e.rejectProposal(ctx, p, perr, err)
   665  			return nil, fmt.Errorf("%w, %v", err, perr)
   666  		}
   667  		mkt, perr, err := buildSpotMarketFromProposal(p.ID, newMarket, e.netp, auctionDuration)
   668  		if err != nil {
   669  			e.rejectProposal(ctx, p, perr, err)
   670  			return nil, fmt.Errorf("%w, %v", err, perr)
   671  		}
   672  		tsb.s = &ToSubmitNewSpotMarket{
   673  			m: mkt,
   674  		}
   675  	}
   676  
   677  	return tsb, nil
   678  }
   679  
   680  func (e *Engine) startProposal(p *types.Proposal) {
   681  	e.activeProposals = append(e.activeProposals, &proposal{
   682  		Proposal:     p,
   683  		yes:          map[string]*types.Vote{},
   684  		no:           map[string]*types.Vote{},
   685  		invalidVotes: map[string]*types.Vote{},
   686  	})
   687  }
   688  
   689  func (e *Engine) startValidatedProposal(p *proposal) {
   690  	e.activeProposals = append(e.activeProposals, p)
   691  }
   692  
   693  func (e *Engine) startValidatedBatchProposal(p *batchProposal) {
   694  	e.activeBatchProposals[p.ID] = p
   695  }
   696  
   697  func (e *Engine) startTwoStepsProposal(ctx context.Context, p *types.Proposal) error {
   698  	return e.nodeProposalValidation.Start(ctx, p)
   699  }
   700  
   701  func (e *Engine) startTwoStepsBatchProposal(ctx context.Context, p *types.BatchProposal) error {
   702  	return e.nodeProposalValidation.StartBatch(ctx, p)
   703  }
   704  
   705  func (e *Engine) isTwoStepsProposal(p *types.Proposal) bool {
   706  	return e.nodeProposalValidation.IsNodeValidationRequired(p)
   707  }
   708  
   709  // isAutoEnactableProposal returns true if the proposal is of a type that has no on-chain enactment
   710  // and so can be automatically enacted without needing to care for the enactment timestamps.
   711  func (e *Engine) isAutoEnactableProposal(p *types.Proposal) bool {
   712  	switch p.Terms.Change.GetTermType() {
   713  	case types.ProposalTermsTypeNewFreeform:
   714  		return true
   715  	}
   716  	return false
   717  }
   718  
   719  func (e *Engine) getProposalParams(proposalTerm types.ProposalTerm) (*types.ProposalParameters, error) {
   720  	switch proposalTerm.GetTermType() {
   721  	case types.ProposalTermsTypeNewMarket:
   722  		return e.getNewMarketProposalParameters(), nil
   723  	case types.ProposalTermsTypeUpdateMarket:
   724  		return e.getUpdateMarketProposalParameters(), nil
   725  	case types.ProposalTermsTypeNewAsset:
   726  		return e.getNewAssetProposalParameters(), nil
   727  	case types.ProposalTermsTypeUpdateAsset:
   728  		return e.getUpdateAssetProposalParameters(), nil
   729  	case types.ProposalTermsTypeUpdateNetworkParameter:
   730  		return e.getUpdateNetworkParameterProposalParameters(), nil
   731  	case types.ProposalTermsTypeNewFreeform:
   732  		return e.getNewFreeformProposalParameters(), nil
   733  	case types.ProposalTermsTypeNewTransfer:
   734  		return e.getNewTransferProposalParameters(), nil
   735  	case types.ProposalTermsTypeCancelTransfer:
   736  		// for governance transfer cancellation reuse the governance transfer proposal params
   737  		return e.getNewTransferProposalParameters(), nil
   738  	case types.ProposalTermsTypeNewSpotMarket:
   739  		return e.getNewSpotMarketProposalParameters(), nil
   740  	case types.ProposalTermsTypeUpdateSpotMarket:
   741  		return e.getUpdateSpotMarketProposalParameters(), nil
   742  	case types.ProposalTermsTypeUpdateMarketState:
   743  		// reusing market update net params
   744  		return e.getUpdateMarketStateProposalParameters(), nil
   745  	case types.ProposalTermsTypeUpdateReferralProgram:
   746  		return e.getReferralProgramNetworkParameters(), nil
   747  	case types.ProposalTermsTypeUpdateVolumeDiscountProgram:
   748  		return e.getVolumeDiscountProgramNetworkParameters(), nil
   749  	case types.ProposalTermsTypeUpdateVolumeRebateProgram:
   750  		return e.getVolumeRebateProgramNetworkParameters(), nil
   751  	case types.ProposalTermsTypeNewProtocolAutomatedPurchase:
   752  		return e.getAutomaticPurchaseConfigNetworkParameters(), nil
   753  	default:
   754  		return nil, ErrUnsupportedProposalType
   755  	}
   756  }
   757  
   758  func (e *Engine) validateOpenProposal(proposal *types.Proposal, params *types.ProposalParameters) (types.ProposalError, error) {
   759  	// assign all requirement to the proposal itself.
   760  	proposal.RequiredMajority = params.RequiredMajority
   761  	proposal.RequiredParticipation = params.RequiredParticipation
   762  	proposal.RequiredLPMajority = params.RequiredMajorityLP
   763  	proposal.RequiredLPParticipation = params.RequiredParticipationLP
   764  
   765  	now := e.timeService.GetTimeNow()
   766  	closeTime := time.Unix(proposal.Terms.ClosingTimestamp, 0)
   767  	minCloseTime := now.Add(params.MinClose)
   768  	if closeTime.Before(minCloseTime) {
   769  		e.log.Debug("proposal close time is too soon",
   770  			logging.Time("expected-min", minCloseTime),
   771  			logging.Time("provided", closeTime),
   772  			logging.String("id", proposal.ID))
   773  		return types.ProposalErrorCloseTimeTooSoon,
   774  			fmt.Errorf("proposal closing time too soon, expected > %v, got %v", minCloseTime.UTC(), closeTime.UTC())
   775  	}
   776  
   777  	maxCloseTime := now.Add(params.MaxClose)
   778  	if closeTime.After(maxCloseTime) {
   779  		e.log.Debug("proposal close time is too late",
   780  			logging.Time("expected-max", maxCloseTime),
   781  			logging.Time("provided", closeTime),
   782  			logging.String("id", proposal.ID))
   783  		return types.ProposalErrorCloseTimeTooLate,
   784  			fmt.Errorf("proposal closing time too late, expected < %v, got %v", maxCloseTime.UTC(), closeTime.UTC())
   785  	}
   786  
   787  	enactTime := time.Unix(proposal.Terms.EnactmentTimestamp, 0)
   788  	minEnactTime := now.Add(params.MinEnact)
   789  	if !e.isAutoEnactableProposal(proposal) && enactTime.Before(minEnactTime) {
   790  		e.log.Debug("proposal enact time is too soon",
   791  			logging.Time("expected-min", minEnactTime),
   792  			logging.Time("provided", enactTime),
   793  			logging.String("id", proposal.ID))
   794  		return types.ProposalErrorEnactTimeTooSoon,
   795  			fmt.Errorf("proposal enactment time too soon, expected > %v, got %v", minEnactTime.UTC(), enactTime.UTC())
   796  	}
   797  
   798  	maxEnactTime := now.Add(params.MaxEnact)
   799  	if !e.isAutoEnactableProposal(proposal) && enactTime.After(maxEnactTime) {
   800  		e.log.Debug("proposal enact time is too late",
   801  			logging.Time("expected-max", maxEnactTime),
   802  			logging.Time("provided", enactTime),
   803  			logging.String("id", proposal.ID))
   804  		return types.ProposalErrorEnactTimeTooLate,
   805  			fmt.Errorf("proposal enactment time too late, expected < %v, got %v", maxEnactTime.UTC(), enactTime.UTC())
   806  	}
   807  
   808  	if e.isTwoStepsProposal(proposal) {
   809  		validationTime := time.Unix(proposal.Terms.ValidationTimestamp, 0)
   810  		if closeTime.Before(validationTime) {
   811  			e.log.Debug("proposal closing time can't be smaller or equal than validation time",
   812  				logging.Time("closing-time", closeTime),
   813  				logging.Time("validation-time", validationTime),
   814  				logging.String("id", proposal.ID))
   815  			return types.ProposalErrorIncompatibleTimestamps,
   816  				fmt.Errorf("proposal closing time cannot be before validation time, expected > %v got %v", validationTime.UTC(), closeTime.UTC())
   817  		}
   818  		if closeTime.Before(now) {
   819  			e.log.Debug("proposal validation time can't be in the past",
   820  				logging.Time("now", now),
   821  				logging.Time("validation-time", validationTime),
   822  				logging.String("id", proposal.ID))
   823  			return types.ProposalErrorIncompatibleTimestamps,
   824  				fmt.Errorf("proposal validation time cannot be in the past, expected > %v got %v", now.UTC(), validationTime.UTC())
   825  		}
   826  	}
   827  
   828  	if !e.isAutoEnactableProposal(proposal) && enactTime.Before(closeTime) {
   829  		e.log.Debug("proposal enactment time can't be smaller than closing time",
   830  			logging.Time("enactment-time", enactTime),
   831  			logging.Time("closing-time", closeTime),
   832  			logging.String("id", proposal.ID))
   833  		return types.ProposalErrorIncompatibleTimestamps,
   834  			fmt.Errorf("proposal enactment time cannot be before closing time, expected > %v got %v", closeTime.UTC(), enactTime.UTC())
   835  	}
   836  
   837  	checkProposerToken := true
   838  
   839  	if proposal.IsMarketUpdate() || proposal.IsMarketStateUpdate() {
   840  		marketID := ""
   841  		if proposal.Terms.GetMarketStateUpdate() != nil {
   842  			marketID = proposal.Terms.GetMarketStateUpdate().Changes.MarketID
   843  		} else {
   844  			marketID = proposal.MarketUpdate().MarketID
   845  		}
   846  		proposalError, err := e.validateMarketUpdate(proposal.ID, marketID, proposal.Party, params)
   847  		if err != nil && proposalError != types.ProposalErrorInsufficientEquityLikeShare {
   848  			return proposalError, err
   849  		}
   850  		checkProposerToken = proposalError == types.ProposalErrorInsufficientEquityLikeShare
   851  	}
   852  
   853  	if proposal.IsSpotMarketUpdate() {
   854  		proposalError, err := e.validateSpotMarketUpdate(proposal, params)
   855  		if err != nil && proposalError != types.ProposalErrorInsufficientEquityLikeShare {
   856  			return proposalError, err
   857  		}
   858  		checkProposerToken = proposalError == types.ProposalErrorInsufficientEquityLikeShare
   859  	}
   860  
   861  	if checkProposerToken {
   862  		proposerTokens, err := getGovernanceTokens(e.accs, proposal.Party)
   863  		if err != nil {
   864  			e.log.Debug("proposer have no governance token",
   865  				logging.PartyID(proposal.Party),
   866  				logging.ProposalID(proposal.ID))
   867  			return types.ProposalErrorInsufficientTokens, err
   868  		}
   869  		if proposerTokens.LT(params.MinProposerBalance) {
   870  			e.log.Debug("proposer have insufficient governance token",
   871  				logging.BigUint("expect-balance", params.MinProposerBalance),
   872  				logging.String("proposer-balance", proposerTokens.String()),
   873  				logging.PartyID(proposal.Party),
   874  				logging.ProposalID(proposal.ID))
   875  			return types.ProposalErrorInsufficientTokens,
   876  				fmt.Errorf("proposer have insufficient governance token, expected >= %v got %v", params.MinProposerBalance, proposerTokens)
   877  		}
   878  	}
   879  
   880  	return e.validateChange(proposal.Terms)
   881  }
   882  
   883  func (e *Engine) ValidatorKeyChanged(ctx context.Context, oldKey, newKey string) {
   884  	for _, p := range e.activeProposals {
   885  		e.updateValidatorKey(ctx, p.yes, oldKey, newKey)
   886  		e.updateValidatorKey(ctx, p.no, oldKey, newKey)
   887  		e.updateValidatorKey(ctx, p.invalidVotes, oldKey, newKey)
   888  	}
   889  }
   890  
   891  // AddVote adds a vote onto an existing active proposal.
   892  func (e *Engine) AddVote(ctx context.Context, cmd types.VoteSubmission, party string) error {
   893  	proposal, found := e.getProposal(cmd.ProposalID)
   894  	batchProposal, batchFound := e.getBatchProposal(cmd.ProposalID)
   895  
   896  	if found {
   897  		if !proposal.IsOpenForVotes() {
   898  			return ErrProposalNotOpenForVotes
   899  		}
   900  		return e.addVote(ctx, cmd, proposal, party)
   901  	}
   902  
   903  	if batchFound {
   904  		if !batchProposal.IsOpenForVotes() {
   905  			return ErrProposalNotOpenForVotes
   906  		}
   907  		return e.addBatchVote(ctx, batchProposal, cmd, party)
   908  	}
   909  
   910  	return ErrProposalDoesNotExist
   911  }
   912  
   913  func (e *Engine) addVote(ctx context.Context, cmd types.VoteSubmission, proposal *proposal, party string) error {
   914  	params, err := e.getProposalParams(proposal.Terms.Change)
   915  	if err != nil {
   916  		return err
   917  	}
   918  
   919  	if err := e.canVote(proposal.Proposal, params, party); err != nil {
   920  		e.log.Debug("invalid vote submission",
   921  			logging.PartyID(party),
   922  			logging.String("vote", cmd.String()),
   923  			logging.Error(err),
   924  		)
   925  		return err
   926  	}
   927  
   928  	vote := types.Vote{
   929  		PartyID:                     party,
   930  		ProposalID:                  cmd.ProposalID,
   931  		Value:                       cmd.Value,
   932  		Timestamp:                   e.timeService.GetTimeNow().UnixNano(),
   933  		TotalGovernanceTokenBalance: getTokensBalance(e.accs, party),
   934  		TotalGovernanceTokenWeight:  num.DecimalZero(),
   935  		TotalEquityLikeShareWeight:  num.DecimalZero(),
   936  	}
   937  	if proposal.IsMarketUpdate() {
   938  		mID := proposal.MarketUpdate().MarketID
   939  		vote.TotalEquityLikeShareWeight, _ = e.markets.GetEquityLikeShareForMarketAndParty(mID, party)
   940  	}
   941  
   942  	if err := proposal.AddVote(vote); err != nil {
   943  		return fmt.Errorf("couldn't cast the vote: %w", err)
   944  	}
   945  
   946  	if e.log.IsDebug() {
   947  		e.log.Debug("vote submission accepted",
   948  			logging.PartyID(party),
   949  			logging.String("vote", cmd.String()),
   950  		)
   951  	}
   952  	e.broker.Send(events.NewVoteEvent(ctx, vote))
   953  
   954  	return nil
   955  }
   956  
   957  func (e *Engine) canVote(
   958  	proposal *types.Proposal,
   959  	params *types.ProposalParameters,
   960  	party string,
   961  ) error {
   962  	voterTokens, err := getGovernanceTokens(e.accs, party)
   963  	if err != nil {
   964  		return err
   965  	}
   966  
   967  	if proposal.IsMarketUpdate() || proposal.IsSpotMarketUpdate() {
   968  		var mktID string
   969  		if proposal.IsMarketUpdate() {
   970  			mktID = proposal.MarketUpdate().MarketID
   971  		} else {
   972  			mktID = proposal.SpotMarketUpdate().MarketID
   973  		}
   974  		partyELS, _ := e.markets.GetEquityLikeShareForMarketAndParty(mktID, party)
   975  		if partyELS.IsZero() && voterTokens.IsZero() {
   976  			return ErrVoterInsufficientTokensAndEquityLikeShare
   977  		}
   978  		// If he is not voting using his equity-like share, he should at least
   979  		// have enough tokens.
   980  		if partyELS.IsZero() && voterTokens.LT(params.MinVoterBalance) {
   981  			return ErrVoterInsufficientTokens
   982  		}
   983  	} else {
   984  		if voterTokens.LT(params.MinVoterBalance) {
   985  			return ErrVoterInsufficientTokens
   986  		}
   987  	}
   988  
   989  	return nil
   990  }
   991  
   992  func (e *Engine) validateMarketUpdate(ID, marketID, party string, params *types.ProposalParameters) (types.ProposalError, error) {
   993  	if !e.markets.MarketExists(marketID) {
   994  		e.log.Debug("market does not exist",
   995  			logging.MarketID(marketID),
   996  			logging.PartyID(party),
   997  			logging.ProposalID(ID))
   998  		return types.ProposalErrorInvalidMarket, ErrMarketDoesNotExist
   999  	}
  1000  	for _, p := range e.activeProposals {
  1001  		if p.ID == marketID && p.IsOpen() {
  1002  			return types.ProposalErrorInvalidMarket, ErrMarketProposalStillOpen
  1003  		}
  1004  	}
  1005  
  1006  	partyELS, _ := e.markets.GetEquityLikeShareForMarketAndParty(marketID, party)
  1007  	if partyELS.LessThan(params.MinEquityLikeShare) {
  1008  		e.log.Debug("proposer have insufficient equity-like share",
  1009  			logging.String("expect-balance", params.MinEquityLikeShare.String()),
  1010  			logging.String("proposer-balance", partyELS.String()),
  1011  			logging.PartyID(party),
  1012  			logging.MarketID(marketID),
  1013  			logging.ProposalID(ID))
  1014  		return types.ProposalErrorInsufficientEquityLikeShare,
  1015  			fmt.Errorf("proposer have insufficient equity-like share, expected >= %v got %v", params.MinEquityLikeShare, partyELS)
  1016  	}
  1017  
  1018  	return types.ProposalErrorUnspecified, nil
  1019  }
  1020  
  1021  func (e *Engine) validateSpotMarketUpdate(proposal *types.Proposal, params *types.ProposalParameters) (types.ProposalError, error) {
  1022  	updateMarket := proposal.SpotMarketUpdate()
  1023  	if !e.markets.MarketExists(updateMarket.MarketID) {
  1024  		e.log.Debug("market does not exist",
  1025  			logging.MarketID(updateMarket.MarketID),
  1026  			logging.PartyID(proposal.Party),
  1027  			logging.ProposalID(proposal.ID))
  1028  		return types.ProposalErrorInvalidMarket, ErrMarketDoesNotExist
  1029  	}
  1030  	for _, p := range e.activeProposals {
  1031  		if p.ID == updateMarket.MarketID && p.IsOpen() {
  1032  			return types.ProposalErrorInvalidMarket, ErrMarketProposalStillOpen
  1033  		}
  1034  	}
  1035  
  1036  	partyELS, _ := e.markets.GetEquityLikeShareForMarketAndParty(updateMarket.MarketID, proposal.Party)
  1037  	if partyELS.LessThan(params.MinEquityLikeShare) {
  1038  		e.log.Debug("proposer have insufficient equity-like share",
  1039  			logging.String("expect-balance", params.MinEquityLikeShare.String()),
  1040  			logging.String("proposer-balance", partyELS.String()),
  1041  			logging.PartyID(proposal.Party),
  1042  			logging.MarketID(updateMarket.MarketID),
  1043  			logging.ProposalID(proposal.ID))
  1044  		return types.ProposalErrorInsufficientEquityLikeShare,
  1045  			fmt.Errorf("proposer have insufficient equity-like share, expected >= %v got %v", params.MinEquityLikeShare, partyELS)
  1046  	}
  1047  
  1048  	return types.ProposalErrorUnspecified, nil
  1049  }
  1050  
  1051  func (e *Engine) validateChange(terms *types.ProposalTerms) (types.ProposalError, error) {
  1052  	enactTime := time.Unix(terms.EnactmentTimestamp, 0)
  1053  	enct := &enactmentTime{current: terms.EnactmentTimestamp}
  1054  
  1055  	switch terms.Change.GetTermType() {
  1056  	case types.ProposalTermsTypeNewMarket:
  1057  		closeTime := time.Unix(terms.ClosingTimestamp, 0)
  1058  		newMarket := terms.GetNewMarket()
  1059  		var parent *types.Market
  1060  		if suc := newMarket.Successor(); suc != nil {
  1061  			pm, ok := e.markets.GetMarket(suc.ParentID, true)
  1062  			if !ok {
  1063  				return types.ProposalErrorInvalidSuccessorMarket, ErrParentMarketDoesNotExist
  1064  			}
  1065  			parent = &pm
  1066  		}
  1067  		return validateNewMarketChange(newMarket, e.assets, true, e.netp, enactTime.Sub(closeTime), enct, parent, e.timeService.GetTimeNow(), false)
  1068  	case types.ProposalTermsTypeUpdateMarket:
  1069  		enct.shouldNotVerify = true
  1070  		marketUpdate := terms.GetUpdateMarket()
  1071  		mkt, ok := e.markets.GetMarket(marketUpdate.MarketID, false)
  1072  		if !ok {
  1073  			return types.ProposalErrorInvalidMarket, ErrParentMarketDoesNotExist
  1074  		}
  1075  
  1076  		return validateUpdateMarketChange(marketUpdate, mkt, enct, e.timeService.GetTimeNow(), e.netp)
  1077  	case types.ProposalTermsTypeNewAsset:
  1078  		return e.validateNewAssetProposal(terms.GetNewAsset())
  1079  	case types.ProposalTermsTypeUpdateAsset:
  1080  		return terms.GetUpdateAsset().Validate()
  1081  	case types.ProposalTermsTypeUpdateNetworkParameter:
  1082  		return validateNetworkParameterUpdate(e.netp, terms.GetUpdateNetworkParameter().Changes)
  1083  	case types.ProposalTermsTypeNewTransfer:
  1084  		return e.validateGovernanceTransfer(terms.GetNewTransfer())
  1085  	case types.ProposalTermsTypeCancelTransfer:
  1086  		return e.validateCancelGovernanceTransfer(terms.GetCancelTransfer().Changes.TransferID)
  1087  	case types.ProposalTermsTypeUpdateMarketState:
  1088  		return e.validateMarketUpdateState(terms.GetMarketStateUpdate().Changes)
  1089  	case types.ProposalTermsTypeNewSpotMarket:
  1090  		closeTime := time.Unix(terms.ClosingTimestamp, 0)
  1091  		return validateNewSpotMarketChange(terms.GetNewSpotMarket(), e.assets, true, e.netp, enactTime.Sub(closeTime), enct)
  1092  	case types.ProposalTermsTypeUpdateSpotMarket:
  1093  		marketUpdate := terms.GetUpdateSpotMarket()
  1094  		if _, ok := e.markets.GetMarket(marketUpdate.MarketID, false); !ok {
  1095  			return types.ProposalErrorInvalidMarket, ErrParentMarketDoesNotExist
  1096  		}
  1097  		enct.shouldNotVerify = true
  1098  		return validateUpdateSpotMarketChange(terms.GetUpdateSpotMarket())
  1099  	case types.ProposalTermsTypeUpdateReferralProgram:
  1100  		return validateUpdateReferralProgram(e.netp, terms.GetUpdateReferralProgram(), terms.EnactmentTimestamp)
  1101  	case types.ProposalTermsTypeUpdateVolumeDiscountProgram:
  1102  		return validateUpdateVolumeDiscountProgram(e.netp, terms.GetUpdateVolumeDiscountProgram())
  1103  	case types.ProposalTermsTypeUpdateVolumeRebateProgram:
  1104  		return validateUpdateVolumeRebateProgram(e.netp, terms.GetUpdateVolumeRebateProgram())
  1105  	case types.ProposalTermsTypeNewProtocolAutomatedPurchase:
  1106  		automatedPurchase := terms.GetAutomatedPurchase()
  1107  		return e.validateNewProtocolAutomatedPurchaseConfiguration(automatedPurchase, enct, e.timeService.GetTimeNow())
  1108  	default:
  1109  		return types.ProposalErrorUnspecified, nil
  1110  	}
  1111  }
  1112  
  1113  func (e *Engine) validateGovernanceTransfer(newTransfer *types.NewTransfer) (types.ProposalError, error) {
  1114  	if err := e.banking.VerifyGovernanceTransfer(newTransfer.Changes); err != nil {
  1115  		return types.ProporsalErrorInvalidGovernanceTransfer, err
  1116  	}
  1117  	return types.ProposalErrorUnspecified, nil
  1118  }
  1119  
  1120  func (e *Engine) validateCancelGovernanceTransfer(transferID string) (types.ProposalError, error) {
  1121  	if err := e.banking.VerifyCancelGovernanceTransfer(transferID); err != nil {
  1122  		return types.ProporsalErrorFailedGovernanceTransferCancel, err
  1123  	}
  1124  	return types.ProposalErrorUnspecified, nil
  1125  }
  1126  
  1127  func (e *Engine) validateMarketUpdateState(update *types.MarketStateUpdateConfiguration) (types.ProposalError, error) {
  1128  	marketID := update.MarketID
  1129  	if !e.markets.MarketExists(marketID) {
  1130  		e.log.Debug("market does not exist", logging.MarketID(marketID))
  1131  		return types.ProposalErrorInvalidMarket, ErrMarketDoesNotExist
  1132  	}
  1133  
  1134  	marketState, err := e.markets.GetMarketState(marketID)
  1135  	if err != nil {
  1136  		return types.ProposalErrorInvalidMarket, err
  1137  	}
  1138  
  1139  	// if the market is already terminated or not yet started or settled
  1140  	if marketState == types.MarketStateCancelled || marketState == types.MarketStateClosed || marketState == types.MarketStateTradingTerminated || marketState == types.MarketStateSettled || marketState == types.MarketStateProposed {
  1141  		return types.ProposalErrorInvalidMarket, ErrMarketStateUpdateNotAllowed
  1142  	}
  1143  
  1144  	// in case the market is a capped future, make sure the settlement price is valid
  1145  	if update.SettlementPrice != nil && !e.markets.ValidateSettlementData(marketID, update.SettlementPrice) {
  1146  		return types.ProposalErrorInvalidStateUpdate, ErrSettlementDataOutOfRange
  1147  	}
  1148  
  1149  	return types.ProposalErrorUnspecified, nil
  1150  }
  1151  
  1152  func (e *Engine) validateNewAssetProposal(newAsset *types.NewAsset) (types.ProposalError, error) {
  1153  	if perr, err := newAsset.Validate(); err != nil {
  1154  		return perr, err
  1155  	}
  1156  
  1157  	erc20 := newAsset.GetChanges().GetERC20()
  1158  	if erc20 == nil {
  1159  		// not and erc20 asset, nothing todo
  1160  		return types.ProposalErrorUnspecified, nil
  1161  	}
  1162  
  1163  	// if we are an erc20 proposal
  1164  	// now we ensure no other proposal is ongoing for this asset, or that
  1165  	// any asset already exists for this address
  1166  
  1167  	for _, p := range e.activeProposals {
  1168  		p := p.Terms.GetNewAsset()
  1169  		if p == nil {
  1170  			continue
  1171  		}
  1172  		if source := p.Changes.GetERC20(); source != nil {
  1173  			if strings.EqualFold(source.ContractAddress, erc20.ContractAddress) {
  1174  				return types.ProposalErrorERC20AddressAlreadyInUse, ErrErc20AddressAlreadyInUse
  1175  			}
  1176  		}
  1177  	}
  1178  
  1179  	for _, p := range e.enactedProposals {
  1180  		p := p.Terms.GetNewAsset()
  1181  		if p == nil {
  1182  			continue
  1183  		}
  1184  		if source := p.Changes.GetERC20(); source != nil {
  1185  			if source.ChainID != erc20.ChainID {
  1186  				continue
  1187  			}
  1188  
  1189  			if strings.EqualFold(source.ContractAddress, erc20.ContractAddress) {
  1190  				return types.ProposalErrorERC20AddressAlreadyInUse, ErrErc20AddressAlreadyInUse
  1191  			}
  1192  		}
  1193  	}
  1194  
  1195  	if err := e.assets.ValidateEthereumAddress(erc20.ContractAddress, erc20.ChainID); err != nil {
  1196  		if err == assets.ErrErc20AddressAlreadyInUse {
  1197  			return types.ProposalErrorERC20AddressAlreadyInUse, err
  1198  		}
  1199  		return types.ProposalErrorInvalidAssetDetails, err
  1200  	}
  1201  
  1202  	return types.ProposalErrorUnspecified, nil
  1203  }
  1204  
  1205  func (e *Engine) closeProposal(ctx context.Context, proposal *proposal) {
  1206  	if !proposal.IsOpen() {
  1207  		return
  1208  	}
  1209  
  1210  	proposal.Close(e.accs, e.markets)
  1211  	if proposal.IsPassed() {
  1212  		e.log.Debug("Proposal passed", logging.ProposalID(proposal.ID))
  1213  	} else if proposal.IsDeclined() {
  1214  		e.log.Debug("Proposal declined", logging.ProposalID(proposal.ID), logging.String("details", proposal.ErrorDetails), logging.String("reason", proposal.Reason.String()))
  1215  	}
  1216  
  1217  	e.broker.Send(events.NewProposalEvent(ctx, *proposal.Proposal))
  1218  	e.broker.SendBatch(newUpdatedProposalEvents(ctx, proposal))
  1219  }
  1220  
  1221  func newUpdatedProposalEvents(ctx context.Context, proposal *proposal) []events.Event {
  1222  	votes := []*events.Vote{}
  1223  
  1224  	for _, y := range proposal.yes {
  1225  		votes = append(votes, events.NewVoteEvent(ctx, *y))
  1226  	}
  1227  	for _, n := range proposal.no {
  1228  		votes = append(votes, events.NewVoteEvent(ctx, *n))
  1229  	}
  1230  	for _, n := range proposal.invalidVotes {
  1231  		votes = append(votes, events.NewVoteEvent(ctx, *n))
  1232  	}
  1233  
  1234  	sort.SliceStable(votes, func(i, j int) bool {
  1235  		return votes[i].PartyID() < votes[j].PartyID()
  1236  	})
  1237  
  1238  	evts := make([]events.Event, 0, len(votes))
  1239  	for _, e := range votes {
  1240  		evts = append(evts, e)
  1241  	}
  1242  
  1243  	return evts
  1244  }
  1245  
  1246  func (e *Engine) updateValidatorKey(ctx context.Context, m map[string]*types.Vote, oldKey, newKey string) {
  1247  	if vote, ok := m[oldKey]; ok {
  1248  		delete(m, oldKey)
  1249  		vote.PartyID = newKey
  1250  		e.broker.Send(events.NewVoteEvent(ctx, *vote))
  1251  		m[newKey] = vote
  1252  	}
  1253  }
  1254  
  1255  func (e *Engine) updatedSpotMarketFromProposal(p *proposal) (*types.Market, types.ProposalError, error) {
  1256  	terms := p.Terms.GetUpdateSpotMarket()
  1257  	existingMarket, exists := e.markets.GetMarket(terms.MarketID, false)
  1258  	if !exists {
  1259  		return nil, types.ProposalErrorInvalidMarket, fmt.Errorf("market \"%s\" doesn't exist anymore", terms.MarketID)
  1260  	}
  1261  
  1262  	newMarket := &types.NewSpotMarket{
  1263  		Changes: &types.NewSpotMarketConfiguration{
  1264  			Instrument: &types.InstrumentConfiguration{
  1265  				Name: terms.Changes.Instrument.Name,
  1266  				Code: terms.Changes.Instrument.Code,
  1267  				Product: &types.InstrumentConfigurationSpot{
  1268  					Spot: &types.SpotProduct{
  1269  						Name:       existingMarket.TradableInstrument.Instrument.GetSpot().Name,
  1270  						BaseAsset:  existingMarket.TradableInstrument.Instrument.GetSpot().BaseAsset,
  1271  						QuoteAsset: existingMarket.TradableInstrument.Instrument.GetSpot().QuoteAsset,
  1272  					},
  1273  				},
  1274  			},
  1275  			PriceDecimalPlaces:        existingMarket.DecimalPlaces,
  1276  			SizeDecimalPlaces:         existingMarket.PositionDecimalPlaces,
  1277  			Metadata:                  terms.Changes.Metadata,
  1278  			PriceMonitoringParameters: terms.Changes.PriceMonitoringParameters,
  1279  			TargetStakeParameters:     terms.Changes.TargetStakeParameters,
  1280  			SLAParams:                 terms.Changes.SLAParams,
  1281  			TickSize:                  terms.Changes.TickSize,
  1282  			LiquidityFeeSettings:      terms.Changes.LiquidityFeeSettings,
  1283  			EnableTxReordering:        terms.Changes.EnableTxReordering,
  1284  			AllowedSellers:            append([]string{}, terms.Changes.AllowedSellers...),
  1285  		},
  1286  	}
  1287  
  1288  	switch riskModel := terms.Changes.RiskParameters.(type) {
  1289  	case nil:
  1290  		return nil, types.ProposalErrorNoRiskParameters, ErrMissingRiskParameters
  1291  	case *types.UpdateSpotMarketConfigurationSimple:
  1292  		newMarket.Changes.RiskParameters = &types.NewSpotMarketConfigurationSimple{
  1293  			Simple: riskModel.Simple,
  1294  		}
  1295  	case *types.UpdateSpotMarketConfigurationLogNormal:
  1296  		newMarket.Changes.RiskParameters = &types.NewSpotMarketConfigurationLogNormal{
  1297  			LogNormal: riskModel.LogNormal,
  1298  		}
  1299  	default:
  1300  		return nil, types.ProposalErrorUnknownRiskParameterType, ErrUnsupportedRiskParameters
  1301  	}
  1302  
  1303  	if perr, err := validateUpdateSpotMarketChange(terms); err != nil {
  1304  		return nil, perr, err
  1305  	}
  1306  
  1307  	previousAuctionDuration := time.Duration(existingMarket.OpeningAuction.Duration) * time.Second
  1308  	return buildSpotMarketFromProposal(existingMarket.ID, newMarket, e.netp, previousAuctionDuration)
  1309  }
  1310  
  1311  func (e *Engine) updatedMarketFromProposal(p *proposal) (*types.Market, types.ProposalError, error) {
  1312  	terms := p.Terms.GetUpdateMarket()
  1313  	existingMarket, exists := e.markets.GetMarket(terms.MarketID, false)
  1314  	if !exists {
  1315  		return nil, types.ProposalErrorInvalidMarket, fmt.Errorf("market \"%s\" doesn't exist anymore", terms.MarketID)
  1316  	}
  1317  
  1318  	allowedEmptyAMMLevels := defaultAllowedEmptyAMMLevels
  1319  	if terms.Changes.AllowedEmptyAmmLevels != nil {
  1320  		allowedEmptyAMMLevels = *terms.Changes.AllowedEmptyAmmLevels
  1321  	}
  1322  
  1323  	newMarket := &types.NewMarket{
  1324  		Changes: &types.NewMarketConfiguration{
  1325  			Instrument: &types.InstrumentConfiguration{
  1326  				Name: terms.Changes.Instrument.Name,
  1327  				Code: terms.Changes.Instrument.Code,
  1328  			},
  1329  			DecimalPlaces:                 existingMarket.DecimalPlaces,
  1330  			PositionDecimalPlaces:         existingMarket.PositionDecimalPlaces,
  1331  			Metadata:                      terms.Changes.Metadata,
  1332  			PriceMonitoringParameters:     terms.Changes.PriceMonitoringParameters,
  1333  			LiquidityMonitoringParameters: terms.Changes.LiquidityMonitoringParameters,
  1334  			LiquiditySLAParameters:        terms.Changes.LiquiditySLAParameters,
  1335  			LinearSlippageFactor:          terms.Changes.LinearSlippageFactor,
  1336  			QuadraticSlippageFactor:       terms.Changes.QuadraticSlippageFactor,
  1337  			LiquidityFeeSettings:          terms.Changes.LiquidityFeeSettings,
  1338  			LiquidationStrategy:           terms.Changes.LiquidationStrategy,
  1339  			MarkPriceConfiguration:        terms.Changes.MarkPriceConfiguration,
  1340  			TickSize:                      terms.Changes.TickSize,
  1341  			EnableTxReordering:            terms.Changes.EnableTxReordering,
  1342  			AllowedEmptyAmmLevels:         &allowedEmptyAMMLevels,
  1343  			AllowedSellers:                append([]string{}, terms.Changes.AllowedSellers...),
  1344  		},
  1345  	}
  1346  
  1347  	switch riskModel := terms.Changes.RiskParameters.(type) {
  1348  	case nil:
  1349  		return nil, types.ProposalErrorNoRiskParameters, ErrMissingRiskParameters
  1350  	case *types.UpdateMarketConfigurationSimple:
  1351  		newMarket.Changes.RiskParameters = &types.NewMarketConfigurationSimple{
  1352  			Simple: riskModel.Simple,
  1353  		}
  1354  	case *types.UpdateMarketConfigurationLogNormal:
  1355  		newMarket.Changes.RiskParameters = &types.NewMarketConfigurationLogNormal{
  1356  			LogNormal: riskModel.LogNormal,
  1357  		}
  1358  	default:
  1359  		return nil, types.ProposalErrorUnknownRiskParameterType, ErrUnsupportedRiskParameters
  1360  	}
  1361  
  1362  	switch product := terms.Changes.Instrument.Product.(type) {
  1363  	case nil:
  1364  		return nil, types.ProposalErrorNoProduct, ErrMissingProduct
  1365  	case *types.UpdateInstrumentConfigurationFuture:
  1366  		assets, _ := existingMarket.GetAssets()
  1367  		newMarket.Changes.Instrument.Product = &types.InstrumentConfigurationFuture{
  1368  			Future: &types.FutureProduct{
  1369  				SettlementAsset:                     assets[0],
  1370  				QuoteName:                           product.Future.QuoteName,
  1371  				DataSourceSpecForSettlementData:     product.Future.DataSourceSpecForSettlementData,
  1372  				DataSourceSpecForTradingTermination: product.Future.DataSourceSpecForTradingTermination,
  1373  				DataSourceSpecBinding:               product.Future.DataSourceSpecBinding,
  1374  				Cap:                                 existingMarket.GetFuture().Cap(),
  1375  			},
  1376  		}
  1377  	case *types.UpdateInstrumentConfigurationPerps:
  1378  		assets, _ := existingMarket.GetAssets()
  1379  		newMarket.Changes.Instrument.Product = &types.InstrumentConfigurationPerps{
  1380  			Perps: &types.PerpsProduct{
  1381  				SettlementAsset:                     assets[0],
  1382  				QuoteName:                           product.Perps.QuoteName,
  1383  				MarginFundingFactor:                 product.Perps.MarginFundingFactor,
  1384  				InterestRate:                        product.Perps.InterestRate,
  1385  				ClampLowerBound:                     product.Perps.ClampLowerBound,
  1386  				ClampUpperBound:                     product.Perps.ClampUpperBound,
  1387  				FundingRateScalingFactor:            product.Perps.FundingRateScalingFactor,
  1388  				FundingRateLowerBound:               product.Perps.FundingRateLowerBound,
  1389  				FundingRateUpperBound:               product.Perps.FundingRateUpperBound,
  1390  				DataSourceSpecForSettlementData:     product.Perps.DataSourceSpecForSettlementData,
  1391  				DataSourceSpecForSettlementSchedule: product.Perps.DataSourceSpecForSettlementSchedule,
  1392  				DataSourceSpecBinding:               product.Perps.DataSourceSpecBinding,
  1393  				InternalCompositePriceConfig:        product.Perps.InternalCompositePrice,
  1394  			},
  1395  		}
  1396  	default:
  1397  		return nil, types.ProposalErrorUnsupportedProduct, ErrUnsupportedProduct
  1398  	}
  1399  	// assign the current liquidation strategy if none was set on the update proposal
  1400  	if newMarket.Changes.LiquidationStrategy == nil {
  1401  		newMarket.Changes.LiquidationStrategy = existingMarket.LiquidationStrategy
  1402  	}
  1403  	if perr, err := validateUpdateMarketChange(terms, existingMarket, &enactmentTime{current: p.Terms.EnactmentTimestamp, shouldNotVerify: true}, e.timeService.GetTimeNow(), e.netp); err != nil {
  1404  		return nil, perr, err
  1405  	}
  1406  
  1407  	previousAuctionDuration := time.Duration(existingMarket.OpeningAuction.Duration) * time.Second
  1408  	return buildMarketFromProposal(existingMarket.ID, newMarket, e.netp, previousAuctionDuration)
  1409  }
  1410  
  1411  func (e *Engine) updatedAssetFromProposal(p *proposal) (*types.Asset, types.ProposalError, error) {
  1412  	a := p.Terms.GetUpdateAsset()
  1413  	existingAsset, err := e.assets.Get(a.AssetID)
  1414  	if err != nil {
  1415  		return nil, types.ProposalErrorInvalidAsset, err
  1416  	}
  1417  
  1418  	newAsset := &types.Asset{
  1419  		ID: a.AssetID,
  1420  		Details: &types.AssetDetails{
  1421  			Name:     existingAsset.ToAssetType().Details.Name,
  1422  			Symbol:   existingAsset.ToAssetType().Details.Symbol,
  1423  			Quantum:  a.Changes.Quantum,
  1424  			Decimals: existingAsset.DecimalPlaces(),
  1425  		},
  1426  	}
  1427  
  1428  	switch src := a.Changes.Source.(type) {
  1429  	case *types.AssetDetailsUpdateERC20:
  1430  		erc20, ok := existingAsset.ERC20()
  1431  		if !ok {
  1432  			return nil, types.ProposalErrorInvalidAsset, ErrExpectedERC20Asset
  1433  		}
  1434  		newAsset.Details.Source = &types.AssetDetailsErc20{
  1435  			ERC20: &types.ERC20{
  1436  				ContractAddress:   erc20.Address(),
  1437  				LifetimeLimit:     src.ERC20Update.LifetimeLimit.Clone(),
  1438  				WithdrawThreshold: src.ERC20Update.WithdrawThreshold.Clone(),
  1439  				ChainID:           erc20.ChainID(),
  1440  			},
  1441  		}
  1442  	default:
  1443  		return nil, types.ProposalErrorInvalidAsset, ErrUnsupportedAssetSourceType
  1444  	}
  1445  
  1446  	return newAsset, types.ProposalErrorUnspecified, nil
  1447  }
  1448  
  1449  func (e *Engine) OnChainIDUpdate(cID uint64) error {
  1450  	e.chainID = cID
  1451  	return nil
  1452  }
  1453  
  1454  func getTokensBalance(accounts StakingAccounts, partyID string) *num.Uint {
  1455  	balance, _ := getGovernanceTokens(accounts, partyID)
  1456  	return balance
  1457  }
  1458  
  1459  func getGovernanceTokens(accounts StakingAccounts, party string) (*num.Uint, error) {
  1460  	balance, err := accounts.GetAvailableBalance(party)
  1461  	if err != nil {
  1462  		return nil, err
  1463  	}
  1464  
  1465  	return balance, err
  1466  }