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 }