diff --git a/.gitignore b/.gitignore index f458e67ba8..b67a8ab7bb 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,10 @@ main # golang-ci-lint binary contrib/golang-ci-lint/golang-ci-lint + +*signer*.txt + +# ignore any rfq config files that are not the template +services/rfq/**/config*.yaml +services/rfq/**/config*.yml +!services/rfq/**/config.yml \ No newline at end of file diff --git a/docs/bridge/docs/06-Services/04-Submitter.md b/docs/bridge/docs/06-Services/04-Submitter.md index ff2f045802..0800d16c1b 100644 --- a/docs/bridge/docs/06-Services/04-Submitter.md +++ b/docs/bridge/docs/06-Services/04-Submitter.md @@ -59,6 +59,9 @@ submitter_config: dynamic_gas_estimate: true # SupportsEIP1559 is whether or not this chain supports EIP1559. supports_eip_1559: true + # DynamicGasUnitAddPercentage - increase gas unit limit (ie: "gas" field on a typical tx) by X% from what dynamic gas estimate returns + # Has no effect if dynamic gas estimation is not also enabled. + dynamic_gas_unit_add_percentage: 5 43114: max_gas_price: 100000000000 # 100 Gwei 10: diff --git a/ethergo/submitter/config/config.go b/ethergo/submitter/config/config.go index af6cf120d7..591029b41e 100644 --- a/ethergo/submitter/config/config.go +++ b/ethergo/submitter/config/config.go @@ -49,6 +49,9 @@ type ChainConfig struct { DynamicGasEstimate bool `yaml:"dynamic_gas_estimate"` // SupportsEIP1559 is whether or not this chain supports EIP1559 SupportsEIP1559 bool `yaml:"supports_eip_1559"` + // DynamicGasUnitAddPercentage - increase gas unit limit (ie: "gas" field on a typical tx) by X% from what dynamic gas estimate returns + // Has no effect if dynamic gas estimation is not also enabled. + DynamicGasUnitAddPercentage int `yaml:"dynamic_gas_unit_add_percentage"` } const ( @@ -64,6 +67,9 @@ const ( // DefaultGasEstimate is the default gas estimate to use for transactions. DefaultGasEstimate = uint64(1200000) + + // DefaultDynamicGasUnitAddPercentage is the default percentage to bump the gas limit by. + DefaultDynamicGasUnitAddPercentage = 5 ) // DefaultMaxPrice is the default max price of a tx. @@ -188,6 +194,24 @@ func (c *Config) GetGasBumpPercentage(chainID int) (gasBumpPercentage int) { return gasBumpPercentage } +// GetDynamicGasUnitAddPercentage returns the percentage to bump the gas limit by +func (c *Config) GetDynamicGasUnitAddPercentage(chainID int) (dynamicGasUnitAddPercentage int) { + chainConfig, ok := c.Chains[chainID] + if ok { + dynamicGasUnitAddPercentage = chainConfig.DynamicGasUnitAddPercentage + } + // if dynamicGasUnitAddPercentage is not set for the chain, use the global config + if dynamicGasUnitAddPercentage == 0 { + dynamicGasUnitAddPercentage = c.DynamicGasUnitAddPercentage + } + + // if the dynamicGasUnitAddPercentage isn't set at all, use the default + if dynamicGasUnitAddPercentage == 0 { + dynamicGasUnitAddPercentage = DefaultDynamicGasUnitAddPercentage + } + return dynamicGasUnitAddPercentage +} + // GetGasEstimate returns the gas estimate to use for transactions // TODO: test this method. func (c *Config) GetGasEstimate(chainID int) (gasEstimate uint64) { @@ -195,12 +219,12 @@ func (c *Config) GetGasEstimate(chainID int) (gasEstimate uint64) { if ok { gasEstimate = chainConfig.GasEstimate } - // if gasBumpPercentage is not set for the chain, use the global config + // if gasEstimate is not set for the chain, use the global config if gasEstimate == 0 { gasEstimate = c.GasEstimate } - // if the gasBumpPercentage isn't set at all, use the default + // if the gasEstimate isn't set at all, use the default if gasEstimate == 0 { gasEstimate = DefaultGasEstimate } diff --git a/ethergo/submitter/config/config_test.go b/ethergo/submitter/config/config_test.go index 2d72049d65..52ed92c1bc 100644 --- a/ethergo/submitter/config/config_test.go +++ b/ethergo/submitter/config/config_test.go @@ -77,6 +77,7 @@ gas_bump_percentage: 10 gas_estimate: 1000 is_l2: true dynamic_gas_estimate: true +dynamic_gas_unit_add_percentage: 20 supports_eip_1559: true` var cfg config.Config err := yaml.Unmarshal([]byte(cfgStr), &cfg) @@ -84,6 +85,7 @@ supports_eip_1559: true` assert.Equal(t, big.NewInt(250000000000), cfg.MaxGasPrice) assert.Equal(t, 60, cfg.BumpIntervalSeconds) assert.Equal(t, 10, cfg.GasBumpPercentage) + assert.Equal(t, 20, cfg.DynamicGasUnitAddPercentage) assert.Equal(t, uint64(1000), cfg.GasEstimate) assert.Equal(t, true, cfg.DynamicGasEstimate) assert.Equal(t, true, cfg.SupportsEIP1559(0)) diff --git a/ethergo/submitter/config/iconfig_generated.go b/ethergo/submitter/config/iconfig_generated.go index 12716431c3..d17b995655 100644 --- a/ethergo/submitter/config/iconfig_generated.go +++ b/ethergo/submitter/config/iconfig_generated.go @@ -41,4 +41,6 @@ type IConfig interface { SetMinGasPrice(basePrice *big.Int) // SetGlobalEIP1559Support is a helper function that sets the global EIP1559 support. SetGlobalEIP1559Support(supportsEIP1559 bool) + // GetDynamicGasUnitAddPercentage returns the percentage to modify the gas unit limit by + GetDynamicGasUnitAddPercentage(chainID int) (dynamicGasUnitAddPercentage int) } diff --git a/ethergo/submitter/submitter.go b/ethergo/submitter/submitter.go index 67b590fe7e..70cfa6cb7b 100644 --- a/ethergo/submitter/submitter.go +++ b/ethergo/submitter/submitter.go @@ -392,8 +392,23 @@ func (t *txSubmitterImpl) SubmitTransaction(parentCtx context.Context, chainID * if err != nil { span.AddEvent("could not set gas price", trace.WithAttributes(attribute.String("error", err.Error()))) } - if !t.config.GetDynamicGasEstimate(int(chainID.Uint64())) { - transactor.GasLimit = t.config.GetGasEstimate(int(chainID.Uint64())) + + performSignature := func(address common.Address, transaction *types.Transaction) (_ *types.Transaction, err error) { + + newNonce, err := t.getNonce(ctx, chainID, address) + if err != nil { + return nil, fmt.Errorf("could not sign tx: %w", err) + } + + txType := t.txTypeForChain(chainID) + + transaction, err = util.CopyTX(transaction, util.WithNonce(newNonce), util.WithTxType(txType)) + if err != nil { + return nil, fmt.Errorf("could not copy tx: %w", err) + } + + //nolint: wrapcheck + return parentTransactor.Signer(address, transaction) } transactor.Signer = func(address common.Address, transaction *types.Transaction) (_ *types.Transaction, err error) { @@ -406,25 +421,42 @@ func (t *txSubmitterImpl) SubmitTransaction(parentCtx context.Context, chainID * } }() - newNonce, err := t.getNonce(ctx, chainID, address) - if err != nil { - return nil, fmt.Errorf("could not sign tx: %w", err) - } + return performSignature(address, transaction) + } - txType := t.txTypeForChain(chainID) + // if dynamic gas estimation is not enabled, use cfg var gas_estimate as a gas limit default and do not run a pre-flight simulation + // since we do not need it to determine proper gas units + if !t.config.GetDynamicGasEstimate(int(chainID.Uint64())) { + transactor.GasLimit = t.config.GetGasEstimate(int(chainID.Uint64())) + } else { - transaction, err = util.CopyTX(transaction, util.WithNonce(newNonce), util.WithTxType(txType)) + // deepcopy the real transactor so we can use it for simulation + transactorForGasEstimate := copyTransactOpts(transactor) + + // override the signer func for our simulation/estimation with a version that does not lock the nonce, + // which would othewrise cause a deadlock with the following *actual* transactor + transactorForGasEstimate.Signer = func(address common.Address, transaction *types.Transaction) (_ *types.Transaction, err error) { + return performSignature(address, transaction) + } + + txForGasEstimate, err := call(transactorForGasEstimate) if err != nil { - return nil, fmt.Errorf("could not copy tx: %w", err) + errMsg := util.FormatError(err) + + return 0, fmt.Errorf("err contract call for gas est: %s", errMsg) } - //nolint: wrapcheck - return parentTransactor.Signer(address, transaction) + // with our gas limit now obtained from the simulation, apply this limit (plus any configured % modifier) to the + // gas limit of the actual transactor that is about to prepare the real transaction + gasLimitAddPercentage := t.config.GetDynamicGasUnitAddPercentage(int(chainID.Uint64())) + transactor.GasLimit = txForGasEstimate.Gas() + (txForGasEstimate.Gas() * uint64(gasLimitAddPercentage) / 100) } + tx, err := call(transactor) if err != nil { - return 0, fmt.Errorf("could not call contract: %w", err) + return 0, fmt.Errorf("err contract call for tx: %w", err) } + defer locker.Unlock() // now that we've stored the tx @@ -676,18 +708,23 @@ func (t *txSubmitterImpl) getGasBlock(ctx context.Context, chainClient client.EV // getGasEstimate gets the gas estimate for the given transaction. // TODO: handle l2s w/ custom gas pricing through contracts. -func (t *txSubmitterImpl) getGasEstimate(ctx context.Context, chainClient client.EVM, chainID int, tx *types.Transaction) (gasEstimate uint64, err error) { +func (t *txSubmitterImpl) getGasEstimate(ctx context.Context, chainClient client.EVM, chainID int, tx *types.Transaction) (gasLimit uint64, err error) { + + // if dynamic gas estimation is not enabled, use cfg var gas_estimate as a default if !t.config.GetDynamicGasEstimate(chainID) { return t.config.GetGasEstimate(chainID), nil } + gasUnitAddPercentage := t.config.GetDynamicGasUnitAddPercentage(chainID) + ctx, span := t.metrics.Tracer().Start(ctx, "submitter.getGasEstimate", trace.WithAttributes( attribute.Int(metrics.ChainID, chainID), attribute.String(metrics.TxHash, tx.Hash().String()), + attribute.Int("gasUnitAddPercentage", gasUnitAddPercentage), )) defer func() { - span.AddEvent("estimated_gas", trace.WithAttributes(attribute.Int64("gas", int64(gasEstimate)))) + span.AddEvent("estimated_gas", trace.WithAttributes(attribute.Int64("gas", int64(gasLimit)))) metrics.EndSpanWithErr(span, err) }() @@ -696,21 +733,21 @@ func (t *txSubmitterImpl) getGasEstimate(ctx context.Context, chainClient client if err != nil { return 0, fmt.Errorf("could not convert tx to call: %w", err) } - // tmpdebug - fmt.Printf("Debug Calling EstimateGas") - gasEstimate, err = chainClient.EstimateGas(ctx, *call) + gasLimitFromEstimate, err := chainClient.EstimateGas(ctx, *call) + if err != nil { span.AddEvent("could not estimate gas", trace.WithAttributes(attribute.String("error", err.Error()))) - // tmpdebug - fmt.Printf("Debug Default Gas Estimate: %d\n", t.config.GetGasEstimate(chainID)) - - // fallback to default + // if we failed to est gas for any reason, use the default flat gas from config return t.config.GetGasEstimate(chainID), nil } - return gasEstimate, nil + // multiply the freshly simulated gasLimit by the configured gas unit add percentage + gasLimitFromEstimate += (gasLimitFromEstimate * uint64(gasUnitAddPercentage) / 100) + gasLimit = gasLimitFromEstimate + + return gasLimit, nil } func (t *txSubmitterImpl) Address() common.Address { diff --git a/ethergo/util/util.go b/ethergo/util/util.go new file mode 100644 index 0000000000..5df4ad2104 --- /dev/null +++ b/ethergo/util/util.go @@ -0,0 +1,17 @@ +package util + +import "strings" + +// FormatError applies custom formatting & noise reduction to error messages. Add more as needed. +func FormatError(err error) string { + if err == nil { + return "" + } + errMsg := err.Error() + + //if an error message contains embedded HTML (eg: many RPC errors), strip it out to reduce noise. + if strings.Contains(errMsg, "") { + errMsg = strings.Split(errMsg, "")[0] + "" + } + return errMsg +} diff --git a/services/rfq/contracts/fastbridgev2/events.go b/services/rfq/contracts/fastbridgev2/events.go index 2547ebad28..d33812112e 100644 --- a/services/rfq/contracts/fastbridgev2/events.go +++ b/services/rfq/contracts/fastbridgev2/events.go @@ -20,6 +20,8 @@ var ( BridgeDepositClaimedTopic common.Hash // BridgeProofDisputedTopic is the topic emitted by a bridge dispute. BridgeProofDisputedTopic common.Hash + // BridgeQuoteDetailsTopic is a secondary topic emitted by a bridge request. + BridgeQuoteDetailsTopic common.Hash ) // static checks to make sure topics actually exist. @@ -36,6 +38,7 @@ func init() { BridgeProofProvidedTopic = parsedABI.Events["BridgeProofProvided"].ID BridgeDepositClaimedTopic = parsedABI.Events["BridgeDepositClaimed"].ID BridgeProofDisputedTopic = parsedABI.Events["BridgeProofDisputed"].ID + BridgeQuoteDetailsTopic = parsedABI.Events["BridgeQuoteDetails"].ID _, err = parsedABI.EventByID(BridgeRequestedTopic) if err != nil { @@ -56,6 +59,16 @@ func init() { if err != nil { panic(err) } + + _, err = parsedABI.EventByID(BridgeDepositClaimedTopic) + if err != nil { + panic(err) + } + + _, err = parsedABI.EventByID(BridgeQuoteDetailsTopic) + if err != nil { + panic(err) + } } // topicMap maps events to topics. @@ -67,6 +80,7 @@ func topicMap() map[EventType]common.Hash { BridgeProofProvidedEvent: BridgeProofProvidedTopic, BridgeDepositClaimedEvent: BridgeDepositClaimedTopic, BridgeDisputeEvent: BridgeProofDisputedTopic, + BridgeQuoteDetailsEvent: BridgeQuoteDetailsTopic, } } diff --git a/services/rfq/contracts/fastbridgev2/eventtype_string.go b/services/rfq/contracts/fastbridgev2/eventtype_string.go index b7c65ba096..9fd676f18d 100644 --- a/services/rfq/contracts/fastbridgev2/eventtype_string.go +++ b/services/rfq/contracts/fastbridgev2/eventtype_string.go @@ -13,11 +13,12 @@ func _() { _ = x[BridgeProofProvidedEvent-3] _ = x[BridgeDepositClaimedEvent-4] _ = x[BridgeDisputeEvent-5] + _ = x[BridgeQuoteDetailsEvent-6] } -const _EventType_name = "BridgeRequestedEventBridgeRelayedEventBridgeProofProvidedEventBridgeDepositClaimedEventBridgeDisputeEvent" +const _EventType_name = "BridgeRequestedEventBridgeRelayedEventBridgeProofProvidedEventBridgeDepositClaimedEventBridgeDisputeEventBridgeQuoteDetailsEvent" -var _EventType_index = [...]uint8{0, 20, 38, 62, 87, 105} +var _EventType_index = [...]uint8{0, 20, 38, 62, 87, 105, 127} func (i EventType) String() string { i -= 1 diff --git a/services/rfq/contracts/fastbridgev2/parser.go b/services/rfq/contracts/fastbridgev2/parser.go index 5fe9d66570..4cc4870e31 100644 --- a/services/rfq/contracts/fastbridgev2/parser.go +++ b/services/rfq/contracts/fastbridgev2/parser.go @@ -23,6 +23,8 @@ const ( BridgeDepositClaimedEvent // BridgeDisputeEvent is the event type for the BridgeDispute event. BridgeDisputeEvent + // BridgeQuoteDetailsEvent is emitted along w/ BridgeRequestedEvent as supplemental data + BridgeQuoteDetailsEvent ) // Parser parses events from the fastbridge contracat. @@ -91,6 +93,12 @@ func (p parserImpl) ParseEvent(log ethTypes.Log) (_ EventType, event interface{} return noOpEvent, nil, false } return eventType, disputed, true + case BridgeQuoteDetailsEvent: + quoteDetails, err := p.filterer.ParseBridgeQuoteDetails(log) + if err != nil { + return noOpEvent, nil, false + } + return eventType, quoteDetails, true } return eventType, nil, true diff --git a/services/rfq/relayer/chain/chain.go b/services/rfq/relayer/chain/chain.go index 7fed1ff887..3ec2da5960 100644 --- a/services/rfq/relayer/chain/chain.go +++ b/services/rfq/relayer/chain/chain.go @@ -86,10 +86,19 @@ func (c Chain) SubmitRelay(ctx context.Context, request reldb.QuoteRequest) (uin } } + fmt.Printf( + "TxID 0x%x %7d.%s > %7d.%s : Submitting \033[32mRelay\033[0m\n", + request.TransactionID, + request.Transaction.OriginChainId, + request.Transaction.OriginToken.Hex()[:6], + request.Transaction.DestChainId, + request.Transaction.DestToken.Hex()[:6]) + nonce, err := c.SubmitTransaction(ctx, func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { transactor.Value = core.CopyBigInt(gasAmount) tx, err = c.Bridge.RelayV2(transactor, request.RawRequest, c.submitter.Address()) + if err != nil { return nil, fmt.Errorf("could not relay: %w", err) } diff --git a/services/rfq/relayer/pricer/fee_pricer.go b/services/rfq/relayer/pricer/fee_pricer.go index b259497b01..5d06ac3829 100644 --- a/services/rfq/relayer/pricer/fee_pricer.go +++ b/services/rfq/relayer/pricer/fee_pricer.go @@ -14,11 +14,12 @@ import ( "github.com/jellydator/ttlcache/v3" "github.com/synapsecns/sanguine/core/metrics" "github.com/synapsecns/sanguine/ethergo/submitter" + ethergoUtil "github.com/synapsecns/sanguine/ethergo/util" "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridgev2" "github.com/synapsecns/sanguine/services/rfq/relayer/chain" "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" - "github.com/synapsecns/sanguine/services/rfq/util" + rfqUtil "github.com/synapsecns/sanguine/services/rfq/util" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) @@ -255,15 +256,19 @@ func (f *feePricer) getZapGasEstimate(ctx context.Context, destination uint32, q Data: encodedData, } // Tx.value needs to match `DestAmount` for native gas token, or `ZapNative` for ERC20s. - if util.IsGasToken(quoteRequest.Transaction.DestToken) { + if rfqUtil.IsGasToken(quoteRequest.Transaction.DestToken) { callMsg.Value = quoteRequest.Transaction.DestAmount } else { callMsg.Value = quoteRequest.Transaction.ZapNative } + // note: this gas amount is intentionally not modified gasEstimate, err = client.EstimateGas(ctx, callMsg) if err != nil { - return 0, fmt.Errorf("could not estimate gas: %w", err) + + errMsg := ethergoUtil.FormatError(err) + + return 0, fmt.Errorf("could not estimate gas: %s", errMsg) } return gasEstimate, nil diff --git a/services/rfq/relayer/quoter/quoter.go b/services/rfq/relayer/quoter/quoter.go index 6456f08154..9b96b7bee5 100644 --- a/services/rfq/relayer/quoter/quoter.go +++ b/services/rfq/relayer/quoter/quoter.go @@ -693,18 +693,12 @@ func (m *Manager) generateQuote(ctx context.Context, input QuoteInput) (quote *m return nil, fmt.Errorf("error getting total fee: %w", err) } - // tmpdebug - fmt.Printf("Debug Total Fee Amt: %s\n", fee.String()) - originRFQAddr, err := m.config.GetRFQAddress(input.OriginChainID) if err != nil { logger.Error("Error getting RFQ address", "error", err) return nil, fmt.Errorf("error getting RFQ address: %w", err) } - // tmpdebug - fmt.Printf("Debug originRFQAddr: %s\n", originRFQAddr.String()) - // Build the quote destAmount, err := m.getDestAmount(ctx, originAmount, destToken, input) if err != nil { @@ -712,9 +706,6 @@ func (m *Manager) generateQuote(ctx context.Context, input QuoteInput) (quote *m return nil, fmt.Errorf("error getting dest amount: %w", err) } - // tmpdebug - fmt.Printf("Debug destAmount: %s\n", destAmount.String()) - quote = &model.PutRelayerQuoteRequest{ OriginChainID: input.OriginChainID, OriginTokenAddr: input.OriginTokenAddr.Hex(), diff --git a/services/rfq/relayer/service/handlers.go b/services/rfq/relayer/service/handlers.go index 5856861976..1016af4a70 100644 --- a/services/rfq/relayer/service/handlers.go +++ b/services/rfq/relayer/service/handlers.go @@ -376,6 +376,7 @@ func (q *QuoteRequestHandler) handleCommitPending(ctx context.Context, span trac // This is the fourth step in the bridge process. Here we submit the relay transaction to the destination chain. // TODO: just to be safe, we should probably check if another relayer has already relayed this. func (q *QuoteRequestHandler) handleCommitConfirmed(ctx context.Context, span trace.Span, request reldb.QuoteRequest) (err error) { + // TODO: store the dest txhash connected to the nonce nonce, _, err := q.Dest.SubmitRelay(ctx, request) if err != nil { @@ -412,7 +413,7 @@ func (r *Relayer) handleRelayLog(parentCtx context.Context, req *fastbridgev2.Fa reqID, err := r.db.GetQuoteRequestByID(ctx, req.TransactionId) if err != nil { - return fmt.Errorf("could not get quote request: %w", err) + return fmt.Errorf("could not get quote request for tx ID %s: %w", hexutil.Encode(req.TransactionId[:]), err) } // we might've accidentally gotten this later, if so we'll just ignore it // note that in the edge case where we pessimistically marked as DeadlineExceeded @@ -470,11 +471,19 @@ func (q *QuoteRequestHandler) handleRelayCompleted(ctx context.Context, span tra return nil } + fmt.Printf( + "TxID 0x%x %7d.%s > %7d.%s : Submitting \033[33mProof\033[0m\n", + request.TransactionID, + request.Transaction.OriginChainId, + request.Transaction.OriginToken.Hex()[:6], + request.Transaction.DestChainId, + request.Transaction.DestToken.Hex()[:6]) + // relay has been finalized, it's time to go back to the origin chain and try to prove _, err = q.Origin.SubmitTransaction(ctx, func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { tx, err = q.Origin.Bridge.Prove(transactor, request.RawRequest, request.DestTxHash) if err != nil { - return nil, fmt.Errorf("could not relay: %w", err) + return nil, fmt.Errorf("could not prove: %w", err) } return tx, nil @@ -616,7 +625,17 @@ func (q *QuoteRequestHandler) handleProofPosted(ctx context.Context, span trace. if !canClaim { return nil } + + fmt.Printf( + "TxID 0x%x %7d.%s > %7d.%s : Submitting \033[35mClaim\033[0m\n", + request.TransactionID, + request.Transaction.OriginChainId, + request.Transaction.OriginToken.Hex()[:6], + request.Transaction.DestChainId, + request.Transaction.DestToken.Hex()[:6]) + _, err = q.Origin.SubmitTransaction(ctx, func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { + tx, err = q.Origin.Bridge.Claim(transactor, request.RawRequest, transactor.From) if err != nil { return nil, fmt.Errorf("could not relay: %w", err)