decred.org/dcrdex@v1.0.5/client/asset/eth/deploy.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package eth
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"math/big"
    10  	"os"
    11  	"strings"
    12  
    13  	"decred.org/dcrdex/client/asset"
    14  	"decred.org/dcrdex/dex"
    15  	erc20v0 "decred.org/dcrdex/dex/networks/erc20/contracts/v0"
    16  	dexeth "decred.org/dcrdex/dex/networks/eth"
    17  	multibal "decred.org/dcrdex/dex/networks/eth/contracts/multibalance"
    18  	ethv0 "decred.org/dcrdex/dex/networks/eth/contracts/v0"
    19  	"github.com/ethereum/go-ethereum"
    20  	"github.com/ethereum/go-ethereum/accounts/abi"
    21  	"github.com/ethereum/go-ethereum/accounts/abi/bind"
    22  	"github.com/ethereum/go-ethereum/common"
    23  	"github.com/ethereum/go-ethereum/core/types"
    24  	"github.com/ethereum/go-ethereum/params"
    25  )
    26  
    27  // contractDeployer deploys a dcrdex swap contract for an evm-compatible
    28  // blockchain. contractDeployer can deploy both base asset contracts or ERC20
    29  // contracts. contractDeployer is used by the cmd/deploy/deploy utility.
    30  type contractDeployer byte
    31  
    32  var ContractDeployer contractDeployer
    33  
    34  // EstimateDeployFunding estimates the fees required to deploy a contract.
    35  // The gas estimate is only accurate if sufficient funds are in the wallet (so
    36  // that estimateGas succeeds), otherwise a generously-padded estimate is
    37  // generated.
    38  func (contractDeployer) EstimateDeployFunding(
    39  	ctx context.Context,
    40  	chain string,
    41  	contractVer uint32,
    42  	tokenAddress common.Address,
    43  	credentialsPath string,
    44  	chainCfg *params.ChainConfig,
    45  	ui *dex.UnitInfo,
    46  	log dex.Logger,
    47  	net dex.Network,
    48  ) error {
    49  	txData, err := ContractDeployer.txData(contractVer, tokenAddress)
    50  	if err != nil {
    51  		return err
    52  	}
    53  	const deploymentGas = 1_000_000 // eth v0: 687_671, token v0 825_478
    54  	return ContractDeployer.estimateDeployFunding(ctx, txData, deploymentGas, chain, credentialsPath, chainCfg, ui, log, net)
    55  }
    56  
    57  func (contractDeployer) estimateDeployFunding(
    58  	ctx context.Context,
    59  	txData []byte,
    60  	deploymentGas uint64,
    61  	chain string,
    62  	credentialsPath string,
    63  	chainCfg *params.ChainConfig,
    64  	ui *dex.UnitInfo,
    65  	log dex.Logger,
    66  	net dex.Network,
    67  ) error {
    68  
    69  	walletDir, err := os.MkdirTemp("", "")
    70  	if err != nil {
    71  		return err
    72  	}
    73  	defer os.RemoveAll(walletDir)
    74  
    75  	cl, maxFeeRate, _, err := ContractDeployer.nodeAndRate(ctx, chain, walletDir, credentialsPath, chainCfg, log, net)
    76  	if err != nil {
    77  		return err
    78  	}
    79  	defer cl.shutdown()
    80  
    81  	log.Infof("Address: %s", cl.address())
    82  
    83  	baseChainBal, err := cl.addressBalance(ctx, cl.address())
    84  	if err != nil {
    85  		return fmt.Errorf("error getting eth balance: %v", err)
    86  	}
    87  
    88  	log.Infof("Balance: %s %s", ui.ConventionalString(dexeth.WeiToGwei(baseChainBal)), ui.Conventional.Unit)
    89  
    90  	var gas uint64
    91  	if baseChainBal.Cmp(new(big.Int)) > 0 {
    92  		// We may be able to get a proper estimate.
    93  		gas, err = cl.EstimateGas(ctx, ethereum.CallMsg{
    94  			From: cl.creds.addr,
    95  			To:   nil, // special value means deploy contract
    96  			Data: txData,
    97  		})
    98  		gas = gas * 5 / 4 // 20% buffer on gas
    99  		if err != nil {
   100  			log.Debugf("EstimateGas error: %v", err)
   101  			log.Info("Unable to get on-chain estimate. balance probably too low. Falling back to rough estimate")
   102  		}
   103  	}
   104  
   105  	feeRate := dexeth.WeiToGweiCeil(maxFeeRate)
   106  	if gas == 0 {
   107  		gas = deploymentGas
   108  	}
   109  	fees := feeRate * gas
   110  
   111  	log.Infof("Estimated fees: %s", ui.ConventionalString(fees))
   112  
   113  	gweiBal := dexeth.WeiToGwei(baseChainBal)
   114  	if fees < gweiBal {
   115  		log.Infof("👍 Current balance (%s %s) sufficient for fees (%s)",
   116  			ui.ConventionalString(gweiBal), ui.Conventional.Unit, ui.ConventionalString(fees))
   117  		return nil
   118  	}
   119  
   120  	shortage := fees - gweiBal
   121  	log.Infof("❌ Current balance (%[1]s %[2]s) insufficient for fees (%[3]s). Send %[4]s %[2]s to %[5]s",
   122  		ui.ConventionalString(gweiBal), ui.Conventional.Unit, ui.ConventionalString(fees),
   123  		ui.ConventionalString(shortage), cl.address())
   124  
   125  	return nil
   126  }
   127  
   128  func (contractDeployer) EstimateMultiBalanceDeployFunding(
   129  	ctx context.Context,
   130  	chain string,
   131  	credentialsPath string,
   132  	chainCfg *params.ChainConfig,
   133  	ui *dex.UnitInfo,
   134  	log dex.Logger,
   135  	net dex.Network,
   136  ) error {
   137  	const deploymentGas = 400_000 // 302_647 for https://goerli.etherscan.io/tx/0x540d3e82888b18f89566a988712a7c2ecd45bd2df472f8dd689e319ae9fa4445
   138  	txData := common.FromHex(multibal.MultiBalanceV0MetaData.Bin)
   139  	return ContractDeployer.estimateDeployFunding(ctx, txData, deploymentGas, chain, credentialsPath, chainCfg, ui, log, net)
   140  }
   141  
   142  func (contractDeployer) txData(contractVer uint32, tokenAddr common.Address) (txData []byte, err error) {
   143  	var abi *abi.ABI
   144  	var bytecode []byte
   145  	isToken := tokenAddr != (common.Address{})
   146  	if isToken {
   147  		switch contractVer {
   148  		case 0:
   149  			bytecode = common.FromHex(erc20v0.ERC20SwapBin)
   150  			abi, err = erc20v0.ERC20SwapMetaData.GetAbi()
   151  		}
   152  	} else {
   153  		switch contractVer {
   154  		case 0:
   155  			bytecode = common.FromHex(ethv0.ETHSwapBin)
   156  			abi, err = ethv0.ETHSwapMetaData.GetAbi()
   157  		}
   158  	}
   159  	if err != nil {
   160  		return nil, fmt.Errorf("error parsing ABI: %w", err)
   161  	}
   162  	if abi == nil {
   163  		return nil, fmt.Errorf("no abi data for version %d", contractVer)
   164  	}
   165  	txData = bytecode
   166  	if isToken {
   167  		argData, err := abi.Pack("", tokenAddr)
   168  		if err != nil {
   169  			return nil, fmt.Errorf("error packing token address: %w", err)
   170  		}
   171  		txData = append(txData, argData...)
   172  	}
   173  	return
   174  }
   175  
   176  // DeployContract deployes a dcrdex swap contract.
   177  func (contractDeployer) DeployContract(
   178  	ctx context.Context,
   179  	chain string,
   180  	contractVer uint32,
   181  	tokenAddress common.Address,
   182  	credentialsPath string,
   183  	chainCfg *params.ChainConfig,
   184  	ui *dex.UnitInfo,
   185  	log dex.Logger,
   186  	net dex.Network,
   187  ) error {
   188  	txData, err := ContractDeployer.txData(contractVer, tokenAddress)
   189  	if err != nil {
   190  		return err
   191  	}
   192  
   193  	var deployer deployerFunc
   194  	isToken := tokenAddress != (common.Address{})
   195  	if isToken {
   196  		switch contractVer {
   197  		case 0:
   198  			deployer = func(txOpts *bind.TransactOpts, cb bind.ContractBackend) (common.Address, *types.Transaction, error) {
   199  				contractAddr, tx, _, err := erc20v0.DeployERC20Swap(txOpts, cb, tokenAddress)
   200  				return contractAddr, tx, err
   201  			}
   202  
   203  		}
   204  	} else {
   205  		switch contractVer {
   206  		case 0:
   207  			deployer = func(txOpts *bind.TransactOpts, cb bind.ContractBackend) (common.Address, *types.Transaction, error) {
   208  				contractAddr, tx, _, err := ethv0.DeployETHSwap(txOpts, cb)
   209  				return contractAddr, tx, err
   210  			}
   211  		}
   212  	}
   213  	if deployer == nil {
   214  		return fmt.Errorf("contract version unknown")
   215  	}
   216  
   217  	return ContractDeployer.deployContract(ctx, txData, deployer, chain, credentialsPath, chainCfg, ui, log, net)
   218  }
   219  
   220  type deployerFunc func(txOpts *bind.TransactOpts, cb bind.ContractBackend) (common.Address, *types.Transaction, error)
   221  
   222  // DeployContract deployes a dcrdex swap contract.
   223  func (contractDeployer) deployContract(
   224  	ctx context.Context,
   225  	txData []byte,
   226  	deployer deployerFunc,
   227  	chain string,
   228  	credentialsPath string,
   229  	chainCfg *params.ChainConfig,
   230  	ui *dex.UnitInfo,
   231  	log dex.Logger,
   232  	net dex.Network,
   233  ) error {
   234  
   235  	walletDir, err := os.MkdirTemp("", "")
   236  	if err != nil {
   237  		return err
   238  	}
   239  	defer os.RemoveAll(walletDir)
   240  
   241  	cl, maxFeeRate, tipRate, err := ContractDeployer.nodeAndRate(ctx, chain, walletDir, credentialsPath, chainCfg, log, net)
   242  	if err != nil {
   243  		return err
   244  	}
   245  	defer cl.shutdown()
   246  
   247  	log.Infof("Address: %s", cl.address())
   248  
   249  	baseChainBal, err := cl.addressBalance(ctx, cl.address())
   250  	if err != nil {
   251  		return fmt.Errorf("error getting eth balance: %v", err)
   252  	}
   253  
   254  	log.Infof("Balance: %s %s", ui.ConventionalString(dexeth.WeiToGwei(baseChainBal)), ui.Conventional.Unit)
   255  
   256  	// We may be able to get a proper estimate.
   257  	gas, err := cl.EstimateGas(ctx, ethereum.CallMsg{
   258  		From: cl.address(),
   259  		To:   nil, // special value means deploy contract
   260  		Data: txData,
   261  	})
   262  	if err != nil {
   263  		return fmt.Errorf("EstimateGas error: %v", err)
   264  	}
   265  
   266  	feeRate := dexeth.WeiToGweiCeil(maxFeeRate)
   267  	log.Infof("Estimated fees: %s gwei / gas", ui.ConventionalString(feeRate*gas))
   268  
   269  	gas *= 5 / 4 // Add 20% buffer
   270  	feesWithBuffer := feeRate * gas
   271  
   272  	gweiBal := dexeth.WeiToGwei(baseChainBal)
   273  	if feesWithBuffer >= gweiBal {
   274  		shortage := feesWithBuffer - gweiBal
   275  		return fmt.Errorf("❌ Current balance (%[1]s %[2]s) insufficient for fees (%[3]s). Send %[4]s %[2]s to %[5]s",
   276  			ui.ConventionalString(gweiBal), ui.Conventional.Unit, ui.ConventionalString(feesWithBuffer),
   277  			ui.ConventionalString(shortage), cl.address())
   278  	}
   279  
   280  	txOpts, err := cl.txOpts(ctx, 0, gas, dexeth.GweiToWei(feeRate), tipRate, nil)
   281  	if err != nil {
   282  		return err
   283  	}
   284  
   285  	contractAddr, tx, err := deployer(txOpts, cl.contractBackend())
   286  	if err != nil {
   287  		return err
   288  	}
   289  
   290  	log.Infof("👍 Contract %s launched with tx %s", contractAddr, tx.Hash())
   291  
   292  	return nil
   293  }
   294  
   295  // ReturnETH returns the remaining base asset balance from the deployment/getgas
   296  // wallet to the specified return address.
   297  func (contractDeployer) ReturnETH(
   298  	ctx context.Context,
   299  	chain string,
   300  	returnAddr common.Address,
   301  	credentialsPath string,
   302  	chainCfg *params.ChainConfig,
   303  	ui *dex.UnitInfo,
   304  	log dex.Logger,
   305  	net dex.Network,
   306  ) error {
   307  
   308  	walletDir, err := os.MkdirTemp("", "")
   309  	if err != nil {
   310  		return err
   311  	}
   312  	defer os.RemoveAll(walletDir)
   313  
   314  	cl, maxFeeRate, tipRate, err := ContractDeployer.nodeAndRate(ctx, chain, walletDir, credentialsPath, chainCfg, log, net)
   315  	if err != nil {
   316  		return err
   317  	}
   318  	defer cl.shutdown()
   319  
   320  	return GetGas.returnFunds(ctx, cl, maxFeeRate, tipRate, returnAddr, nil, ui, log, net)
   321  }
   322  
   323  func (contractDeployer) nodeAndRate(
   324  	ctx context.Context,
   325  	chain string,
   326  	walletDir,
   327  	credentialsPath string,
   328  
   329  	chainCfg *params.ChainConfig,
   330  	log dex.Logger,
   331  	net dex.Network,
   332  ) (*multiRPCClient, *big.Int, *big.Int, error) {
   333  
   334  	seed, providers, err := getFileCredentials(chain, credentialsPath, net)
   335  	if err != nil {
   336  		return nil, nil, nil, err
   337  	}
   338  
   339  	pw := []byte("abc")
   340  	chainID := chainCfg.ChainID.Int64()
   341  
   342  	if err := CreateEVMWallet(chainID, &asset.CreateWalletParams{
   343  		Type:     walletTypeRPC,
   344  		Seed:     seed,
   345  		Pass:     pw,
   346  		Settings: map[string]string{providersKey: strings.Join(providers, " ")},
   347  		DataDir:  walletDir,
   348  		Net:      net,
   349  		Logger:   log,
   350  	}, nil /* we don't need the full api, skipConnect = true allows for nil compat */, true); err != nil {
   351  		return nil, nil, nil, fmt.Errorf("error creating wallet: %w", err)
   352  	}
   353  
   354  	cl, err := newMultiRPCClient(walletDir, providers, log, chainCfg, 3, net)
   355  	if err != nil {
   356  		return nil, nil, nil, fmt.Errorf("error creating rpc client: %w", err)
   357  	}
   358  
   359  	if err := cl.unlock(string(pw)); err != nil {
   360  		return nil, nil, nil, fmt.Errorf("error unlocking rpc client: %w", err)
   361  	}
   362  
   363  	if err = cl.connect(ctx); err != nil {
   364  		return nil, nil, nil, fmt.Errorf("error connecting: %w", err)
   365  	}
   366  
   367  	baseRate, tipRate, err := cl.currentFees(ctx)
   368  	if err != nil {
   369  		cl.shutdown()
   370  		return nil, nil, nil, fmt.Errorf("Error estimating fee rate: %v", err)
   371  	}
   372  
   373  	maxFeeRate := new(big.Int).Add(tipRate, new(big.Int).Mul(baseRate, big.NewInt(2)))
   374  	return cl, maxFeeRate, tipRate, nil
   375  }
   376  
   377  // DeployMultiBalance deployes a contract with a function for reading all
   378  // balances in one call.
   379  func (contractDeployer) DeployMultiBalance(
   380  	ctx context.Context,
   381  	chain string,
   382  	credentialsPath string,
   383  	chainCfg *params.ChainConfig,
   384  	ui *dex.UnitInfo,
   385  	log dex.Logger,
   386  	net dex.Network,
   387  ) error {
   388  	txData := common.FromHex(multibal.MultiBalanceV0MetaData.Bin)
   389  	deployer := func(txOpts *bind.TransactOpts, cb bind.ContractBackend) (common.Address, *types.Transaction, error) {
   390  		contractAddr, tx, _, err := multibal.DeployMultiBalanceV0(txOpts, cb)
   391  		return contractAddr, tx, err
   392  	}
   393  	return ContractDeployer.deployContract(ctx, txData, deployer, chain, credentialsPath, chainCfg, ui, log, net)
   394  }