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  }