decred.org/dcrdex@v1.0.5/client/asset/eth/contractor.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 "crypto/sha256" 9 "fmt" 10 "math/big" 11 "time" 12 13 "decred.org/dcrdex/client/asset" 14 "decred.org/dcrdex/dex" 15 "decred.org/dcrdex/dex/encode" 16 "decred.org/dcrdex/dex/networks/erc20" 17 erc20v0 "decred.org/dcrdex/dex/networks/erc20/contracts/v0" 18 dexeth "decred.org/dcrdex/dex/networks/eth" 19 swapv0 "decred.org/dcrdex/dex/networks/eth/contracts/v0" 20 "github.com/ethereum/go-ethereum" 21 "github.com/ethereum/go-ethereum/accounts/abi" 22 "github.com/ethereum/go-ethereum/accounts/abi/bind" 23 "github.com/ethereum/go-ethereum/common" 24 "github.com/ethereum/go-ethereum/core/types" 25 ) 26 27 // contractor is a translation layer between the abigen bindings and the DEX app. 28 // The intention is that if a new contract is implemented, the contractor 29 // interface itself will not require any updates. 30 type contractor interface { 31 swap(ctx context.Context, secretHash [32]byte) (*dexeth.SwapState, error) 32 initiate(*bind.TransactOpts, []*asset.Contract) (*types.Transaction, error) 33 redeem(txOpts *bind.TransactOpts, redeems []*asset.Redemption) (*types.Transaction, error) 34 refund(opts *bind.TransactOpts, secretHash [32]byte) (*types.Transaction, error) 35 estimateInitGas(ctx context.Context, n int) (uint64, error) 36 estimateRedeemGas(ctx context.Context, secrets [][32]byte) (uint64, error) 37 estimateRefundGas(ctx context.Context, secretHash [32]byte) (uint64, error) 38 // value checks the incoming or outgoing contract value. This is just the 39 // one of redeem, refund, or initiate values. It is not an error if the 40 // transaction does not pay to the contract, and the values returned in that 41 // case will always be zero. 42 value(context.Context, *types.Transaction) (incoming, outgoing uint64, err error) 43 isRefundable(secretHash [32]byte) (bool, error) 44 } 45 46 // tokenContractor interacts with an ERC20 token contract and a token swap 47 // contract. 48 type tokenContractor interface { 49 contractor 50 tokenAddress() common.Address 51 balance(context.Context) (*big.Int, error) 52 allowance(context.Context) (*big.Int, error) 53 approve(*bind.TransactOpts, *big.Int) (*types.Transaction, error) 54 estimateApproveGas(context.Context, *big.Int) (uint64, error) 55 transfer(*bind.TransactOpts, common.Address, *big.Int) (*types.Transaction, error) 56 parseTransfer(*types.Receipt) (uint64, error) 57 estimateTransferGas(context.Context, *big.Int) (uint64, error) 58 } 59 60 type contractorConstructor func(contractAddr, addr common.Address, ec bind.ContractBackend) (contractor, error) 61 type tokenContractorConstructor func(net dex.Network, token *dexeth.Token, acctAddr common.Address, ec bind.ContractBackend) (tokenContractor, error) 62 63 // contractV0 is the interface common to a version 0 swap contract or version 0 64 // token swap contract. 65 type contractV0 interface { 66 Initiate(opts *bind.TransactOpts, initiations []swapv0.ETHSwapInitiation) (*types.Transaction, error) 67 Redeem(opts *bind.TransactOpts, redemptions []swapv0.ETHSwapRedemption) (*types.Transaction, error) 68 Swap(opts *bind.CallOpts, secretHash [32]byte) (swapv0.ETHSwapSwap, error) 69 Refund(opts *bind.TransactOpts, secretHash [32]byte) (*types.Transaction, error) 70 IsRefundable(opts *bind.CallOpts, secretHash [32]byte) (bool, error) 71 } 72 73 // contractorV0 is the contractor for contract version 0. 74 // Redeem and Refund methods of swapv0.ETHSwap already have suitable return types. 75 type contractorV0 struct { 76 contractV0 // *swapv0.ETHSwap 77 abi *abi.ABI 78 cb bind.ContractBackend 79 contractAddr common.Address 80 acctAddr common.Address 81 isToken bool 82 evmify func(uint64) *big.Int 83 atomize func(*big.Int) uint64 84 } 85 86 var _ contractor = (*contractorV0)(nil) 87 88 // newV0Contractor is the constructor for a version 0 ETH swap contract. For 89 // token swap contracts, use newV0TokenContractor to construct a 90 // tokenContractorV0. 91 func newV0Contractor(contractAddr, acctAddr common.Address, cb bind.ContractBackend) (contractor, error) { 92 c, err := swapv0.NewETHSwap(contractAddr, cb) 93 if err != nil { 94 return nil, err 95 } 96 97 return &contractorV0{ 98 contractV0: c, 99 abi: dexeth.ABIs[0], 100 cb: cb, 101 contractAddr: contractAddr, 102 acctAddr: acctAddr, 103 evmify: dexeth.GweiToWei, 104 atomize: dexeth.WeiToGwei, 105 }, nil 106 } 107 108 // initiate sends the initiations to the swap contract's initiate function. 109 func (c *contractorV0) initiate(txOpts *bind.TransactOpts, contracts []*asset.Contract) (tx *types.Transaction, err error) { 110 inits := make([]swapv0.ETHSwapInitiation, 0, len(contracts)) 111 secrets := make(map[[32]byte]bool, len(contracts)) 112 113 for _, contract := range contracts { 114 if len(contract.SecretHash) != dexeth.SecretHashSize { 115 return nil, fmt.Errorf("wrong secret hash length. wanted %d, got %d", dexeth.SecretHashSize, len(contract.SecretHash)) 116 } 117 118 var secretHash [32]byte 119 copy(secretHash[:], contract.SecretHash) 120 121 if secrets[secretHash] { 122 return nil, fmt.Errorf("secret hash %s is a duplicate", contract.SecretHash) 123 } 124 secrets[secretHash] = true 125 126 if !common.IsHexAddress(contract.Address) { 127 return nil, fmt.Errorf("%q is not an address", contract.Address) 128 } 129 130 inits = append(inits, swapv0.ETHSwapInitiation{ 131 RefundTimestamp: big.NewInt(int64(contract.LockTime)), 132 SecretHash: secretHash, 133 Participant: common.HexToAddress(contract.Address), 134 Value: c.evmify(contract.Value), 135 }) 136 } 137 138 return c.contractV0.Initiate(txOpts, inits) 139 } 140 141 // redeem sends the redemptions to the swap contracts redeem method. 142 func (c *contractorV0) redeem(txOpts *bind.TransactOpts, redemptions []*asset.Redemption) (tx *types.Transaction, err error) { 143 redemps := make([]swapv0.ETHSwapRedemption, 0, len(redemptions)) 144 secretHashes := make(map[[32]byte]bool, len(redemptions)) 145 146 for _, r := range redemptions { 147 secretB, secretHashB := r.Secret, r.Spends.SecretHash 148 if len(secretB) != 32 || len(secretHashB) != 32 { 149 return nil, fmt.Errorf("invalid secret and/or secret hash sizes, %d and %d", len(secretB), len(secretHashB)) 150 } 151 var secret, secretHash [32]byte 152 copy(secret[:], secretB) 153 copy(secretHash[:], secretHashB) 154 if secretHashes[secretHash] { 155 return nil, fmt.Errorf("duplicate secret hash %x", secretHash[:]) 156 } 157 secretHashes[secretHash] = true 158 159 redemps = append(redemps, swapv0.ETHSwapRedemption{ 160 Secret: secret, 161 SecretHash: secretHash, 162 }) 163 } 164 return c.contractV0.Redeem(txOpts, redemps) 165 } 166 167 // swap retrieves the swap info from the read-only swap method. 168 func (c *contractorV0) swap(ctx context.Context, secretHash [32]byte) (*dexeth.SwapState, error) { 169 callOpts := &bind.CallOpts{ 170 From: c.acctAddr, 171 Context: ctx, 172 } 173 state, err := c.contractV0.Swap(callOpts, secretHash) 174 if err != nil { 175 return nil, err 176 } 177 178 return &dexeth.SwapState{ 179 BlockHeight: state.InitBlockNumber.Uint64(), 180 LockTime: time.Unix(state.RefundBlockTimestamp.Int64(), 0), 181 Secret: state.Secret, 182 Initiator: state.Initiator, 183 Participant: state.Participant, 184 Value: state.Value, 185 State: dexeth.SwapStep(state.State), 186 }, nil 187 } 188 189 // refund issues the refund command to the swap contract. Use isRefundable first 190 // to ensure the refund will be accepted. 191 func (c *contractorV0) refund(txOpts *bind.TransactOpts, secretHash [32]byte) (tx *types.Transaction, err error) { 192 return c.contractV0.Refund(txOpts, secretHash) 193 } 194 195 // isRefundable exposes the isRefundable method of the swap contract. 196 func (c *contractorV0) isRefundable(secretHash [32]byte) (bool, error) { 197 return c.contractV0.IsRefundable(&bind.CallOpts{From: c.acctAddr}, secretHash) 198 } 199 200 // estimateRedeemGas estimates the gas used to redeem. The secret hashes 201 // supplied must reference existing swaps, so this method can't be used until 202 // the swap is initiated. 203 func (c *contractorV0) estimateRedeemGas(ctx context.Context, secrets [][32]byte) (uint64, error) { 204 redemps := make([]swapv0.ETHSwapRedemption, 0, len(secrets)) 205 for _, secret := range secrets { 206 redemps = append(redemps, swapv0.ETHSwapRedemption{ 207 Secret: secret, 208 SecretHash: sha256.Sum256(secret[:]), 209 }) 210 } 211 return c.estimateGas(ctx, nil, "redeem", redemps) 212 } 213 214 // estimateRefundGas estimates the gas used to refund. The secret hashes 215 // supplied must reference existing swaps that are refundable, so this method 216 // can't be used until the swap is initiated and the lock time has expired. 217 func (c *contractorV0) estimateRefundGas(ctx context.Context, secretHash [32]byte) (uint64, error) { 218 return c.estimateGas(ctx, nil, "refund", secretHash) 219 } 220 221 // estimateInitGas estimates the gas used to initiate n generic swaps. The 222 // account must have a balance of at least n wei, as well as the eth balance 223 // to cover fees. 224 func (c *contractorV0) estimateInitGas(ctx context.Context, n int) (uint64, error) { 225 initiations := make([]swapv0.ETHSwapInitiation, 0, n) 226 for j := 0; j < n; j++ { 227 var secretHash [32]byte 228 copy(secretHash[:], encode.RandomBytes(32)) 229 initiations = append(initiations, swapv0.ETHSwapInitiation{ 230 RefundTimestamp: big.NewInt(1), 231 SecretHash: secretHash, 232 Participant: c.acctAddr, 233 Value: big.NewInt(1), 234 }) 235 } 236 237 var value *big.Int 238 if !c.isToken { 239 value = big.NewInt(int64(n)) 240 } 241 242 return c.estimateGas(ctx, value, "initiate", initiations) 243 } 244 245 // estimateGas estimates the gas used to interact with the swap contract. 246 func (c *contractorV0) estimateGas(ctx context.Context, value *big.Int, method string, args ...any) (uint64, error) { 247 data, err := c.abi.Pack(method, args...) 248 if err != nil { 249 return 0, fmt.Errorf("Pack error: %v", err) 250 } 251 252 return c.cb.EstimateGas(ctx, ethereum.CallMsg{ 253 From: c.acctAddr, 254 To: &c.contractAddr, 255 Data: data, 256 Value: value, 257 }) 258 } 259 260 // value calculates the incoming or outgoing value of the transaction, excluding 261 // fees but including operations against the swap contract. 262 func (c *contractorV0) value(ctx context.Context, tx *types.Transaction) (in, out uint64, err error) { 263 if *tx.To() != c.contractAddr { 264 return 0, 0, nil 265 } 266 267 if v, err := c.incomingValue(ctx, tx); err != nil { 268 return 0, 0, fmt.Errorf("incomingValue error: %w", err) 269 } else if v > 0 { 270 return v, 0, nil 271 } 272 273 return 0, c.outgoingValue(tx), nil 274 } 275 276 // incomingValue calculates the value being redeemed for refunded in the tx. 277 func (c *contractorV0) incomingValue(ctx context.Context, tx *types.Transaction) (uint64, error) { 278 if redeems, err := dexeth.ParseRedeemData(tx.Data(), 0); err == nil { 279 var redeemed uint64 280 for _, redeem := range redeems { 281 swap, err := c.swap(ctx, redeem.SecretHash) 282 if err != nil { 283 return 0, fmt.Errorf("redeem swap error: %w", err) 284 } 285 redeemed += c.atomize(swap.Value) 286 } 287 return redeemed, nil 288 } 289 secretHash, err := dexeth.ParseRefundData(tx.Data(), 0) 290 if err != nil { 291 return 0, nil 292 } 293 swap, err := c.swap(ctx, secretHash) 294 if err != nil { 295 return 0, fmt.Errorf("refund swap error: %w", err) 296 } 297 return c.atomize(swap.Value), nil 298 } 299 300 // outgoingValue calculates the value sent in swaps in the tx. 301 func (c *contractorV0) outgoingValue(tx *types.Transaction) (swapped uint64) { 302 if inits, err := dexeth.ParseInitiateData(tx.Data(), 0); err == nil { 303 for _, init := range inits { 304 swapped += c.atomize(init.Value) 305 } 306 } 307 return 308 } 309 310 // tokenContractorV0 is a contractor that implements the tokenContractor 311 // methods, providing access to the methods of the token's ERC20 contract. 312 type tokenContractorV0 struct { 313 *contractorV0 314 tokenAddr common.Address 315 tokenContract *erc20.IERC20 316 } 317 318 var _ contractor = (*tokenContractorV0)(nil) 319 var _ tokenContractor = (*tokenContractorV0)(nil) 320 321 // newV0TokenContractor is a contractor for version 0 erc20 token swap contract. 322 func newV0TokenContractor(net dex.Network, token *dexeth.Token, acctAddr common.Address, cb bind.ContractBackend) (tokenContractor, error) { 323 netToken, found := token.NetTokens[net] 324 if !found { 325 return nil, fmt.Errorf("token %s has no network %s", token.Name, net) 326 } 327 tokenAddr := netToken.Address 328 contract, found := netToken.SwapContracts[0] // contract version 0 329 if !found { 330 return nil, fmt.Errorf("token %s version 0 has no network %s token info", token.Name, net) 331 } 332 swapContractAddr := contract.Address 333 334 c, err := erc20v0.NewERC20Swap(swapContractAddr, cb) 335 if err != nil { 336 return nil, err 337 } 338 339 // tokenContract := bind.NewBoundContract(tokenAddr, *erc20.ERC20ABI, cb, cb, cb) 340 tokenContract, err := erc20.NewIERC20(tokenAddr, cb) 341 if err != nil { 342 return nil, err 343 } 344 345 if boundAddr, err := c.TokenAddress(&bind.CallOpts{ 346 Context: context.TODO(), 347 }); err != nil { 348 return nil, fmt.Errorf("error reading bound token address: %w", err) 349 } else if boundAddr != tokenAddr { 350 return nil, fmt.Errorf("wrong bound address. expected %s, got %s", tokenAddr, boundAddr) 351 } 352 353 return &tokenContractorV0{ 354 contractorV0: &contractorV0{ 355 contractV0: c, 356 abi: erc20.ERC20SwapABIV0, 357 cb: cb, 358 contractAddr: swapContractAddr, 359 acctAddr: acctAddr, 360 isToken: true, 361 evmify: token.AtomicToEVM, 362 atomize: token.EVMToAtomic, 363 }, 364 tokenAddr: tokenAddr, 365 tokenContract: tokenContract, 366 }, nil 367 } 368 369 // balance exposes the read-only balanceOf method of the erc20 token contract. 370 func (c *tokenContractorV0) balance(ctx context.Context) (*big.Int, error) { 371 callOpts := &bind.CallOpts{ 372 From: c.acctAddr, 373 Context: ctx, 374 } 375 376 return c.tokenContract.BalanceOf(callOpts, c.acctAddr) 377 } 378 379 // allowance exposes the read-only allowance method of the erc20 token contract. 380 func (c *tokenContractorV0) allowance(ctx context.Context) (*big.Int, error) { 381 // See if we support the pending state. 382 _, pendingUnavailable := c.cb.(*multiRPCClient) 383 callOpts := &bind.CallOpts{ 384 Pending: !pendingUnavailable, 385 From: c.acctAddr, 386 Context: ctx, 387 } 388 return c.tokenContract.Allowance(callOpts, c.acctAddr, c.contractAddr) 389 } 390 391 // approve sends an approve transaction approving the linked contract to call 392 // transferFrom for the specified amount. 393 func (c *tokenContractorV0) approve(txOpts *bind.TransactOpts, amount *big.Int) (tx *types.Transaction, err error) { 394 return c.tokenContract.Approve(txOpts, c.contractAddr, amount) 395 } 396 397 // transfer calls the transfer method of the erc20 token contract. Used for 398 // sends or withdrawals. 399 func (c *tokenContractorV0) transfer(txOpts *bind.TransactOpts, addr common.Address, amount *big.Int) (tx *types.Transaction, err error) { 400 return c.tokenContract.Transfer(txOpts, addr, amount) 401 } 402 403 func (c *tokenContractorV0) parseTransfer(receipt *types.Receipt) (uint64, error) { 404 var transferredAmt uint64 405 for _, log := range receipt.Logs { 406 if log.Address != c.tokenAddr { 407 continue 408 } 409 transfer, err := c.tokenContract.ParseTransfer(*log) 410 if err != nil { 411 continue 412 } 413 if transfer.To == c.acctAddr { 414 transferredAmt += transfer.Value.Uint64() 415 } 416 } 417 418 if transferredAmt > 0 { 419 return transferredAmt, nil 420 } 421 422 return 0, fmt.Errorf("transfer log to %s not found", c.acctAddr) 423 } 424 425 // estimateApproveGas estimates the gas needed to send an approve tx. 426 func (c *tokenContractorV0) estimateApproveGas(ctx context.Context, amount *big.Int) (uint64, error) { 427 return c.estimateGas(ctx, "approve", c.contractAddr, amount) 428 } 429 430 // estimateTransferGas estimates the gas needed for a transfer tx. The account 431 // needs to have > amount tokens to use this method. 432 func (c *tokenContractorV0) estimateTransferGas(ctx context.Context, amount *big.Int) (uint64, error) { 433 return c.estimateGas(ctx, "transfer", c.acctAddr, amount) 434 } 435 436 // estimateGas estimates the gas needed for methods on the ERC20 token contract. 437 // For estimating methods on the swap contract, use (contractorV0).estimateGas. 438 func (c *tokenContractorV0) estimateGas(ctx context.Context, method string, args ...any) (uint64, error) { 439 data, err := erc20.ERC20ABI.Pack(method, args...) 440 if err != nil { 441 return 0, fmt.Errorf("token estimateGas Pack error: %v", err) 442 } 443 444 return c.cb.EstimateGas(ctx, ethereum.CallMsg{ 445 From: c.acctAddr, 446 To: &c.tokenAddr, 447 Data: data, 448 }) 449 } 450 451 // value finds incoming or outgoing value for the tx to either the swap contract 452 // or the erc20 token contract. For the token contract, only transfer and 453 // transferFrom are parsed. It is not an error if this tx is a call to another 454 // method of the token contract, but no values will be parsed. 455 func (c *tokenContractorV0) value(ctx context.Context, tx *types.Transaction) (in, out uint64, err error) { 456 to := *tx.To() 457 if to == c.contractAddr { 458 return c.contractorV0.value(ctx, tx) 459 } 460 if to != c.tokenAddr { 461 return 0, 0, nil 462 } 463 464 // Consider removing. We'll never be sending transferFrom transactions 465 // directly. 466 if sender, _, value, err := erc20.ParseTransferFromData(tx.Data()); err == nil && sender == c.acctAddr { 467 return 0, c.atomize(value), nil 468 } 469 470 if _, value, err := erc20.ParseTransferData(tx.Data()); err == nil { 471 return 0, c.atomize(value), nil 472 } 473 474 return 0, 0, nil 475 } 476 477 // tokenAddress exposes the token_address immutable address of the token-bound 478 // swap contract. 479 func (c *tokenContractorV0) tokenAddress() common.Address { 480 return c.tokenAddr 481 } 482 483 var contractorConstructors = map[uint32]contractorConstructor{ 484 0: newV0Contractor, 485 } 486 487 var tokenContractorConstructors = map[uint32]tokenContractorConstructor{ 488 0: newV0TokenContractor, 489 }