code.vegaprotocol.io/vega@v0.79.0/core/assets/assets.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 assets 17 18 import ( 19 "context" 20 "errors" 21 "fmt" 22 "sort" 23 "strings" 24 "sync" 25 "time" 26 27 "code.vegaprotocol.io/vega/core/assets/builtin" 28 "code.vegaprotocol.io/vega/core/assets/erc20" 29 "code.vegaprotocol.io/vega/core/broker" 30 "code.vegaprotocol.io/vega/core/events" 31 nweth "code.vegaprotocol.io/vega/core/nodewallets/eth" 32 "code.vegaprotocol.io/vega/core/types" 33 "code.vegaprotocol.io/vega/logging" 34 ) 35 36 var ( 37 ErrAssetDoesNotExist = errors.New("asset does not exist") 38 ErrUnknownAssetSource = errors.New("unknown asset source") 39 ErrErc20AddressAlreadyInUse = errors.New("erc20 address already in use") 40 ErrUnknownChainID = errors.New("erc20 chain-id does not correspond to a bridge") 41 ) 42 43 //go:generate go run github.com/golang/mock/mockgen -destination mocks/mocks.go -package mocks code.vegaprotocol.io/vega/core/assets ERC20BridgeView,Notary 44 45 type ERC20BridgeView interface { 46 FindAsset(asset *types.AssetDetails) error 47 } 48 49 type Notary interface { 50 StartAggregate(resID string, kind types.NodeSignatureKind, signature []byte) 51 OfferSignatures(kind types.NodeSignatureKind, f func(id string) []byte) 52 } 53 54 type Service struct { 55 log *logging.Logger 56 cfg Config 57 58 broker broker.Interface 59 60 // id to asset 61 // these assets exists and have been save 62 amu sync.RWMutex 63 assets map[string]*Asset 64 65 // this is a list of pending asset which are currently going through 66 // proposal, they can later on be promoted to the asset lists once 67 // the proposal is accepted by both the nodes and the users 68 pamu sync.RWMutex 69 pendingAssets map[string]*Asset 70 pendingAssetUpdates map[string]*Asset 71 72 ethWallet nweth.EthereumWallet 73 74 primaryEthChainID string 75 primaryEthClient erc20.ETHClient 76 primaryBridgeView ERC20BridgeView 77 78 secondaryEthChainID string 79 secondaryEthClient erc20.ETHClient 80 secondaryBridgeView ERC20BridgeView 81 82 notary Notary 83 ass *assetsSnapshotState 84 85 ethToVega map[string]string 86 isValidator bool 87 } 88 89 func New( 90 ctx context.Context, 91 log *logging.Logger, 92 cfg Config, 93 nw nweth.EthereumWallet, 94 primaryEthClient erc20.ETHClient, 95 secondaryEthClient erc20.ETHClient, 96 broker broker.Interface, 97 primaryBridgeView ERC20BridgeView, 98 secondaryBridgeView ERC20BridgeView, 99 notary Notary, 100 isValidator bool, 101 ) (*Service, error) { 102 log = log.Named(namedLogger) 103 log.SetLevel(cfg.Level.Get()) 104 105 s := &Service{ 106 log: log, 107 cfg: cfg, 108 broker: broker, 109 assets: map[string]*Asset{}, 110 pendingAssets: map[string]*Asset{}, 111 pendingAssetUpdates: map[string]*Asset{}, 112 ethWallet: nw, 113 primaryEthClient: primaryEthClient, 114 secondaryEthClient: secondaryEthClient, 115 notary: notary, 116 ass: &assetsSnapshotState{}, 117 isValidator: isValidator, 118 ethToVega: map[string]string{}, 119 primaryBridgeView: primaryBridgeView, 120 secondaryBridgeView: secondaryBridgeView, 121 } 122 123 if isValidator { 124 primaryChainID, err := s.primaryEthClient.ChainID(ctx) 125 if err != nil { 126 return nil, fmt.Errorf("could not fetch chain ID from the primary ethereum client: %w", err) 127 } 128 s.primaryEthChainID = primaryChainID.String() 129 130 secondaryChainID, err := s.secondaryEthClient.ChainID(ctx) 131 if err != nil { 132 return nil, fmt.Errorf("could not fetch chain ID from the secondary ethereum client: %w", err) 133 } 134 s.secondaryEthChainID = secondaryChainID.String() 135 } 136 137 return s, nil 138 } 139 140 // ReloadConf updates the internal configuration. 141 func (s *Service) ReloadConf(cfg Config) { 142 s.log.Info("reloading configuration") 143 if s.log.GetLevel() != cfg.Level.Get() { 144 s.log.Info("updating log level", 145 logging.String("old", s.log.GetLevel().String()), 146 logging.String("new", cfg.Level.String()), 147 ) 148 s.log.SetLevel(cfg.Level.Get()) 149 } 150 151 s.cfg = cfg 152 } 153 154 // Enable move the state of an from pending the list of valid and accepted assets. 155 func (s *Service) Enable(ctx context.Context, assetID string) error { 156 s.pamu.Lock() 157 defer s.pamu.Unlock() 158 asset, ok := s.pendingAssets[assetID] 159 if !ok { 160 return ErrAssetDoesNotExist 161 } 162 163 asset.SetEnabled() 164 s.amu.Lock() 165 defer s.amu.Unlock() 166 s.assets[assetID] = asset 167 if asset.IsERC20() { 168 eth, _ := asset.ERC20() 169 s.ethToVega[eth.ProtoAsset().GetDetails().GetErc20().GetContractAddress()] = assetID 170 } 171 delete(s.pendingAssets, assetID) 172 s.broker.Send(events.NewAssetEvent(ctx, *asset.Type())) 173 return nil 174 } 175 176 // EnactPendingAsset the given id for an asset has just been enacted by the governance engine so we 177 // now need to generate signatures so that the asset can be listed. 178 func (s *Service) EnactPendingAsset(id string) { 179 pa, _ := s.Get(id) 180 var err error 181 var signature []byte 182 if s.isValidator { 183 switch { 184 case pa.IsERC20(): 185 asset, _ := pa.ERC20() 186 _, signature, err = asset.SignListAsset() 187 if err != nil { 188 s.log.Panic("couldn't to sign transaction to list asset, is the node properly configured as a validator?", 189 logging.Error(err)) 190 } 191 default: 192 s.log.Panic("trying to generate signatures for an unknown asset type") 193 } 194 } 195 196 s.notary.StartAggregate(id, types.NodeSignatureKindAssetNew, signature) 197 } 198 199 // ValidateEthereumAddress checks that the given ERC20 address and chainID corresponds to one of Vega's bridges 200 // and isn't the address of an asset that already exists. 201 func (s *Service) ValidateEthereumAddress(address, chainID string) error { 202 if chainID != s.primaryEthChainID && chainID != s.secondaryEthChainID { 203 return ErrUnknownChainID 204 } 205 206 for _, a := range s.assets { 207 if source, ok := a.ERC20(); ok { 208 if source.ChainID() != chainID { 209 // asset is on a different chain, definitely is not a dupe of it 210 continue 211 } 212 213 if strings.EqualFold(source.Address(), address) { 214 return ErrErc20AddressAlreadyInUse 215 } 216 } 217 } 218 for _, a := range s.pendingAssets { 219 if source, ok := a.ERC20(); ok { 220 if source.ChainID() != chainID { 221 // asset is on a different chain, definitely is not a dupe of it 222 continue 223 } 224 225 if strings.EqualFold(source.Address(), address) { 226 return ErrErc20AddressAlreadyInUse 227 } 228 } 229 } 230 231 return nil 232 } 233 234 // SetPendingListing update the state of an asset from proposed 235 // to pending listing on the bridge. 236 func (s *Service) SetPendingListing(ctx context.Context, assetID string) error { 237 s.pamu.Lock() 238 defer s.pamu.Unlock() 239 asset, ok := s.pendingAssets[assetID] 240 if !ok { 241 return ErrAssetDoesNotExist 242 } 243 244 asset.SetPendingListing() 245 s.broker.Send(events.NewAssetEvent(ctx, *asset.Type())) 246 return nil 247 } 248 249 // SetRejected update the state of an asset from proposed 250 // to pending listing on the bridge. 251 func (s *Service) SetRejected(ctx context.Context, assetID string) error { 252 s.pamu.Lock() 253 defer s.pamu.Unlock() 254 asset, ok := s.pendingAssets[assetID] 255 if !ok { 256 return ErrAssetDoesNotExist 257 } 258 259 asset.SetRejected() 260 s.broker.Send(events.NewAssetEvent(ctx, *asset.Type())) 261 delete(s.pendingAssets, assetID) 262 return nil 263 } 264 265 func (s *Service) GetVegaIDFromEthereumAddress(address string) string { 266 s.amu.Lock() 267 defer s.amu.Unlock() 268 return s.ethToVega[address] 269 } 270 271 func (s *Service) IsEnabled(assetID string) bool { 272 s.amu.RLock() 273 defer s.amu.RUnlock() 274 _, ok := s.assets[assetID] 275 return ok 276 } 277 278 // SetBridgeChainID sets the chain-ids for the bridge once we have processed the network parameters 279 // this is necessary so that non-validator nodes (which cannot just ask the eth-client) can know what they 280 // are. 281 func (s *Service) SetBridgeChainID(chainID string, primary bool) { 282 if primary { 283 s.primaryEthChainID = chainID 284 return 285 } 286 s.secondaryEthChainID = chainID 287 } 288 289 func (s *Service) OnTick(_ context.Context, _ time.Time) { 290 s.notary.OfferSignatures(types.NodeSignatureKindAssetNew, s.offerERC20NotarySignatures) 291 } 292 293 func (s *Service) offerERC20NotarySignatures(id string) []byte { 294 if !s.isValidator { 295 return nil 296 } 297 298 pa, err := s.Get(id) 299 if err != nil { 300 s.log.Panic("unable to find asset", logging.AssetID(id)) 301 } 302 303 asset, _ := pa.ERC20() 304 _, signature, err := asset.SignListAsset() 305 if err != nil { 306 s.log.Panic("couldn't to sign transaction to list asset, is the node properly configured as a validator?", 307 logging.Error(err)) 308 } 309 310 return signature 311 } 312 313 func (s *Service) assetFromDetails(assetID string, assetDetails *types.AssetDetails) (*Asset, error) { 314 switch assetDetails.Source.(type) { 315 case *types.AssetDetailsBuiltinAsset: 316 return &Asset{ 317 builtin.New(assetID, assetDetails), 318 }, nil 319 case *types.AssetDetailsErc20: 320 var asset *erc20.ERC20 321 if s.isValidator { 322 client, err := s.ethClientByChainID(assetDetails.GetERC20().ChainID) 323 if err != nil { 324 return nil, err 325 } 326 a, err := erc20.New(assetID, assetDetails, s.ethWallet, client) 327 if err != nil { 328 return nil, err 329 } 330 asset = a 331 } else { 332 a, err := erc20.New(assetID, assetDetails, nil, nil) 333 if err != nil { 334 return nil, err 335 } 336 asset = a 337 } 338 return &Asset{asset}, nil 339 default: 340 return nil, ErrUnknownAssetSource 341 } 342 } 343 344 func (s *Service) buildAssetUpdateFromProto(asset *types.Asset) (*Asset, error) { 345 switch asset.Details.Source.(type) { 346 case *types.AssetDetailsBuiltinAsset: 347 return &Asset{ 348 builtin.New(asset.ID, asset.Details), 349 }, nil 350 case *types.AssetDetailsErc20: 351 var ( 352 erc20Asset *erc20.ERC20 353 err error 354 ) 355 if s.isValidator { 356 client, err := s.ethClientByChainID(asset.Details.GetERC20().ChainID) 357 if err != nil { 358 return nil, err 359 } 360 a, err := erc20.New(asset.ID, asset.Details, s.ethWallet, client) 361 if err != nil { 362 return nil, err 363 } 364 erc20Asset = a 365 } else { 366 a, err := erc20.New(asset.ID, asset.Details, nil, nil) 367 if err != nil { 368 return nil, err 369 } 370 erc20Asset = a 371 } 372 if err != nil { 373 return nil, err 374 } 375 return &Asset{erc20Asset}, nil 376 default: 377 return nil, ErrUnknownAssetSource 378 } 379 } 380 381 // NewAsset add a new asset to the pending list of assets 382 // the ref is the reference of proposal which submitted the new asset 383 // returns the assetID and an error. 384 func (s *Service) NewAsset(ctx context.Context, proposalID string, assetDetails *types.AssetDetails) (string, error) { 385 s.pamu.Lock() 386 defer s.pamu.Unlock() 387 asset, err := s.assetFromDetails(proposalID, assetDetails) 388 if err != nil { 389 return "", err 390 } 391 s.pendingAssets[proposalID] = asset 392 s.broker.Send(events.NewAssetEvent(ctx, *asset.Type())) 393 394 return proposalID, err 395 } 396 397 func (s *Service) StageAssetUpdate(updatedAssetProto *types.Asset) error { 398 s.pamu.Lock() 399 defer s.pamu.Unlock() 400 if _, ok := s.assets[updatedAssetProto.ID]; !ok { 401 return ErrAssetDoesNotExist 402 } 403 404 updatedAsset, err := s.buildAssetUpdateFromProto(updatedAssetProto) 405 if err != nil { 406 return fmt.Errorf("couldn't update asset: %w", err) 407 } 408 s.pendingAssetUpdates[updatedAssetProto.ID] = updatedAsset 409 return nil 410 } 411 412 func (s *Service) ApplyAssetUpdate(ctx context.Context, assetID string) error { 413 s.pamu.Lock() 414 defer s.pamu.Unlock() 415 416 updatedAsset, ok := s.pendingAssetUpdates[assetID] 417 if !ok { 418 return ErrAssetDoesNotExist 419 } 420 421 s.amu.Lock() 422 defer s.amu.Unlock() 423 424 currentAsset, ok := s.assets[assetID] 425 if !ok { 426 return ErrAssetDoesNotExist 427 } 428 updatedAsset.SetEnabled() 429 if err := currentAsset.Update(updatedAsset); err != nil { 430 s.log.Panic("couldn't update the asset", logging.Error(err)) 431 } 432 433 delete(s.pendingAssetUpdates, assetID) 434 s.broker.Send(events.NewAssetEvent(ctx, *updatedAsset.Type())) 435 return nil 436 } 437 438 func (s *Service) GetEnabledAssets() []*types.Asset { 439 s.amu.RLock() 440 defer s.amu.RUnlock() 441 ret := make([]*types.Asset, 0, len(s.assets)) 442 for _, a := range s.assets { 443 ret = append(ret, a.ToAssetType()) 444 } 445 sort.SliceStable(ret, func(i, j int) bool { return ret[i].ID < ret[j].ID }) 446 return ret 447 } 448 449 func (s *Service) getPendingAssets() []*types.Asset { 450 s.pamu.RLock() 451 defer s.pamu.RUnlock() 452 ret := make([]*types.Asset, 0, len(s.assets)) 453 for _, a := range s.pendingAssets { 454 ret = append(ret, a.ToAssetType()) 455 } 456 sort.SliceStable(ret, func(i, j int) bool { return ret[i].ID < ret[j].ID }) 457 return ret 458 } 459 460 func (s *Service) getPendingAssetUpdates() []*types.Asset { 461 s.pamu.RLock() 462 defer s.pamu.RUnlock() 463 ret := make([]*types.Asset, 0, len(s.assets)) 464 for _, a := range s.pendingAssetUpdates { 465 ret = append(ret, a.ToAssetType()) 466 } 467 sort.SliceStable(ret, func(i, j int) bool { return ret[i].ID < ret[j].ID }) 468 return ret 469 } 470 471 func (s *Service) Get(assetID string) (*Asset, error) { 472 s.amu.RLock() 473 defer s.amu.RUnlock() 474 asset, ok := s.assets[assetID] 475 if ok { 476 return asset, nil 477 } 478 s.pamu.RLock() 479 defer s.pamu.RUnlock() 480 asset, ok = s.pendingAssets[assetID] 481 if ok { 482 return asset, nil 483 } 484 return nil, ErrAssetDoesNotExist 485 } 486 487 // ValidateAssetNonValidator is only to be used by non-validators 488 // at startup when loading genesis file. We just assume assets are 489 // valid. 490 func (s *Service) ValidateAssetNonValidator(assetID string) error { 491 // get the asset to validate from the assets pool 492 asset, err := s.Get(assetID) 493 // if we get an error here, we'll never change the state of the proposal, 494 // so it will be dismissed later on by all the whole network 495 if err != nil || asset == nil { 496 s.log.Error("Validating asset, unable to get the asset", 497 logging.AssetID(assetID), 498 logging.Error(err), 499 ) 500 return errors.New("invalid asset ID") 501 } 502 503 asset.SetValid() 504 return nil 505 } 506 507 func (s *Service) ValidateAsset(assetID string) error { 508 // get the asset to validate from the assets pool 509 asset, err := s.Get(assetID) 510 // if we get an error here, we'll never change the state of the proposal, 511 // so it will be dismissed later on by all the whole network 512 if err != nil || asset == nil { 513 s.log.Error("Validating asset, unable to get the asset", 514 logging.AssetID(assetID), 515 logging.Error(err), 516 ) 517 return errors.New("invalid asset ID") 518 } 519 520 return s.validateAsset(asset) 521 } 522 523 func (s *Service) validateAsset(a *Asset) error { 524 if erc20Asset, ok := a.ERC20(); ok { 525 details := erc20Asset.Type().Details 526 bridgeView, err := s.bridgeViewByChainID(details.GetERC20().ChainID) 527 if err != nil { 528 return err 529 } 530 if err := bridgeView.FindAsset(details); err != nil { 531 return err 532 } 533 // no error, our asset exists on chain 534 erc20Asset.SetValid() 535 } 536 return nil 537 } 538 539 func (s *Service) ethClientByChainID(chainID string) (erc20.ETHClient, error) { 540 switch chainID { 541 case s.primaryEthChainID: 542 return s.primaryEthClient, nil 543 case s.secondaryEthChainID: 544 return s.secondaryEthClient, nil 545 default: 546 return nil, fmt.Errorf("chain id %q is not supported", chainID) 547 } 548 } 549 550 func (s *Service) bridgeViewByChainID(chainID string) (ERC20BridgeView, error) { 551 switch chainID { 552 case s.primaryEthChainID: 553 return s.primaryBridgeView, nil 554 case s.secondaryEthChainID: 555 return s.secondaryBridgeView, nil 556 default: 557 return nil, fmt.Errorf("chain id %q is not supported", chainID) 558 } 559 }