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 }