diff --git a/client/asset/eth/eth.go b/client/asset/eth/eth.go index 90003cc60c..50b551b490 100644 --- a/client/asset/eth/eth.go +++ b/client/asset/eth/eth.go @@ -1243,7 +1243,7 @@ func (eth *baseWallet) wallet(assetID uint32) *assetWallet { return eth.wallets[assetID] } -func (eth *baseWallet) gasFeeLimit() uint64 { +func (eth *baseWallet) GasFeeLimit() uint64 { return atomic.LoadUint64(ð.gasFeeLimitV) } @@ -2069,12 +2069,6 @@ func (w *ETHWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uint6 dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, dexeth.MinGasTipCap) } - if w.gasFeeLimit() < ord.MaxFeeRate { - return nil, nil, 0, fmt.Errorf( - "%v: server's max fee rate %v higher than configured fee rate limit %v", - dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit()) - } - contractVer := contractVersion(ord.AssetVersion) g, err := w.initGasEstimate(int(ord.MaxSwapCount), contractVer, @@ -2109,12 +2103,6 @@ func (w *TokenWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uin dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, dexeth.MinGasTipCap) } - if w.gasFeeLimit() < ord.MaxFeeRate { - return nil, nil, 0, fmt.Errorf( - "%v: server's max fee rate %v higher than configured fee rate limit %v", - dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit()) - } - approvalStatus, err := w.swapContractApprovalStatus(ord.AssetVersion) if err != nil { return nil, nil, 0, fmt.Errorf("error getting approval status: %v", err) @@ -2161,12 +2149,6 @@ func (w *TokenWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uin // FundMultiOrder funds multiple orders in one shot. No special handling is // required for ETH as ETH does not over-lock during funding. func (w *ETHWallet) FundMultiOrder(ord *asset.MultiOrder, maxLock uint64) ([]asset.Coins, [][]dex.Bytes, uint64, error) { - if w.gasFeeLimit() < ord.MaxFeeRate { - return nil, nil, 0, fmt.Errorf( - "%v: server's max fee rate %v higher than configured fee rate limit %v", - dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit()) - } - g, err := w.initGasEstimate(1, ord.AssetVersion, ord.RedeemVersion, ord.RedeemAssetID, ord.MaxFeeRate) if err != nil { return nil, nil, 0, fmt.Errorf("error estimating swap gas: %v", err) @@ -2199,12 +2181,6 @@ func (w *ETHWallet) FundMultiOrder(ord *asset.MultiOrder, maxLock uint64) ([]ass // FundMultiOrder funds multiple orders in one shot. No special handling is // required for ETH as ETH does not over-lock during funding. func (w *TokenWallet) FundMultiOrder(ord *asset.MultiOrder, maxLock uint64) ([]asset.Coins, [][]dex.Bytes, uint64, error) { - if w.gasFeeLimit() < ord.MaxFeeRate { - return nil, nil, 0, fmt.Errorf( - "%v: server's max fee rate %v higher than configured fee rate limit %v", - dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit()) - } - approvalStatus, err := w.swapContractApprovalStatus(ord.AssetVersion) if err != nil { return nil, nil, 0, fmt.Errorf("error getting approval status: %v", err) diff --git a/client/asset/eth/eth_test.go b/client/asset/eth/eth_test.go index b93dd36016..8e6afcdc0d 100644 --- a/client/asset/eth/eth_test.go +++ b/client/asset/eth/eth_test.go @@ -2386,15 +2386,6 @@ func testFundOrderReturnCoinsFundingCoins(t *testing.T, assetID uint32) { node.tokenContractor.allow = unlimitedAllowance } - // Test eth wallet gas fee limit > server MaxFeeRate causes error - tmpGasFeeLimit := eth.gasFeeLimit() - eth.gasFeeLimitV = order.MaxFeeRate - 1 - _, _, _, err = w.FundOrder(&order) - if err == nil { - t.Fatalf("eth wallet gas fee limit > server MaxFeeRate should cause error") - } - eth.gasFeeLimitV = tmpGasFeeLimit - w2, eth2, _, shutdown2 := tassetWallet(assetID) defer shutdown2() eth2.node = node @@ -4605,8 +4596,8 @@ func TestDriverOpen(t *testing.T) { if !ok { t.Fatalf("failed to cast wallet as ETHWallet") } - if eth.gasFeeLimit() != defaultGasFeeLimit { - t.Fatalf("expected gasFeeLimit to be default, but got %v", eth.gasFeeLimit()) + if eth.GasFeeLimit() != defaultGasFeeLimit { + t.Fatalf("expected GasFeeLimit to be default, but got %v", eth.GasFeeLimit()) } // Make sure gas fee limit is properly parsed from settings @@ -4619,8 +4610,8 @@ func TestDriverOpen(t *testing.T) { if !ok { t.Fatalf("failed to cast wallet as ETHWallet") } - if eth.gasFeeLimit() != 150 { - t.Fatalf("expected gasFeeLimit to be 150, but got %v", eth.gasFeeLimit()) + if eth.GasFeeLimit() != 150 { + t.Fatalf("expected GasFeeLimit to be 150, but got %v", eth.GasFeeLimit()) } } @@ -5170,7 +5161,7 @@ func TestReconfigure(t *testing.T) { t.Fatalf("unexpected restart") } - if eth.baseWallet.gasFeeLimit() != ethCfg.GasFeeLimit { + if eth.baseWallet.GasFeeLimit() != ethCfg.GasFeeLimit { t.Fatal("gas fee limit was not updated properly") } } diff --git a/client/asset/interface.go b/client/asset/interface.go index e9416ff23b..d5275fc98f 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -948,6 +948,9 @@ type DynamicSwapper interface { DynamicSwapFeesPaid(ctx context.Context, coinID, contractData dex.Bytes) (fee uint64, secretHashes [][]byte, err error) // DynamicRedemptionFeesPaid returns fees for redemption transactions. DynamicRedemptionFeesPaid(ctx context.Context, coinID, contractData dex.Bytes) (fee uint64, secretHashes [][]byte, err error) + // GasFeeLimit returns the user set gas fee limit to be used as the max + // fee if higher than server. + GasFeeLimit() uint64 } // FeeRater is capable of retrieving a non-critical fee rate estimate for an diff --git a/client/asset/polygon/polygon.go b/client/asset/polygon/polygon.go index 1764b891b3..9364b8d4a7 100644 --- a/client/asset/polygon/polygon.go +++ b/client/asset/polygon/polygon.go @@ -55,7 +55,7 @@ func init() { const ( // BipID is the BIP-0044 asset ID for Polygon. BipID = 966 - defaultGasFeeLimit = 1000 + defaultGasFeeLimit = 2500 walletTypeRPC = "rpc" walletTypeToken = "token" ) diff --git a/client/core/core.go b/client/core/core.go index 55c844c30b..87b51148ca 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -6218,7 +6218,8 @@ func (c *Core) prepareForTradeRequestPrep(pw []byte, base, quote uint32, host st } func (c *Core) createTradeRequest(wallets *walletSet, coins asset.Coins, redeemScripts []dex.Bytes, dc *dexConnection, redeemAddr string, - form *TradeForm, redemptionRefundLots uint64, fundingFees uint64, assetConfigs *assetSet, mktConf *msgjson.Market, errCloser *dex.ErrorCloser) (*tradeRequest, error) { + form *TradeForm, redemptionRefundLots uint64, fundingFees, maxFeeRate uint64, assetConfigs *assetSet, mktConf *msgjson.Market, + errCloser *dex.ErrorCloser) (*tradeRequest, error) { coinIDs := make([]order.CoinID, 0, len(coins)) for i := range coins { coinIDs = append(coinIDs, []byte(coins[i].ID())) @@ -6498,11 +6499,21 @@ func (c *Core) prepareTradeRequest(pw []byte, form *TradeForm) (*tradeRequest, e qty, assetConfigs.baseAsset.Symbol, rate, mktConf.LotSize) } + maxFeeRate := assetConfigs.fromAsset.MaxFeeRate + if dynamicSwapper, is := fromWallet.Wallet.(asset.DynamicSwapper); is { + localMaxFee := dynamicSwapper.GasFeeLimit() + if maxFeeRate > localMaxFee { + return nil, codedError(walletErr, fmt.Errorf("%v: server's max fee rate %v higher than configured fee rate limit %v", + dex.BipIDSymbol(assetConfigs.fromAsset.ID), maxFeeRate, localMaxFee)) + } + maxFeeRate = localMaxFee + } + coins, redeemScripts, fundingFees, err := fromWallet.FundOrder(&asset.Order{ AssetVersion: assetConfigs.fromAsset.Version, Value: fundQty, MaxSwapCount: lots, - MaxFeeRate: assetConfigs.fromAsset.MaxFeeRate, + MaxFeeRate: maxFeeRate, Immediate: isImmediate, FeeSuggestion: c.feeSuggestion(dc, assetConfigs.fromAsset.ID), Options: form.Options, @@ -6535,7 +6546,7 @@ func (c *Core) prepareTradeRequest(pw []byte, form *TradeForm) (*tradeRequest, e }) tradeRequest, err := c.createTradeRequest(wallets, coins, redeemScripts, dc, redeemAddr, form, - redemptionRefundLots, fundingFees, assetConfigs, mktConf, errCloser) + redemptionRefundLots, fundingFees, maxFeeRate, assetConfigs, mktConf, errCloser) if err != nil { return nil, err } @@ -6588,10 +6599,20 @@ func (c *Core) prepareMultiTradeRequests(pw []byte, form *MultiTradeForm) ([]*tr }) } + maxFeeRate := assetConfigs.fromAsset.MaxFeeRate + if dynamicSwapper, is := fromWallet.Wallet.(asset.DynamicSwapper); is { + localMaxFee := dynamicSwapper.GasFeeLimit() + if maxFeeRate > localMaxFee { + return nil, codedError(walletErr, fmt.Errorf("%v: server's max fee rate %v higher than configured fee rate limit %v", + dex.BipIDSymbol(assetConfigs.fromAsset.ID), maxFeeRate, localMaxFee)) + } + maxFeeRate = localMaxFee + } + allCoins, allRedeemScripts, fundingFees, err := fromWallet.FundMultiOrder(&asset.MultiOrder{ AssetVersion: assetConfigs.fromAsset.Version, Values: orderValues, - MaxFeeRate: assetConfigs.fromAsset.MaxFeeRate, + MaxFeeRate: maxFeeRate, FeeSuggestion: c.feeSuggestion(dc, assetConfigs.fromAsset.ID), Options: form.Options, RedeemVersion: assetConfigs.toAsset.Version, @@ -6646,7 +6667,7 @@ func (c *Core) prepareMultiTradeRequests(pw []byte, form *MultiTradeForm) ([]*tr fees = fundingFees } req, err := c.createTradeRequest(wallets, coins, allRedeemScripts[i], dc, redeemAddresses[i], tradeForm, - orderValues[i].MaxSwapCount, fees, assetConfigs, mktConf, errClosers[i]) + orderValues[i].MaxSwapCount, fees, maxFeeRate, assetConfigs, mktConf, errClosers[i]) if err != nil { return nil, err } diff --git a/client/core/core_test.go b/client/core/core_test.go index fd011595e7..4d4f2da1cf 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -10732,9 +10732,47 @@ func (dtfc *TDynamicSwapper) DynamicSwapFeesPaid(ctx context.Context, coinID, co func (dtfc *TDynamicSwapper) DynamicRedemptionFeesPaid(ctx context.Context, coinID, contractData dex.Bytes) (uint64, [][]byte, error) { return dtfc.tfpPaid, dtfc.tfpSecretHashes, dtfc.tfpErr } +func (dtfc *TDynamicSwapper) GasFeeLimit() uint64 { + return 200 +} var _ asset.DynamicSwapper = (*TDynamicSwapper)(nil) +// TDynamicAccountLocker combines TAccountLocker with DynamicSwapper interface +// for testing gas fee limit validation in prepareTradeRequest. +type TDynamicAccountLocker struct { + *TAccountLocker + gasFeeLimit uint64 + tfpPaid uint64 + tfpSecretHashes [][]byte + tfpErr error +} + +func newTDynamicAccountLocker(assetID uint32) (*xcWallet, *TDynamicAccountLocker) { + xcWallet, accountLocker := newTAccountLocker(assetID) + dynamicAccountLocker := &TDynamicAccountLocker{ + TAccountLocker: accountLocker, + gasFeeLimit: 200, // default higher than tACCTAsset.MaxFeeRate (20) + } + xcWallet.Wallet = dynamicAccountLocker + return xcWallet, dynamicAccountLocker +} + +func (w *TDynamicAccountLocker) DynamicSwapFeesPaid(ctx context.Context, coinID, contractData dex.Bytes) (uint64, [][]byte, error) { + return w.tfpPaid, w.tfpSecretHashes, w.tfpErr +} + +func (w *TDynamicAccountLocker) DynamicRedemptionFeesPaid(ctx context.Context, coinID, contractData dex.Bytes) (uint64, [][]byte, error) { + return w.tfpPaid, w.tfpSecretHashes, w.tfpErr +} + +func (w *TDynamicAccountLocker) GasFeeLimit() uint64 { + return w.gasFeeLimit +} + +var _ asset.DynamicSwapper = (*TDynamicAccountLocker)(nil) +var _ asset.AccountLocker = (*TDynamicAccountLocker)(nil) + func TestUpdateFeesPaid(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -10820,6 +10858,118 @@ func TestUpdateFeesPaid(t *testing.T) { } } +// TestDynamicSwapperGasFeeLimit tests the gas fee limit validation in +// prepareTradeRequest for DynamicSwapper wallets. +func TestDynamicSwapperGasFeeLimit(t *testing.T) { + rig := newTestRig() + defer rig.shutdown() + tCore := rig.core + + // Set up BTC wallet (to wallet when buying BTC with ETH) + btcWallet, _ := newTWallet(tUTXOAssetB.ID) + tCore.wallets[tUTXOAssetB.ID] = btcWallet + btcWallet.address = "12DXGkvxFjuq5btXYkwWfBZaz1rVwFgini" + btcWallet.Unlock(rig.crypter) + + // Set up ETH wallet with DynamicSwapper interface (from wallet when buying BTC with ETH) + ethWallet, tEthWallet := newTDynamicAccountLocker(tACCTAsset.ID) + tCore.wallets[tACCTAsset.ID] = ethWallet + ethWallet.address = "18d65fb8d60c1199bb1ad381be47aa692b482605" + ethWallet.Unlock(rig.crypter) + + var lots uint64 = 10 + qty := dcrBtcLotSize * lots + rate := dcrBtcRateStep * 1000 + + // Set up ETH as funding coins (ETH is from wallet when Sell=false on btc_eth market) + ethVal := calc.BaseToQuote(rate, qty*2) + ethCoin := &tCoin{ + id: encode.RandomBytes(36), + val: ethVal, + } + tEthWallet.fundingCoins = asset.Coins{ethCoin} + tEthWallet.fundRedeemScripts = []dex.Bytes{nil} + + book := newBookie(rig.dc, tUTXOAssetB.ID, tACCTAsset.ID, nil, tLogger) + rig.dc.books[tBtcEthMktName] = book + + msgOrderNote := &msgjson.BookOrderNote{ + OrderNote: msgjson.OrderNote{ + OrderID: encode.RandomBytes(32), + }, + TradeNote: msgjson.TradeNote{ + Side: msgjson.SellOrderNum, + Quantity: dcrBtcLotSize, + Time: uint64(time.Now().Unix()), + Rate: rate, + }, + } + + err := book.Sync(&msgjson.OrderBook{ + MarketID: tBtcEthMktName, + Seq: 1, + Epoch: 1, + Orders: []*msgjson.BookOrderNote{msgOrderNote}, + }) + if err != nil { + t.Fatalf("order book sync error: %v", err) + } + + handleLimit := func(msg *msgjson.Message, f msgFunc) error { + t.Helper() + msgOrder := new(msgjson.LimitOrder) + err := msg.Unmarshal(msgOrder) + if err != nil { + t.Fatalf("unmarshal error: %v", err) + } + lo := convertMsgLimitOrder(msgOrder) + f(orderResponse(msg.ID, msgOrder, lo, false, false, false)) + return nil + } + + // Form for buying BTC with ETH (ETH is the from/funding wallet with DynamicSwapper) + // Market is btc_eth (Base=BTC, Quote=ETH), Sell=false means buying base (BTC), + // so the from wallet is the quote asset (ETH). + form := &TradeForm{ + Host: tDexHost, + IsLimit: true, + Sell: false, // Buying BTC, selling ETH. ETH is from wallet. + Base: tUTXOAssetB.ID, + Quote: tACCTAsset.ID, + Qty: qty, + Rate: rate, + TifNow: false, + } + + // Test 1: Gas fee limit higher than server's max fee rate - should succeed + // tACCTAsset.MaxFeeRate = 20, gasFeeLimit = 200 + tEthWallet.gasFeeLimit = 200 + rig.ws.queueResponse(msgjson.LimitRoute, handleLimit) + _, err = tCore.Trade(tPW, form) + if err != nil { + t.Fatalf("trade with adequate gas fee limit should succeed: %v", err) + } + + // Test 2: Gas fee limit lower than server's max fee rate - should fail + // tACCTAsset.MaxFeeRate = 20, gasFeeLimit = 10 + tEthWallet.gasFeeLimit = 10 + _, err = tCore.Trade(tPW, form) + if err == nil { + t.Fatal("trade with gas fee limit lower than server max fee rate should fail") + } + if !strings.Contains(err.Error(), "higher than configured fee rate limit") { + t.Fatalf("expected gas fee limit error, got: %v", err) + } + + // Test 3: Gas fee limit equal to server's max fee rate - should succeed + tEthWallet.gasFeeLimit = tACCTAsset.MaxFeeRate + rig.ws.queueResponse(msgjson.LimitRoute, handleLimit) + _, err = tCore.Trade(tPW, form) + if err != nil { + t.Fatalf("trade with gas fee limit equal to server max fee rate should succeed: %v", err) + } +} + func TestUpdateBondOptions(t *testing.T) { const feeRate = 50 diff --git a/client/core/trade.go b/client/core/trade.go index 4a6d0685d4..865336df22 100644 --- a/client/core/trade.go +++ b/client/core/trade.go @@ -2399,6 +2399,11 @@ func (t *trackedTrade) revokeMatch(matchID order.MatchID, fromServer bool) error // This method modifies match fields and MUST be called with the trackedTrade // mutex lock held for writes. func (c *Core) swapMatches(t *trackedTrade, matches []*matchTracker) (err error) { + feeRate, err := t.bestSwapGroupFeeRate(matches) + if err != nil { + return err + } + errs := newErrorSet("swapMatches order %s - ", t.ID()) groupables := make([]*matchTracker, 0, len(matches)) // Over-allocating if there are suspect matches var suspects []*matchTracker @@ -2409,7 +2414,6 @@ func (c *Core) swapMatches(t *trackedTrade, matches []*matchTracker) (err error) groupables = append(groupables, m) } } - feeRate := t.bestSwapGroupFeeRate(matches) if len(groupables) > 0 { var maxSwapsInTx int if counter, is := t.wallets.fromWallet.Wallet.(asset.MaxMatchesCounter); is { @@ -2436,7 +2440,7 @@ func (c *Core) swapMatches(t *trackedTrade, matches []*matchTracker) (err error) } // bestSwapGroupRate gets the most appropriate fee rate for a group of swaps. -func (t *trackedTrade) bestSwapGroupFeeRate(matches []*matchTracker) uint64 { +func (t *trackedTrade) bestSwapGroupFeeRate(matches []*matchTracker) (uint64, error) { var highestFeeRate uint64 for _, match := range matches { if match.FeeRateSwap > highestFeeRate { @@ -2444,14 +2448,15 @@ func (t *trackedTrade) bestSwapGroupFeeRate(matches []*matchTracker) uint64 { } } // Use a higher swap fee rate if a local estimate is higher than the - // prescribed rate, but not higher than the funded (max) rate. + // prescribed rate. If the fresh rate is higher than the max fee rate, + // our swap transactions will not be mined, so fail. if highestFeeRate < t.metaData.MaxFeeRate { freshRate := t.wallets.fromWallet.feeRate() if freshRate == 0 { // either not a FeeRater, or FeeRate failed freshRate = t.dc.bestBookFeeSuggestion(t.wallets.fromWallet.AssetID) } if freshRate > t.metaData.MaxFeeRate { - freshRate = t.metaData.MaxFeeRate + return 0, fmt.Errorf("current fee rate higher than the order's max %d > %d", freshRate, t.metaData.MaxFeeRate) } if highestFeeRate < freshRate { t.dc.log.Infof("Prescribed %v fee rate %v looks low, using %v", @@ -2459,7 +2464,7 @@ func (t *trackedTrade) bestSwapGroupFeeRate(matches []*matchTracker) uint64 { highestFeeRate = freshRate } } - return highestFeeRate + return highestFeeRate, nil } // swapMatchGroup will send a transaction with swap outputs for the specified diff --git a/dex/networks/polygon/genesis_config.go b/dex/networks/polygon/genesis_config.go index 5059dfbb57..164473de99 100644 --- a/dex/networks/polygon/genesis_config.go +++ b/dex/networks/polygon/genesis_config.go @@ -23,7 +23,7 @@ var allocs embed.FS const ( MainnetChainID = 137 TestnetChainID = 80001 // Mumbai - SimnetChainID = 90001 + SimnetChainID = 1337 ) var ( diff --git a/dex/testing/loadbot/mantle.go b/dex/testing/loadbot/mantle.go index 1a6573f63c..d1aa026bd2 100644 --- a/dex/testing/loadbot/mantle.go +++ b/dex/testing/loadbot/mantle.go @@ -854,10 +854,7 @@ func newBotWallet(symbol, node, name string, port string, pass []byte, minFunds, }, } case polygon, usdcp: - rpcProvider := filepath.Join(dextestDir, "polygon", "alpha", "bor", "bor.ipc") - if node == beta { - rpcProvider = filepath.Join(dextestDir, "eth", "beta", "bor", "bor.ipc") - } + rpcProvider := "ws://127.0.0.1:34983" form = &core.WalletForm{ Type: "rpc", AssetID: polygonID,