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 }