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  }