code.vegaprotocol.io/vega@v0.79.0/core/bridges/erc20_logic_view.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 bridges 17 18 import ( 19 "context" 20 "encoding/hex" 21 "errors" 22 "fmt" 23 "math/big" 24 "net/http" 25 "strconv" 26 "strings" 27 "time" 28 29 erc20contract "code.vegaprotocol.io/vega/core/contracts/erc20" 30 bridgecontract "code.vegaprotocol.io/vega/core/contracts/erc20_bridge_logic_restricted" 31 "code.vegaprotocol.io/vega/core/metrics" 32 "code.vegaprotocol.io/vega/core/types" 33 vgerrors "code.vegaprotocol.io/vega/libs/errors" 34 "code.vegaprotocol.io/vega/libs/num" 35 36 "github.com/ethereum/go-ethereum" 37 "github.com/ethereum/go-ethereum/accounts/abi/bind" 38 ethcommon "github.com/ethereum/go-ethereum/common" 39 ethtypes "github.com/ethereum/go-ethereum/core/types" 40 ) 41 42 var ( 43 ErrNotAnERC20Asset = errors.New("not an erc20 asset") 44 ErrUnableToFindERC20AssetList = errors.New("unable to find erc20 asset list event") 45 ErrUnableToFindERC20BridgeStopped = errors.New("unable to find erc20 bridge stopped event") 46 ErrUnableToFindERC20BridgeResumed = errors.New("unable to find erc20 bridge resumed event") 47 ErrUnableToFindERC20Deposit = errors.New("unable to find erc20 asset deposit") 48 ErrUnableToFindERC20Withdrawal = errors.New("unabled to find erc20 asset withdrawal") 49 ErrUnableToFindERC20AssetLimitsUpdated = errors.New("unable to find erc20 asset limits update event") 50 ) 51 52 //go:generate go run github.com/golang/mock/mockgen -destination mocks/eth_client_mock.go -package mocks code.vegaprotocol.io/vega/core/bridges ETHClient 53 type ETHClient interface { 54 bind.ContractBackend 55 ethereum.ChainReader 56 HeaderByNumber(context.Context, *big.Int) (*ethtypes.Header, error) 57 CollateralBridgeAddress() ethcommon.Address 58 CurrentHeight(context.Context) (uint64, error) 59 ConfirmationsRequired() uint64 60 } 61 62 //go:generate go run github.com/golang/mock/mockgen -destination mocks/eth_confirmations_mock.go -package mocks code.vegaprotocol.io/vega/core/bridges EthConfirmations 63 type EthConfirmations interface { 64 Check(uint64) error 65 } 66 67 type ERC20LogicView struct { 68 clt ETHClient 69 ethConfs EthConfirmations 70 } 71 72 func NewERC20LogicView( 73 clt ETHClient, 74 ethConfs EthConfirmations, 75 ) *ERC20LogicView { 76 return &ERC20LogicView{ 77 clt: clt, 78 ethConfs: ethConfs, 79 } 80 } 81 82 func (e *ERC20LogicView) CollateralBridgeAddress() string { 83 return e.clt.CollateralBridgeAddress().Hex() 84 } 85 86 // FindAsset will try to find an asset and validate it's details on ethereum. 87 func (e *ERC20LogicView) FindAsset( 88 asset *types.AssetDetails, 89 ) error { 90 source := asset.GetERC20() 91 if source == nil { 92 return ErrNotAnERC20Asset 93 } 94 95 t, err := erc20contract.NewErc20(ethcommon.HexToAddress(source.ContractAddress), e.clt) 96 if err != nil { 97 return err 98 } 99 100 validationErrs := vgerrors.NewCumulatedErrors() 101 102 if name, err := t.Name(&bind.CallOpts{}); err != nil { 103 validationErrs.Add(fmt.Errorf("couldn't get name: %w", err)) 104 } else if name != asset.Name { 105 validationErrs.Add(fmt.Errorf("invalid name, contract(%s), proposal(%s)", name, asset.Name)) 106 } 107 108 if symbol, err := t.Symbol(&bind.CallOpts{}); err != nil { 109 validationErrs.Add(fmt.Errorf("couldn't get symbol: %w", err)) 110 } else if symbol != asset.Symbol { 111 validationErrs.Add(fmt.Errorf("invalid symbol, contract(%s), proposal(%s)", symbol, asset.Symbol)) 112 } 113 114 if decimals, err := t.Decimals(&bind.CallOpts{}); err != nil { 115 validationErrs.Add(fmt.Errorf("couldn't get decimals: %w", err)) 116 } else if uint64(decimals) != asset.Decimals { 117 validationErrs.Add(fmt.Errorf("invalid decimals, contract(%d), proposal(%d)", decimals, asset.Decimals)) 118 } 119 120 if validationErrs.HasAny() { 121 return validationErrs 122 } 123 124 return nil 125 } 126 127 // FindAssetList will look at the ethereum logs and try to find the 128 // given transaction. 129 func (e *ERC20LogicView) FindAssetList( 130 al *types.ERC20AssetList, 131 blockNumber, 132 logIndex uint64, 133 txHash string, 134 ) error { 135 bf, err := bridgecontract.NewErc20BridgeLogicRestrictedFilterer( 136 e.clt.CollateralBridgeAddress(), e.clt) 137 if err != nil { 138 return err 139 } 140 141 resp := "ok" 142 defer func() { 143 metrics.EthCallInc("find_asset_list", al.VegaAssetID, resp) 144 }() 145 146 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 147 defer cancel() 148 iter, err := bf.FilterAssetListed( 149 &bind.FilterOpts{ 150 Start: blockNumber - 1, 151 End: &blockNumber, 152 Context: ctx, 153 }, 154 []ethcommon.Address{ethcommon.HexToAddress(al.AssetSource)}, 155 [][32]byte{}, 156 ) 157 if err != nil { 158 resp = getMaybeHTTPStatus(err) 159 return err 160 } 161 defer iter.Close() 162 163 var event *bridgecontract.Erc20BridgeLogicRestrictedAssetListed 164 assetID := strings.TrimPrefix(al.VegaAssetID, "0x") 165 166 for iter.Next() { 167 if !iter.Event.Raw.Removed && 168 hex.EncodeToString(iter.Event.VegaAssetId[:]) == assetID && 169 iter.Event.Raw.BlockNumber == blockNumber && 170 uint64(iter.Event.Raw.Index) == logIndex && 171 iter.Event.Raw.TxHash.Hex() == txHash { 172 event = iter.Event 173 174 break 175 } 176 } 177 178 if event == nil { 179 return ErrUnableToFindERC20AssetList 180 } 181 182 // now ensure we have enough confirmations 183 if err := e.ethConfs.Check(event.Raw.BlockNumber); err != nil { 184 return err 185 } 186 187 return nil 188 } 189 190 // FindBridgeStopped will look at the ethereum logs and try to find the 191 // given transaction. 192 func (e *ERC20LogicView) FindBridgeStopped( 193 al *types.ERC20EventBridgeStopped, 194 blockNumber, 195 logIndex uint64, 196 txHash string, 197 ) error { 198 bf, err := bridgecontract.NewErc20BridgeLogicRestrictedFilterer( 199 e.clt.CollateralBridgeAddress(), e.clt) 200 if err != nil { 201 return err 202 } 203 204 resp := "ok" 205 defer func() { 206 metrics.EthCallInc("find_bridge_stopped", "", resp) 207 }() 208 209 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 210 defer cancel() 211 iter, err := bf.FilterBridgeStopped( 212 &bind.FilterOpts{ 213 Start: blockNumber - 1, 214 End: &blockNumber, 215 Context: ctx, 216 }, 217 ) 218 if err != nil { 219 resp = getMaybeHTTPStatus(err) 220 return err 221 } 222 defer iter.Close() 223 224 var event *bridgecontract.Erc20BridgeLogicRestrictedBridgeStopped 225 226 for iter.Next() { 227 if !iter.Event.Raw.Removed && 228 iter.Event.Raw.BlockNumber == blockNumber && 229 uint64(iter.Event.Raw.Index) == logIndex && 230 iter.Event.Raw.TxHash.Hex() == txHash { 231 event = iter.Event 232 233 break 234 } 235 } 236 237 if event == nil { 238 return ErrUnableToFindERC20BridgeStopped 239 } 240 241 // now ensure we have enough confirmations 242 if err := e.ethConfs.Check(event.Raw.BlockNumber); err != nil { 243 return err 244 } 245 246 return nil 247 } 248 249 // FindBridgeResumed will look at the ethereum logs and try to find the 250 // given transaction. 251 func (e *ERC20LogicView) FindBridgeResumed( 252 al *types.ERC20EventBridgeResumed, 253 blockNumber, 254 logIndex uint64, 255 txHash string, 256 ) error { 257 bf, err := bridgecontract.NewErc20BridgeLogicRestrictedFilterer( 258 e.clt.CollateralBridgeAddress(), e.clt) 259 if err != nil { 260 return err 261 } 262 263 resp := "ok" 264 defer func() { 265 metrics.EthCallInc("find_bridge_stopped", "", resp) 266 }() 267 268 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 269 defer cancel() 270 iter, err := bf.FilterBridgeResumed( 271 &bind.FilterOpts{ 272 Start: blockNumber - 1, 273 End: &blockNumber, 274 Context: ctx, 275 }, 276 ) 277 if err != nil { 278 resp = getMaybeHTTPStatus(err) 279 return err 280 } 281 defer iter.Close() 282 283 var event *bridgecontract.Erc20BridgeLogicRestrictedBridgeResumed 284 285 for iter.Next() { 286 if !iter.Event.Raw.Removed && 287 iter.Event.Raw.BlockNumber == blockNumber && 288 uint64(iter.Event.Raw.Index) == logIndex && 289 iter.Event.Raw.TxHash.Hex() == txHash { 290 event = iter.Event 291 292 break 293 } 294 } 295 296 if event == nil { 297 return ErrUnableToFindERC20BridgeStopped 298 } 299 300 // now ensure we have enough confirmations 301 if err := e.ethConfs.Check(event.Raw.BlockNumber); err != nil { 302 return err 303 } 304 305 return nil 306 } 307 308 func (e *ERC20LogicView) FindDeposit( 309 d *types.ERC20Deposit, 310 blockNumber, logIndex uint64, 311 ethAssetAddress string, 312 txHash string, 313 ) error { 314 bf, err := bridgecontract.NewErc20BridgeLogicRestrictedFilterer( 315 e.clt.CollateralBridgeAddress(), e.clt) 316 if err != nil { 317 return err 318 } 319 320 resp := "ok" 321 defer func() { 322 metrics.EthCallInc("find_deposit", d.VegaAssetID, resp) 323 }() 324 325 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 326 defer cancel() 327 iter, err := bf.FilterAssetDeposited( 328 &bind.FilterOpts{ 329 Start: blockNumber - 1, 330 End: &blockNumber, 331 Context: ctx, 332 }, 333 // user_address 334 []ethcommon.Address{ethcommon.HexToAddress(d.SourceEthereumAddress)}, 335 // asset_source 336 []ethcommon.Address{ethcommon.HexToAddress(ethAssetAddress)}) 337 if err != nil { 338 resp = getMaybeHTTPStatus(err) 339 return err 340 } 341 defer iter.Close() 342 343 depamount := d.Amount.BigInt() 344 var event *bridgecontract.Erc20BridgeLogicRestrictedAssetDeposited 345 targetPartyID := strings.TrimPrefix(d.TargetPartyID, "0x") 346 347 for iter.Next() { 348 if !iter.Event.Raw.Removed && 349 hex.EncodeToString(iter.Event.VegaPublicKey[:]) == targetPartyID && 350 iter.Event.Amount.Cmp(depamount) == 0 && 351 iter.Event.Raw.BlockNumber == blockNumber && 352 uint64(iter.Event.Raw.Index) == logIndex && 353 iter.Event.Raw.TxHash.Hex() == txHash { 354 event = iter.Event 355 break 356 } 357 } 358 359 if event == nil { 360 return ErrUnableToFindERC20Deposit 361 } 362 363 // now ensure we have enough confirmations 364 if err := e.ethConfs.Check(event.Raw.BlockNumber); err != nil { 365 return err 366 } 367 368 return nil 369 } 370 371 func (e *ERC20LogicView) FindWithdrawal( 372 w *types.ERC20Withdrawal, 373 blockNumber, logIndex uint64, 374 ethAssetAddress string, 375 txHash string, 376 ) (*big.Int, string, uint, error) { 377 bf, err := bridgecontract.NewErc20BridgeLogicRestrictedFilterer( 378 e.clt.CollateralBridgeAddress(), e.clt) 379 if err != nil { 380 return nil, "", 0, err 381 } 382 383 resp := "ok" 384 defer func() { 385 metrics.EthCallInc("find_withdrawal", w.VegaAssetID, resp) 386 }() 387 388 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 389 defer cancel() 390 iter, err := bf.FilterAssetWithdrawn( 391 &bind.FilterOpts{ 392 Start: blockNumber - 1, 393 End: &blockNumber, 394 Context: ctx, 395 }, 396 // user_address 397 []ethcommon.Address{ethcommon.HexToAddress(w.TargetEthereumAddress)}, 398 // asset_source 399 []ethcommon.Address{ethcommon.HexToAddress(ethAssetAddress)}) 400 if err != nil { 401 resp = getMaybeHTTPStatus(err) 402 return nil, "", 0, err 403 } 404 defer iter.Close() 405 406 var event *bridgecontract.Erc20BridgeLogicRestrictedAssetWithdrawn 407 nonce := &big.Int{} 408 _, ok := nonce.SetString(w.ReferenceNonce, 10) 409 if !ok { 410 return nil, "", 0, fmt.Errorf("could not use reference nonce, expected base 10 integer: %v", w.ReferenceNonce) 411 } 412 413 for iter.Next() { 414 if !iter.Event.Raw.Removed && 415 nonce.Cmp(iter.Event.Nonce) == 0 && 416 iter.Event.Raw.BlockNumber == blockNumber && 417 uint64(iter.Event.Raw.Index) == logIndex && 418 iter.Event.Raw.TxHash.Hex() == txHash { 419 event = iter.Event 420 421 break 422 } 423 } 424 425 if event == nil { 426 return nil, "", 0, ErrUnableToFindERC20Withdrawal 427 } 428 429 // now ensure we have enough confirmations 430 if err := e.ethConfs.Check(event.Raw.BlockNumber); err != nil { 431 return nil, "", 0, err 432 } 433 434 return nonce, event.Raw.TxHash.Hex(), event.Raw.Index, nil 435 } 436 437 func (e *ERC20LogicView) FindAssetLimitsUpdated( 438 update *types.ERC20AssetLimitsUpdated, 439 blockNumber uint64, logIndex uint64, 440 ethAssetAddress string, 441 txHash string, 442 ) error { 443 bf, err := bridgecontract.NewErc20BridgeLogicRestrictedFilterer( 444 e.clt.CollateralBridgeAddress(), e.clt) 445 if err != nil { 446 return err 447 } 448 449 resp := "ok" 450 defer func() { 451 metrics.EthCallInc("find_asset_limits_updated", update.VegaAssetID, resp) 452 }() 453 454 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 455 defer cancel() 456 iter, err := bf.FilterAssetLimitsUpdated( 457 &bind.FilterOpts{ 458 Start: blockNumber - 1, 459 End: &blockNumber, 460 Context: ctx, 461 }, 462 []ethcommon.Address{ethcommon.HexToAddress(ethAssetAddress)}, 463 ) 464 if err != nil { 465 resp = getMaybeHTTPStatus(err) 466 return err 467 } 468 defer iter.Close() 469 470 var event *bridgecontract.Erc20BridgeLogicRestrictedAssetLimitsUpdated 471 472 for iter.Next() { 473 eventLifetimeLimit, _ := num.UintFromBig(iter.Event.LifetimeLimit) 474 eventWithdrawThreshold, _ := num.UintFromBig(iter.Event.WithdrawThreshold) 475 if !iter.Event.Raw.Removed && 476 update.LifetimeLimits.EQ(eventLifetimeLimit) && 477 update.WithdrawThreshold.EQ(eventWithdrawThreshold) && 478 iter.Event.Raw.BlockNumber == blockNumber && 479 uint64(iter.Event.Raw.Index) == logIndex && 480 iter.Event.Raw.TxHash.Hex() == txHash { 481 event = iter.Event 482 break 483 } 484 } 485 486 if event == nil { 487 return ErrUnableToFindERC20AssetLimitsUpdated 488 } 489 490 // now ensure we have enough confirmations 491 if err := e.ethConfs.Check(event.Raw.BlockNumber); err != nil { 492 return err 493 } 494 495 return nil 496 } 497 498 func getMaybeHTTPStatus(err error) string { 499 errstr := err.Error() 500 if len(errstr) < 3 { 501 return "tooshort" 502 } 503 i, err := strconv.Atoi(errstr[:3]) 504 if err != nil { 505 return "nan" 506 } 507 if http.StatusText(i) == "" { 508 return "unknown" 509 } 510 511 return errstr[:3] 512 }