code.vegaprotocol.io/vega@v0.79.0/core/banking/erc20.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 banking
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  	"math/big"
    23  	"time"
    24  
    25  	"code.vegaprotocol.io/vega/core/assets/erc20"
    26  	"code.vegaprotocol.io/vega/core/events"
    27  	"code.vegaprotocol.io/vega/core/types"
    28  	"code.vegaprotocol.io/vega/libs/num"
    29  	"code.vegaprotocol.io/vega/logging"
    30  )
    31  
    32  var (
    33  	ErrInvalidWithdrawalReferenceNonce       = errors.New("invalid withdrawal reference nonce")
    34  	ErrWithdrawalAmountUnderMinimalRequired  = errors.New("invalid withdrawal, amount under minimum required")
    35  	ErrAssetAlreadyBeingListed               = errors.New("asset already being listed")
    36  	ErrWithdrawalDisabledWhenBridgeIsStopped = errors.New("cannot issue withdrawals when the bridge is stopped")
    37  )
    38  
    39  type ERC20BridgeView interface {
    40  	FindAssetList(al *types.ERC20AssetList, blockNumber, logIndex uint64, txHash string) error
    41  	FindBridgeStopped(al *types.ERC20EventBridgeStopped, blockNumber, logIndex uint64, txHash string) error
    42  	FindBridgeResumed(al *types.ERC20EventBridgeResumed, blockNumber, logIndex uint64, txHash string) error
    43  	FindDeposit(d *types.ERC20Deposit, blockNumber, logIndex uint64, ethAssetAddress string, txHash string) error
    44  	FindAssetLimitsUpdated(update *types.ERC20AssetLimitsUpdated, blockNumber uint64, logIndex uint64, ethAssetAddress string, txHash string) error
    45  	CollateralBridgeAddress() string
    46  }
    47  
    48  func (e *Engine) EnableERC20(
    49  	_ context.Context,
    50  	al *types.ERC20AssetList,
    51  	id string,
    52  	blockNumber, txIndex uint64,
    53  	txHash, chainID string,
    54  ) error {
    55  	asset, _ := e.assets.Get(al.VegaAssetID)
    56  	if _, ok := e.assetActions[al.VegaAssetID]; ok {
    57  		e.log.Error("asset already being listed", logging.AssetID(al.VegaAssetID))
    58  		return ErrAssetAlreadyBeingListed
    59  	}
    60  
    61  	bridgeView, err := e.bridgeViewForChainID(chainID)
    62  	if err != nil {
    63  		return err
    64  	}
    65  
    66  	aa := &assetAction{
    67  		id:          id,
    68  		state:       newPendingState(),
    69  		erc20AL:     al,
    70  		asset:       asset,
    71  		blockHeight: blockNumber,
    72  		logIndex:    txIndex,
    73  		txHash:      txHash,
    74  		chainID:     chainID,
    75  		bridgeView:  bridgeView,
    76  	}
    77  	e.addAction(aa)
    78  	return e.witness.StartCheck(aa, e.onCheckDone, e.timeService.GetTimeNow().Add(defaultValidationDuration))
    79  }
    80  
    81  func (e *Engine) UpdateERC20(
    82  	_ context.Context,
    83  	event *types.ERC20AssetLimitsUpdated,
    84  	id string,
    85  	blockNumber, txIndex uint64,
    86  	txHash, chainID string,
    87  ) error {
    88  	asset, err := e.assets.Get(event.VegaAssetID)
    89  	if err != nil {
    90  		e.log.Panic("couldn't retrieve the ERC20 asset",
    91  			logging.AssetID(event.VegaAssetID),
    92  		)
    93  	}
    94  
    95  	bridgeView, err := e.bridgeViewForChainID(chainID)
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	aa := &assetAction{
   101  		id:                      id,
   102  		state:                   newPendingState(),
   103  		erc20AssetLimitsUpdated: event,
   104  		asset:                   asset,
   105  		blockHeight:             blockNumber,
   106  		logIndex:                txIndex,
   107  		txHash:                  txHash,
   108  		chainID:                 chainID,
   109  		bridgeView:              bridgeView,
   110  	}
   111  	e.addAction(aa)
   112  	return e.witness.StartCheck(aa, e.onCheckDone, e.timeService.GetTimeNow().Add(defaultValidationDuration))
   113  }
   114  
   115  func (e *Engine) DepositERC20(
   116  	ctx context.Context,
   117  	d *types.ERC20Deposit,
   118  	id string,
   119  	blockNumber, logIndex uint64,
   120  	txHash, chainID string,
   121  ) error {
   122  	dep := e.newDeposit(id, d.TargetPartyID, d.VegaAssetID, d.Amount, txHash)
   123  
   124  	// check if the asset is correct
   125  	asset, err := e.assets.Get(d.VegaAssetID)
   126  	if err != nil {
   127  		dep.Status = types.DepositStatusCancelled
   128  		e.broker.Send(events.NewDepositEvent(ctx, *dep))
   129  		e.log.Error("unable to get asset by id",
   130  			logging.AssetID(d.VegaAssetID),
   131  			logging.Error(err))
   132  		return err
   133  	}
   134  
   135  	if !asset.IsERC20() {
   136  		dep.Status = types.DepositStatusCancelled
   137  		e.broker.Send(events.NewDepositEvent(ctx, *dep))
   138  		return fmt.Errorf("%v: %w", asset.String(), ErrWrongAssetTypeUsedInERC20ChainEvent)
   139  	}
   140  
   141  	bridgeView, err := e.bridgeViewForChainID(chainID)
   142  	if err != nil {
   143  		return err
   144  	}
   145  
   146  	aa := &assetAction{
   147  		id:          dep.ID,
   148  		state:       newPendingState(),
   149  		erc20D:      d,
   150  		asset:       asset,
   151  		blockHeight: blockNumber,
   152  		logIndex:    logIndex,
   153  		txHash:      txHash,
   154  		chainID:     chainID,
   155  		bridgeView:  bridgeView,
   156  	}
   157  	e.addAction(aa)
   158  	e.deposits[dep.ID] = dep
   159  
   160  	e.broker.Send(events.NewDepositEvent(ctx, *dep))
   161  	return e.witness.StartCheck(aa, e.onCheckDone, e.timeService.GetTimeNow().Add(defaultValidationDuration))
   162  }
   163  
   164  func (e *Engine) ERC20WithdrawalEvent(ctx context.Context, w *types.ERC20Withdrawal, blockNumber uint64, txHash string, chainID string) error {
   165  	// check straight away if the withdrawal is signed
   166  	nonce, ok := new(big.Int).SetString(w.ReferenceNonce, 10)
   167  	if !ok {
   168  		return fmt.Errorf("%s: %w", w.ReferenceNonce, ErrInvalidWithdrawalReferenceNonce)
   169  	}
   170  
   171  	withd, err := e.getWithdrawalFromRef(nonce)
   172  	if err != nil {
   173  		return fmt.Errorf("%s: %w", w.ReferenceNonce, err)
   174  	}
   175  	if withd.Status != types.WithdrawalStatusFinalized {
   176  		return fmt.Errorf("%s: %w", withd.ID, ErrInvalidWithdrawalState)
   177  	}
   178  	if _, ok := e.notary.IsSigned(ctx, withd.ID, types.NodeSignatureKindAssetWithdrawal); !ok {
   179  		return ErrWithdrawalNotReady
   180  	}
   181  
   182  	if e.primaryEthChainID == chainID && blockNumber > e.lastSeenPrimaryEthBlock {
   183  		e.lastSeenPrimaryEthBlock = blockNumber
   184  	} else if e.secondaryEthChainID == chainID && blockNumber > e.lastSeenSecondaryEthBlock {
   185  		e.lastSeenSecondaryEthBlock = blockNumber
   186  	}
   187  	withd.WithdrawalDate = e.timeService.GetTimeNow().UnixNano()
   188  	withd.TxHash = txHash
   189  	e.broker.Send(events.NewWithdrawalEvent(ctx, *withd))
   190  
   191  	return nil
   192  }
   193  
   194  func (e *Engine) WithdrawERC20(
   195  	ctx context.Context,
   196  	id, party, assetID string,
   197  	amount *num.Uint,
   198  	ext *types.Erc20WithdrawExt,
   199  ) error {
   200  	asset, err := e.assets.Get(assetID)
   201  	if err != nil {
   202  		e.log.Debug("unable to get asset by id",
   203  			logging.AssetID(assetID),
   204  			logging.Error(err))
   205  		return err
   206  	}
   207  
   208  	if !asset.IsERC20() {
   209  		return fmt.Errorf("asset %q is not an ERC20 token: %w", assetID, err)
   210  	}
   211  
   212  	erc20Token, _ := asset.ERC20()
   213  	bridgeState, err := e.bridgeStateForChainID(erc20Token.ChainID())
   214  	if err != nil {
   215  		return err
   216  	}
   217  
   218  	if bridgeState.IsStopped() {
   219  		return ErrWithdrawalDisabledWhenBridgeIsStopped
   220  	}
   221  
   222  	wext := &types.WithdrawExt{
   223  		Ext: &types.WithdrawExtErc20{
   224  			Erc20: ext,
   225  		},
   226  	}
   227  
   228  	w, ref := e.newWithdrawal(id, party, assetID, amount, wext)
   229  
   230  	e.broker.Send(events.NewWithdrawalEvent(ctx, *w))
   231  	e.withdrawals[w.ID] = withdrawalRef{w: w, ref: ref}
   232  
   233  	// check for minimal amount reached
   234  	quantum := asset.Type().Details.Quantum
   235  	// no reason this would produce an error
   236  	minAmount, _ := num.UintFromDecimal(quantum.Mul(e.minWithdrawQuantumMultiple))
   237  
   238  	// now verify amount
   239  	if amount.LT(minAmount) {
   240  		e.log.Debug("cannot withdraw funds, the request is less than minimum withdrawal amount",
   241  			logging.BigUint("min-amount", minAmount),
   242  			logging.BigUint("requested-amount", amount),
   243  		)
   244  		w.Status = types.WithdrawalStatusRejected
   245  		e.broker.Send(events.NewWithdrawalEvent(ctx, *w))
   246  		return ErrWithdrawalAmountUnderMinimalRequired
   247  	}
   248  
   249  	if a, ok := asset.ERC20(); !ok {
   250  		w.Status = types.WithdrawalStatusRejected
   251  		e.broker.Send(events.NewWithdrawalEvent(ctx, *w))
   252  		return ErrWrongAssetUsedForERC20Withdraw
   253  	} else if threshold := a.Type().Details.GetERC20().WithdrawThreshold; threshold != nil && threshold.NEQ(num.UintZero()) {
   254  		// a delay will be applied on this withdrawal
   255  		if threshold.LT(amount) {
   256  			e.log.Debug("withdraw threshold breached, delay will be applied",
   257  				logging.PartyID(party),
   258  				logging.BigUint("threshold", threshold),
   259  				logging.BigUint("amount", amount),
   260  				logging.AssetID(assetID),
   261  				logging.Error(err))
   262  		}
   263  	}
   264  
   265  	// try to withdraw if no error, this'll just abort
   266  	if err := e.finalizeWithdraw(ctx, w); err != nil {
   267  		return err
   268  	}
   269  
   270  	// no check error as we checked earlier we had an erc20 asset.
   271  	erc20asset, _ := asset.ERC20()
   272  
   273  	// startup aggregating signature for the bundle
   274  	return e.startERC20Signatures(w, erc20asset, ref)
   275  }
   276  
   277  func (e *Engine) startERC20Signatures(w *types.Withdrawal, asset *erc20.ERC20, ref *big.Int) error {
   278  	var (
   279  		signature []byte
   280  		err       error
   281  	)
   282  
   283  	creation := time.Unix(0, w.CreationDate)
   284  	// if we are a validator, we want to build a signature
   285  	if e.top.IsValidator() {
   286  		_, signature, err = asset.SignWithdrawal(
   287  			w.Amount, w.Ext.GetErc20().GetReceiverAddress(), ref, creation)
   288  		if err != nil {
   289  			// there's no reason we cannot build the signature here
   290  			// apart if the node isn't configure properly
   291  			e.log.Panic("unable to sign withdrawal",
   292  				logging.WithdrawalID(w.ID),
   293  				logging.PartyID(w.PartyID),
   294  				logging.AssetID(w.Asset),
   295  				logging.BigUint("amount", w.Amount),
   296  				logging.Error(err))
   297  		}
   298  	}
   299  
   300  	// we were able to lock the funds, then we can send the vote through the network
   301  	e.notary.StartAggregate(w.ID, types.NodeSignatureKindAssetWithdrawal, signature)
   302  
   303  	return nil
   304  }
   305  
   306  func (e *Engine) offerERC20NotarySignatures(resource string) []byte {
   307  	if !e.top.IsValidator() {
   308  		return nil
   309  	}
   310  
   311  	wref, ok := e.withdrawals[resource]
   312  	if !ok {
   313  		// there's no reason we cannot find the withdrawal here
   314  		// apart if the node isn't configured properly
   315  		e.log.Panic("unable to find withdrawal",
   316  			logging.WithdrawalID(resource))
   317  	}
   318  	w := wref.w
   319  
   320  	asset, err := e.assets.Get(w.Asset)
   321  	if err != nil {
   322  		// there's no reason we cannot build the signature here
   323  		// apart if the node isn't configure properly
   324  		e.log.Panic("unable to get asset when offering signature",
   325  			logging.WithdrawalID(w.ID),
   326  			logging.PartyID(w.PartyID),
   327  			logging.AssetID(w.Asset),
   328  			logging.BigUint("amount", w.Amount),
   329  			logging.Error(err))
   330  	}
   331  
   332  	creation := time.Unix(0, w.CreationDate)
   333  	erc20asset, _ := asset.ERC20()
   334  	_, signature, err := erc20asset.SignWithdrawal(
   335  		w.Amount, w.Ext.GetErc20().GetReceiverAddress(), wref.ref, creation)
   336  	if err != nil {
   337  		// there's no reason we cannot build the signature here
   338  		// apart if the node isn't configure properly
   339  		e.log.Panic("unable to sign withdrawal",
   340  			logging.WithdrawalID(w.ID),
   341  			logging.PartyID(w.PartyID),
   342  			logging.AssetID(w.Asset),
   343  			logging.BigUint("amount", w.Amount),
   344  			logging.Error(err))
   345  	}
   346  
   347  	return signature
   348  }
   349  
   350  func (e *Engine) addAction(aa *assetAction) {
   351  	e.assetActions[aa.id] = aa
   352  }