code.vegaprotocol.io/vega@v0.79.0/core/governance/proposal.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  	"time"
    20  
    21  	"code.vegaprotocol.io/vega/core/types"
    22  	"code.vegaprotocol.io/vega/libs/num"
    23  )
    24  
    25  type batchProposal struct {
    26  	*types.BatchProposal
    27  	yes          map[string]*types.Vote
    28  	no           map[string]*types.Vote
    29  	invalidVotes map[string]*types.Vote
    30  }
    31  
    32  // AddVote registers the last vote casted by a party. The proposal has to be
    33  // open, it returns an error otherwise.
    34  func (p *batchProposal) AddVote(vote types.Vote) error {
    35  	if !p.IsOpenForVotes() {
    36  		return ErrProposalNotOpenForVotes
    37  	}
    38  
    39  	if vote.Value == types.VoteValueYes {
    40  		delete(p.no, vote.PartyID)
    41  		p.yes[vote.PartyID] = &vote
    42  	} else {
    43  		delete(p.yes, vote.PartyID)
    44  		p.no[vote.PartyID] = &vote
    45  	}
    46  
    47  	return nil
    48  }
    49  
    50  func (p *batchProposal) IsOpenForVotes() bool {
    51  	// It's allowed to vote during the validation of the proposal by the node.
    52  	return p.State == types.ProposalStateOpen || p.State == types.ProposalStateWaitingForNodeVote
    53  }
    54  
    55  type proposal struct {
    56  	*types.Proposal
    57  	yes          map[string]*types.Vote
    58  	no           map[string]*types.Vote
    59  	invalidVotes map[string]*types.Vote
    60  }
    61  
    62  // ShouldClose tells if the proposal should be closed or not.
    63  // We also check the "open" state, alongside the closing timestamp as solely
    64  // relying on the closing timestamp could lead to call Close() on an
    65  // already-closed proposal.
    66  func (p *proposal) ShouldClose(now int64) bool {
    67  	return p.IsOpen() && p.Terms.ClosingTimestamp < now
    68  }
    69  
    70  func (p *proposal) IsTimeToEnact(now int64) bool {
    71  	return p.Terms.EnactmentTimestamp < now
    72  }
    73  
    74  func (p *proposal) SucceedsMarket(parentID string) bool {
    75  	nm := p.NewMarket()
    76  	if nm == nil {
    77  		return false
    78  	}
    79  	if pid, ok := nm.ParentMarketID(); !ok || pid != parentID {
    80  		return false
    81  	}
    82  	return true
    83  }
    84  
    85  func (p *proposal) IsOpenForVotes() bool {
    86  	// It's allowed to vote during the validation of the proposal by the node.
    87  	return p.State == types.ProposalStateOpen || p.State == types.ProposalStateWaitingForNodeVote
    88  }
    89  
    90  // AddVote registers the last vote casted by a party. The proposal has to be
    91  // open, it returns an error otherwise.
    92  func (p *proposal) AddVote(vote types.Vote) error {
    93  	if !p.IsOpenForVotes() {
    94  		return ErrProposalNotOpenForVotes
    95  	}
    96  
    97  	if vote.Value == types.VoteValueYes {
    98  		delete(p.no, vote.PartyID)
    99  		p.yes[vote.PartyID] = &vote
   100  	} else {
   101  		delete(p.yes, vote.PartyID)
   102  		p.no[vote.PartyID] = &vote
   103  	}
   104  
   105  	return nil
   106  }
   107  
   108  // Close determines the state of the proposal, passed or declined based on the
   109  // vote balance and weight.
   110  // Warning: this method should only be called once. Use ShouldClose() to know
   111  // when to call.
   112  func (p *proposal) Close(accounts StakingAccounts, markets Markets) {
   113  	if !p.IsOpen() {
   114  		return
   115  	}
   116  
   117  	defer func() {
   118  		p.purgeBlankVotes(p.yes)
   119  		p.purgeBlankVotes(p.no)
   120  	}()
   121  
   122  	tokenVoteState, tokenVoteError := p.computeVoteStateUsingTokens(accounts)
   123  
   124  	p.State = tokenVoteState
   125  	p.Reason = tokenVoteError
   126  
   127  	// Proposals, other than market updates, solely relies on votes using the
   128  	// governance tokens. So, only proposals for market update can go beyond this
   129  	// guard.
   130  	if !p.IsMarketUpdate() && !p.IsSpotMarketUpdate() {
   131  		return
   132  	}
   133  
   134  	if tokenVoteState == types.ProposalStateDeclined && tokenVoteError == types.ProposalErrorParticipationThresholdNotReached {
   135  		elsVoteState, elsVoteError := p.computeVoteStateUsingEquityLikeShare(markets)
   136  		p.State = elsVoteState
   137  		p.Reason = elsVoteError
   138  	}
   139  }
   140  
   141  func (p *proposal) computeVoteStateUsingTokens(accounts StakingAccounts) (types.ProposalState, types.ProposalError) {
   142  	totalStake := accounts.GetStakingAssetTotalSupply()
   143  
   144  	yes := p.countTokens(p.yes, accounts)
   145  	yesDec := num.DecimalFromUint(yes)
   146  	no := p.countTokens(p.no, accounts)
   147  	totalTokens := num.Sum(yes, no)
   148  	totalTokensDec := num.DecimalFromUint(totalTokens)
   149  	p.weightVotesFromToken(p.yes, totalTokensDec)
   150  	p.weightVotesFromToken(p.no, totalTokensDec)
   151  	majorityThreshold := totalTokensDec.Mul(p.RequiredMajority)
   152  	totalStakeDec := num.DecimalFromUint(totalStake)
   153  	participationThreshold := totalStakeDec.Mul(p.RequiredParticipation)
   154  
   155  	// if we have 0 votes, then just return straight away,
   156  	// prevents a proposal to go through if the participation is set to 0
   157  	if totalTokens.IsZero() {
   158  		return types.ProposalStateDeclined, types.ProposalErrorParticipationThresholdNotReached
   159  	}
   160  
   161  	if yesDec.GreaterThanOrEqual(majorityThreshold) && totalTokensDec.GreaterThanOrEqual(participationThreshold) {
   162  		return types.ProposalStatePassed, types.ProposalErrorUnspecified
   163  	}
   164  
   165  	if totalTokensDec.LessThan(participationThreshold) {
   166  		return types.ProposalStateDeclined, types.ProposalErrorParticipationThresholdNotReached
   167  	}
   168  
   169  	return types.ProposalStateDeclined, types.ProposalErrorMajorityThresholdNotReached
   170  }
   171  
   172  func (p *proposal) computeVoteStateUsingEquityLikeShare(markets Markets) (types.ProposalState, types.ProposalError) {
   173  	yes := p.countEquityLikeShare(p.yes, markets)
   174  	no := p.countEquityLikeShare(p.no, markets)
   175  	totalEquityLikeShare := yes.Add(no)
   176  	threshold := totalEquityLikeShare.Mul(p.RequiredLPMajority)
   177  
   178  	if yes.GreaterThanOrEqual(threshold) && totalEquityLikeShare.GreaterThanOrEqual(p.RequiredLPParticipation) {
   179  		return types.ProposalStatePassed, types.ProposalErrorUnspecified
   180  	}
   181  
   182  	if totalEquityLikeShare.LessThan(p.RequiredLPParticipation) {
   183  		return types.ProposalStateDeclined, types.ProposalErrorParticipationThresholdNotReached
   184  	}
   185  
   186  	return types.ProposalStateDeclined, types.ProposalErrorMajorityThresholdNotReached
   187  }
   188  
   189  func (p *proposal) countTokens(votes map[string]*types.Vote, accounts StakingAccounts) *num.Uint {
   190  	tally := num.UintZero()
   191  	for _, v := range votes {
   192  		v.TotalGovernanceTokenBalance = getTokensBalance(accounts, v.PartyID)
   193  		tally.AddSum(v.TotalGovernanceTokenBalance)
   194  	}
   195  
   196  	return tally
   197  }
   198  
   199  func (p *proposal) countEquityLikeShare(votes map[string]*types.Vote, markets Markets) num.Decimal {
   200  	tally := num.DecimalZero()
   201  	for _, v := range votes {
   202  		var marketID string
   203  		if p.MarketUpdate() != nil {
   204  			marketID = p.MarketUpdate().MarketID
   205  		} else {
   206  			marketID = p.SpotMarketUpdate().MarketID
   207  		}
   208  		v.TotalEquityLikeShareWeight, _ = markets.GetEquityLikeShareForMarketAndParty(marketID, v.PartyID)
   209  		tally = tally.Add(v.TotalEquityLikeShareWeight)
   210  	}
   211  
   212  	return tally
   213  }
   214  
   215  func (p *proposal) weightVotesFromToken(votes map[string]*types.Vote, totalVotes num.Decimal) {
   216  	if totalVotes.IsZero() {
   217  		return
   218  	}
   219  
   220  	for _, v := range votes {
   221  		tokenBalanceDec := num.DecimalFromUint(v.TotalGovernanceTokenBalance)
   222  		v.TotalGovernanceTokenWeight = tokenBalanceDec.Div(totalVotes)
   223  	}
   224  }
   225  
   226  // purgeBlankVotes removes votes that don't have tokens or equity-like share
   227  // associated. The user may have withdrawn their governance token or their
   228  // equity-like share before the end of the vote.
   229  // We will then purge them from the map if it's the case.
   230  func (p *proposal) purgeBlankVotes(votes map[string]*types.Vote) {
   231  	for k, v := range votes {
   232  		if v.TotalGovernanceTokenBalance.IsZero() && v.TotalEquityLikeShareWeight.IsZero() {
   233  			p.invalidVotes[k] = v
   234  			delete(votes, k)
   235  			continue
   236  		}
   237  	}
   238  }
   239  
   240  // ToEnact wraps the proposal in a type that has a convenient interface
   241  // to quickly work out what change we're dealing with, and get the data.
   242  type ToEnact struct {
   243  	p                      *proposal
   244  	m                      *ToEnactNewMarket
   245  	s                      *ToEnactNewSpotMarket
   246  	newAsset               *types.Asset
   247  	updatedAsset           *types.Asset
   248  	n                      *types.NetworkParameter
   249  	as                     *types.AssetDetails
   250  	updatedMarket          *types.Market
   251  	updatedSpotMarket      *types.Market
   252  	f                      *ToEnactFreeform
   253  	t                      *ToEnactTransfer
   254  	c                      *ToEnactCancelTransfer
   255  	msu                    *ToEnactMarketStateUpdate
   256  	referralProgramChanges *types.ReferralProgram
   257  	volumeDiscountProgram  *types.VolumeDiscountProgram
   258  	volumeRebateProgram    *types.VolumeRebateProgram
   259  	automaticPurchase      *ToEnactAutomatedPurchase
   260  }
   261  
   262  type ToEnactMarketStateUpdate struct{}
   263  
   264  type ToEnactTransfer struct{}
   265  
   266  type ToEnactCancelTransfer struct{}
   267  
   268  type ToEnactAutomatedPurchase struct{}
   269  
   270  // ToEnactNewMarket is just a empty struct, to signal
   271  // an enacted market. nothing to be done with it
   272  // for now (later maybe add information to check
   273  // end of opening auction or so).
   274  type ToEnactNewMarket struct{}
   275  
   276  type ToEnactNewSpotMarket struct{}
   277  
   278  // ToEnactFreeform there is nothing to enact with a freeform proposal.
   279  type ToEnactFreeform struct{}
   280  
   281  func (t ToEnact) IsVolumeDiscountProgramUpdate() bool {
   282  	return t.volumeDiscountProgram != nil
   283  }
   284  
   285  func (t ToEnact) IsVolumeRebateProgramUpdate() bool {
   286  	return t.volumeRebateProgram != nil
   287  }
   288  
   289  func (t ToEnact) IsReferralProgramUpdate() bool {
   290  	return t.referralProgramChanges != nil
   291  }
   292  
   293  func (t ToEnact) IsMarketStateUpdate() bool {
   294  	return t.msu != nil
   295  }
   296  
   297  func (t ToEnact) IsCancelTransfer() bool {
   298  	return t.c != nil
   299  }
   300  
   301  func (t ToEnact) IsNewTransfer() bool {
   302  	return t.t != nil
   303  }
   304  
   305  func (t ToEnact) IsNewMarket() bool {
   306  	return t.m != nil
   307  }
   308  
   309  func (t ToEnact) IsNewSpotMarket() bool {
   310  	return t.s != nil
   311  }
   312  
   313  func (t ToEnact) IsNewAsset() bool {
   314  	a := t.p.Terms.GetNewAsset()
   315  	return a != nil
   316  }
   317  
   318  func (t ToEnact) IsUpdateMarket() bool {
   319  	return t.updatedMarket != nil
   320  }
   321  
   322  func (t ToEnact) IsUpdateSpotMarket() bool {
   323  	return t.updatedSpotMarket != nil
   324  }
   325  
   326  func (t ToEnact) IsUpdateNetworkParameter() bool {
   327  	return t.n != nil
   328  }
   329  
   330  func (t ToEnact) IsNewAssetDetails() bool {
   331  	return t.IsNewAsset()
   332  }
   333  
   334  func (t ToEnact) IsFreeform() bool {
   335  	return t.f != nil
   336  }
   337  
   338  func (t *ToEnact) MarketStateUpdate() *ToEnactMarketStateUpdate {
   339  	return t.msu
   340  }
   341  
   342  func (t *ToEnact) NewTransfer() *ToEnactTransfer {
   343  	return t.t
   344  }
   345  
   346  func (t *ToEnact) IsAutomatedPurchase() bool {
   347  	return t.automaticPurchase != nil
   348  }
   349  
   350  func (t *ToEnact) NewProtocolAutomatedPurchase() *ToEnactAutomatedPurchase {
   351  	return t.automaticPurchase
   352  }
   353  
   354  func (t *ToEnact) CancelTransfer() *ToEnactCancelTransfer {
   355  	return t.c
   356  }
   357  
   358  func (t *ToEnact) NewMarket() *ToEnactNewMarket {
   359  	return t.m
   360  }
   361  
   362  func (t *ToEnact) NewAsset() *types.Asset {
   363  	return t.newAsset
   364  }
   365  
   366  func (t *ToEnact) NewAssetDetails() *types.AssetDetails {
   367  	return t.as
   368  }
   369  
   370  func (t *ToEnact) UpdateNetworkParameter() *types.NetworkParameter {
   371  	return t.n
   372  }
   373  
   374  func (t *ToEnact) ReferralProgramChanges() *types.ReferralProgram {
   375  	return t.referralProgramChanges
   376  }
   377  
   378  func (t *ToEnact) VolumeDiscountProgramUpdate() *types.VolumeDiscountProgram {
   379  	return t.volumeDiscountProgram
   380  }
   381  
   382  func (t *ToEnact) VolumeRebateProgramUpdate() *types.VolumeRebateProgram {
   383  	return t.volumeRebateProgram
   384  }
   385  
   386  func (t *ToEnact) UpdateMarket() *types.Market {
   387  	return t.updatedMarket
   388  }
   389  
   390  func (t *ToEnact) UpdateSpotMarket() *types.Market {
   391  	return t.updatedSpotMarket
   392  }
   393  
   394  func (t *ToEnact) NewFreeform() *ToEnactFreeform {
   395  	return t.f
   396  }
   397  
   398  func (t *ToEnact) ProposalData() *proposal { //revive:disable:unexported-return
   399  	return t.p
   400  }
   401  
   402  func (t *ToEnact) Proposal() *types.Proposal {
   403  	return t.p.Proposal
   404  }
   405  
   406  func (t *ToEnact) IsUpdateAsset() bool {
   407  	return t.updatedAsset != nil
   408  }
   409  
   410  func (t *ToEnact) UpdateAsset() *types.Asset {
   411  	return t.updatedAsset
   412  }
   413  
   414  // ToSubmit wraps the proposal in a type that has a convenient interface
   415  // to quickly work out what change we're dealing with, and get the data
   416  // This cover every kind of proposal which requires action after a proposal
   417  // is submitted.
   418  type ToSubmit struct {
   419  	p *types.Proposal
   420  	m *ToSubmitNewMarket
   421  	s *ToSubmitNewSpotMarket
   422  }
   423  
   424  func (t *ToSubmit) Proposal() *types.Proposal {
   425  	return t.p
   426  }
   427  
   428  func (t ToSubmit) IsNewMarket() bool {
   429  	return t.m != nil
   430  }
   431  
   432  func (t *ToSubmit) NewMarket() *ToSubmitNewMarket {
   433  	return t.m
   434  }
   435  
   436  func (t ToSubmit) IsNewSpotMarket() bool {
   437  	return t.s != nil
   438  }
   439  
   440  func (t *ToSubmit) NewSpotMarket() *ToSubmitNewSpotMarket {
   441  	return t.s
   442  }
   443  
   444  type ToSubmitNewSpotMarket struct {
   445  	m *types.Market
   446  }
   447  
   448  func (t *ToSubmitNewSpotMarket) Market() *types.Market {
   449  	return t.m
   450  }
   451  
   452  func (t *ToSubmit) ParentMarketID() string {
   453  	return t.m.m.ParentMarketID
   454  }
   455  
   456  func (t *ToSubmit) InsurancePoolFraction() *num.Decimal {
   457  	if len(t.m.m.ParentMarketID) == 0 {
   458  		return nil
   459  	}
   460  	ipf := t.m.m.InsurancePoolFraction
   461  	return &ipf
   462  }
   463  
   464  type ToSubmitNewMarket struct {
   465  	m   *types.Market
   466  	oos time.Time // opening auction start
   467  }
   468  
   469  func (t *ToSubmitNewMarket) Market() *types.Market {
   470  	return t.m
   471  }
   472  
   473  func (t *ToSubmitNewMarket) OpeningAuctionStart() time.Time {
   474  	return t.oos
   475  }
   476  
   477  type VoteClosed struct {
   478  	p *types.Proposal
   479  	m *NewMarketVoteClosed
   480  }
   481  
   482  func (t *VoteClosed) Proposal() *types.Proposal {
   483  	return t.p
   484  }
   485  
   486  func (t *VoteClosed) IsNewMarket() bool {
   487  	return t.m != nil
   488  }
   489  
   490  func (t *VoteClosed) NewMarket() *NewMarketVoteClosed {
   491  	return t.m
   492  }
   493  
   494  type NewMarketVoteClosed struct {
   495  	// true if the auction is to be started
   496  	// false if the vote did get a majority of true
   497  	// and the market is to be rejected.
   498  	startAuction bool
   499  }
   500  
   501  func (t *NewMarketVoteClosed) Rejected() bool {
   502  	return !t.startAuction
   503  }
   504  
   505  func (t *NewMarketVoteClosed) StartAuction() bool {
   506  	return t.startAuction
   507  }