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 }