decred.org/dcrdex@v1.0.5/server/asset/eth/coiner.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  	"errors"
     9  	"fmt"
    10  	"math/big"
    11  
    12  	dexeth "decred.org/dcrdex/dex/networks/eth"
    13  	"decred.org/dcrdex/server/asset"
    14  	"github.com/ethereum/go-ethereum"
    15  	"github.com/ethereum/go-ethereum/common"
    16  )
    17  
    18  var _ asset.Coin = (*swapCoin)(nil)
    19  var _ asset.Coin = (*redeemCoin)(nil)
    20  
    21  type baseCoin struct {
    22  	backend      *AssetBackend
    23  	secretHash   [32]byte
    24  	gasFeeCap    *big.Int
    25  	gasTipCap    *big.Int
    26  	txHash       common.Hash
    27  	value        *big.Int
    28  	txData       []byte
    29  	serializedTx []byte
    30  	contractVer  uint32
    31  }
    32  
    33  type swapCoin struct {
    34  	*baseCoin
    35  	dexAtoms uint64
    36  	init     *dexeth.Initiation
    37  }
    38  
    39  type redeemCoin struct {
    40  	*baseCoin
    41  	secret [32]byte
    42  }
    43  
    44  // newSwapCoin creates a new swapCoin that stores and retrieves info about a
    45  // swap. It requires a coinID that is a txid type of the initial transaction
    46  // initializing or redeeming the swap. A txid type and not a swap type is
    47  // required because the contract will give us no useful information about the
    48  // swap before it is mined. Having the initial transaction allows us to track
    49  // it in the mempool. It also tells us all the data we need to confirm a tx
    50  // will do what we expect if mined and satisfies contract constraints. These
    51  // fields are verified when the Confirmations method is called.
    52  func (be *AssetBackend) newSwapCoin(coinID []byte, contractData []byte) (*swapCoin, error) {
    53  	bc, err := be.baseCoin(coinID, contractData)
    54  	if err != nil {
    55  		return nil, err
    56  	}
    57  
    58  	inits, err := dexeth.ParseInitiateData(bc.txData, ethContractVersion)
    59  	if err != nil {
    60  		return nil, fmt.Errorf("unable to parse initiate call data: %v", err)
    61  	}
    62  
    63  	init, ok := inits[bc.secretHash]
    64  	if !ok {
    65  		return nil, fmt.Errorf("tx %v does not contain initiation with secret hash %x", bc.txHash, bc.secretHash)
    66  	}
    67  
    68  	if be.assetID == be.baseChainID {
    69  		sum := new(big.Int)
    70  		for _, in := range inits {
    71  			sum.Add(sum, in.Value)
    72  		}
    73  		if bc.value.Cmp(sum) < 0 {
    74  			return nil, fmt.Errorf("tx %s value < sum of inits. %d < %d", bc.txHash, bc.value, sum)
    75  		}
    76  	}
    77  
    78  	return &swapCoin{
    79  		baseCoin: bc,
    80  		init:     init,
    81  		dexAtoms: be.atomize(init.Value),
    82  	}, nil
    83  }
    84  
    85  // newRedeemCoin pulls the tx for the coinID => txHash, extracts the secret, and
    86  // provides a coin to check confirmations, as required by asset.Coin interface,
    87  // TODO: The redeemCoin's Confirmation method is never used by the current
    88  // swapper implementation. Might consider an API change for
    89  // asset.Backend.Redemption.
    90  func (be *AssetBackend) newRedeemCoin(coinID []byte, contractData []byte) (*redeemCoin, error) {
    91  	bc, err := be.baseCoin(coinID, contractData)
    92  	if err == asset.CoinNotFoundError {
    93  		// If the coin is not found, check to see if the swap has been
    94  		// redeemed by another transaction.
    95  		contractVer, secretHash, err := dexeth.DecodeContractData(contractData)
    96  		if err != nil {
    97  			return nil, err
    98  		}
    99  		be.log.Warnf("redeem coin with ID %x for secret hash %x was not found", coinID, secretHash)
   100  		swapState, err := be.node.swap(be.ctx, be.assetID, secretHash)
   101  		if err != nil {
   102  			return nil, err
   103  		}
   104  		if swapState.State != dexeth.SSRedeemed {
   105  			return nil, asset.CoinNotFoundError
   106  		}
   107  		bc = &baseCoin{
   108  			backend:     be,
   109  			secretHash:  secretHash,
   110  			contractVer: contractVer,
   111  		}
   112  		return &redeemCoin{
   113  			baseCoin: bc,
   114  			secret:   swapState.Secret,
   115  		}, nil
   116  	} else if err != nil {
   117  		return nil, err
   118  	}
   119  
   120  	if bc.value.Cmp(new(big.Int)) != 0 {
   121  		return nil, fmt.Errorf("expected tx value of zero for redeem but got: %d", bc.value)
   122  	}
   123  
   124  	redemptions, err := dexeth.ParseRedeemData(bc.txData, ethContractVersion)
   125  	if err != nil {
   126  		return nil, fmt.Errorf("unable to parse redemption call data: %v", err)
   127  	}
   128  	redemption, ok := redemptions[bc.secretHash]
   129  	if !ok {
   130  		return nil, fmt.Errorf("tx %v does not contain redemption with secret hash %x", bc.txHash, bc.secretHash)
   131  	}
   132  
   133  	return &redeemCoin{
   134  		baseCoin: bc,
   135  		secret:   redemption.Secret,
   136  	}, nil
   137  }
   138  
   139  // The baseCoin is basic tx and swap contract data.
   140  func (be *AssetBackend) baseCoin(coinID []byte, contractData []byte) (*baseCoin, error) {
   141  	txHash, err := dexeth.DecodeCoinID(coinID)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	tx, _, err := be.node.transaction(be.ctx, txHash)
   146  	if err != nil {
   147  		if errors.Is(err, ethereum.NotFound) {
   148  			return nil, asset.CoinNotFoundError
   149  		}
   150  		return nil, fmt.Errorf("unable to fetch transaction: %v", err)
   151  	}
   152  
   153  	serializedTx, err := tx.MarshalBinary()
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	contractVer, secretHash, err := dexeth.DecodeContractData(contractData)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  	if contractVer != version {
   163  		return nil, fmt.Errorf("contract version %d not supported, only %d", contractVer, version)
   164  	}
   165  	contractAddr := tx.To()
   166  	if *contractAddr != be.contractAddr {
   167  		return nil, fmt.Errorf("contract address is not supported: %v", contractAddr)
   168  	}
   169  
   170  	// Gas price is not stored in the swap, and is used to determine if the
   171  	// initialization transaction could take a long time to be mined. A
   172  	// transaction with a very low gas price may need to be resent with a
   173  	// higher price.
   174  	//
   175  	// Although we only retrieve the GasFeeCap and GasTipCap here, legacy
   176  	// transaction are also supported. In legacy transactions, the full
   177  	// gas price that is specified will be used no matter what, so the
   178  	// values returned from GasFeeCap and GasTipCap will both be equal to the
   179  	// gas price.
   180  	zero := new(big.Int)
   181  	gasFeeCap := tx.GasFeeCap()
   182  	if gasFeeCap == nil || gasFeeCap.Cmp(zero) <= 0 {
   183  		return nil, fmt.Errorf("Failed to parse gas fee cap from tx %s", txHash)
   184  	}
   185  
   186  	gasTipCap := tx.GasTipCap()
   187  	if gasTipCap == nil || gasTipCap.Cmp(zero) <= 0 {
   188  		return nil, fmt.Errorf("Failed to parse gas tip cap from tx %s", txHash)
   189  	}
   190  
   191  	return &baseCoin{
   192  		backend:      be,
   193  		secretHash:   secretHash,
   194  		gasFeeCap:    gasFeeCap,
   195  		gasTipCap:    gasTipCap,
   196  		txHash:       txHash,
   197  		value:        tx.Value(),
   198  		txData:       tx.Data(),
   199  		serializedTx: serializedTx,
   200  		contractVer:  contractVer,
   201  	}, nil
   202  }
   203  
   204  // Confirmations returns the number of confirmations for a Coin's
   205  // transaction.
   206  //
   207  // In the case of ethereum it is extra important to check confirmations before
   208  // confirming a swap. Even though we check the initial transaction's data, if
   209  // that transaction were in mempool at the time, it could be swapped out with
   210  // any other values if a user sent another transaction with a higher gas fee
   211  // and the same account and nonce, effectively voiding the transaction we
   212  // expected to be mined.
   213  func (c *swapCoin) Confirmations(ctx context.Context) (int64, error) {
   214  	swap, err := c.backend.node.swap(ctx, c.backend.assetID, c.secretHash)
   215  	if err != nil {
   216  		return -1, err
   217  	}
   218  
   219  	// Uninitiated state is zero confs. It could still be in mempool.
   220  	// It is important to only trust confirmations according to the
   221  	// swap contract. Until there are confirmations we cannot be sure
   222  	// that initiation happened successfully.
   223  	if swap.State == dexeth.SSNone {
   224  		// Assume the tx still has a chance of being mined.
   225  		return 0, nil
   226  	}
   227  	// Any other swap state is ok. We are sure that initialization
   228  	// happened.
   229  
   230  	// The swap initiation transaction has some number of
   231  	// confirmations, and we are sure the secret hash belongs to
   232  	// this swap. Assert that the value, receiver, and locktime are
   233  	// as expected.
   234  	if swap.Value.Cmp(c.init.Value) != 0 {
   235  		return -1, fmt.Errorf("tx data swap val (%d) does not match contract value (%d)",
   236  			c.init.Value, swap.Value)
   237  	}
   238  	if swap.Participant != c.init.Participant {
   239  		return -1, fmt.Errorf("tx data participant %q does not match contract value %q",
   240  			c.init.Participant, swap.Participant)
   241  	}
   242  
   243  	// locktime := swap.RefundBlockTimestamp.Int64()
   244  	if !swap.LockTime.Equal(c.init.LockTime) {
   245  		return -1, fmt.Errorf("expected swap locktime (%s) does not match expected (%s)",
   246  			c.init.LockTime, swap.LockTime)
   247  	}
   248  
   249  	bn, err := c.backend.node.blockNumber(ctx)
   250  	if err != nil {
   251  		return 0, fmt.Errorf("unable to fetch block number: %v", err)
   252  	}
   253  	return int64(bn - swap.BlockHeight + 1), nil
   254  }
   255  
   256  func (c *redeemCoin) Confirmations(ctx context.Context) (int64, error) {
   257  	swap, err := c.backend.node.swap(ctx, c.backend.assetID, c.secretHash)
   258  	if err != nil {
   259  		return -1, err
   260  	}
   261  
   262  	// There should be no need to check the counter party, or value
   263  	// as a swap with a specific secret hash that has been redeemed
   264  	// wouldn't have been redeemed without ensuring the initiator
   265  	// is the expected address and value was also as expected. Also
   266  	// not validating the locktime, as the swap is redeemed and
   267  	// locktime no longer relevant.
   268  	if swap.State == dexeth.SSRedeemed {
   269  		bn, err := c.backend.node.blockNumber(ctx)
   270  		if err != nil {
   271  			return 0, fmt.Errorf("unable to fetch block number: %v", err)
   272  		}
   273  		return int64(bn - swap.BlockHeight + 1), nil
   274  	}
   275  	// If swap is in the Initiated state, the redemption may be
   276  	// unmined.
   277  	if swap.State == dexeth.SSInitiated {
   278  		// Assume the tx still has a chance of being mined.
   279  		return 0, nil
   280  	}
   281  	// If swap is in None state, then the redemption can't possibly
   282  	// succeed as the swap must already be in the Initialized state
   283  	// to redeem. If the swap is in the Refunded state, then the
   284  	// redemption either failed or never happened.
   285  	return -1, fmt.Errorf("redemption in failed state with swap at %s state", swap.State)
   286  }
   287  
   288  func (c *redeemCoin) Value() uint64 { return 0 }
   289  
   290  // ID is the swap's coin ID.
   291  func (c *baseCoin) ID() []byte {
   292  	return c.txHash.Bytes() // c.txHash[:]
   293  }
   294  
   295  // TxID is the original init transaction txid.
   296  func (c *baseCoin) TxID() string {
   297  	return c.txHash.String()
   298  }
   299  
   300  // String is a human readable representation of the swap coin.
   301  func (c *baseCoin) String() string {
   302  	return c.txHash.String()
   303  }
   304  
   305  // FeeRate returns the gas rate, in gwei/gas. It is set in initialization of
   306  // the swapCoin.
   307  func (c *baseCoin) FeeRate() uint64 {
   308  	return dexeth.WeiToGweiCeil(c.gasFeeCap)
   309  }
   310  
   311  // Value returns the value of one swap in order to validate during processing.
   312  func (c *swapCoin) Value() uint64 {
   313  	return c.dexAtoms
   314  }