Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(rfq-relayer): gas estimation for zaps #3413

Merged
merged 21 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/contracts-rfq/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"lint": "forge fmt && npm run solhint",
"lint:check": "forge fmt --check && npm run solhint:check",
"ci:lint": "npm run lint:check",
"build:go": "./flatten.sh contracts/*.sol test/*.sol test/mocks/*.sol",
"build:go": "./flatten.sh contracts/*.sol test/*.sol test/mocks/*.sol test/harnesses/*.sol",
"solhint": "solhint '{contracts,script,test}/**/*.sol' --fix --noPrompt --max-warnings 3",
"solhint:check": "solhint '{contracts,script,test}/**/*.sol' --max-warnings 3"
}
Expand Down
2 changes: 2 additions & 0 deletions services/rfq/api/model/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ type QuoteData struct {
ZapData string `json:"zap_data"`
ZapNative string `json:"zap_native"`
OriginAmountExact string `json:"origin_amount_exact"`
OriginSender string `json:"origin_sender"`
DestRecipient string `json:"dest_recipient"`
DestAmount *string `json:"dest_amount"`
RelayerAddress *string `json:"relayer_address"`
QuoteID *string `json:"quote_id"`
Expand Down
4,004 changes: 4,004 additions & 0 deletions services/rfq/contracts/bridgetransactionv2/bridgetransactionv2.abigen.go

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions services/rfq/contracts/bridgetransactionv2/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Package bridgetransactionv2 is the bridge transaction contract.
package bridgetransactionv2

//go:generate go run github.com/synapsecns/sanguine/tools/abigen generate --sol ../../../../packages/contracts-rfq/flattened/BridgeTransactionV2Harness.sol --pkg bridgetransactionv2 --sol-version 0.8.24 --filename bridgetransactionv2 --evm-version istanbul
35 changes: 35 additions & 0 deletions services/rfq/contracts/bridgetransactionv2/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package bridgetransactionv2

import (
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
)

// BridgeTransactionV2Ref is a bound fast bridge contract that returns the address of the contract.
//
//nolint:golint
type BridgeTransactionV2Ref struct {
*BridgeTransactionV2Harness
address common.Address
}

// Address gets the ocntract address.
func (f *BridgeTransactionV2Ref) Address() common.Address {
return f.address
}

// NewBridgeTransactionV2Ref creates a new fast bridge mock contract with a ref.
func NewBridgeTransactionV2Ref(address common.Address, backend bind.ContractBackend) (*BridgeTransactionV2Ref, error) {
bridgetransactionv2, err := NewBridgeTransactionV2Harness(address, backend)
if err != nil {
return nil, err
}

return &BridgeTransactionV2Ref{
BridgeTransactionV2Harness: bridgetransactionv2,
address: address,
}, nil
}

var _ vm.ContractRef = &BridgeTransactionV2Ref{}
108 changes: 108 additions & 0 deletions services/rfq/e2e/rfq_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,19 @@ import (
cctpTest "github.com/synapsecns/sanguine/services/cctp-relayer/testutil"
omnirpcClient "github.com/synapsecns/sanguine/services/omnirpc/client"
"github.com/synapsecns/sanguine/services/rfq/api/client"
"github.com/synapsecns/sanguine/services/rfq/contracts/bridgetransactionv2"
"github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge"
"github.com/synapsecns/sanguine/services/rfq/contracts/fastbridgev2"
"github.com/synapsecns/sanguine/services/rfq/guard/guarddb"
guardService "github.com/synapsecns/sanguine/services/rfq/guard/service"
"github.com/synapsecns/sanguine/services/rfq/relayer/chain"
"github.com/synapsecns/sanguine/services/rfq/relayer/reldb"
"github.com/synapsecns/sanguine/services/rfq/relayer/service"
"github.com/synapsecns/sanguine/services/rfq/testutil"
"github.com/synapsecns/sanguine/services/rfq/util"
"golang.org/x/sync/errgroup"

"github.com/brianvoe/gofakeit/v6"
)

type IntegrationSuite struct {
Expand Down Expand Up @@ -773,3 +777,107 @@ func (i *IntegrationSuite) TestConcurrentBridges() {
return true
})
}

//nolint:gosec
func (i *IntegrationSuite) TestEncodeBridgeTransactionParity() {
_, handle := i.manager.GetBridgeTransactionV2(i.GetTestContext(), i.originBackend)

mockAddress := func() common.Address {
// Generate 20 random bytes for the address
b := make([]byte, 20)
for i := range b {
b[i] = byte(gofakeit.Number(0, 255))
}
return common.BytesToAddress(b)
}

// Generate random values that will be used for both transactions
originChainId := uint32(gofakeit.Number(1, 1000000))
destChainId := uint32(gofakeit.Number(1, 1000000))
originSender := mockAddress()
destRecipient := mockAddress()
originToken := mockAddress()
destToken := mockAddress()
originAmount := new(big.Int).SetUint64(gofakeit.Uint64())
destAmount := new(big.Int).SetUint64(gofakeit.Uint64())
originFeeAmount := new(big.Int).SetUint64(gofakeit.Uint64())
deadline := new(big.Int).SetUint64(gofakeit.Uint64())
nonce := new(big.Int).SetUint64(gofakeit.Uint64())
exclusivityRelayer := mockAddress()
exclusivityEndTime := new(big.Int).SetUint64(gofakeit.Uint64())
zapNative := new(big.Int).SetUint64(gofakeit.Uint64())

// Random size and values for zapData
zapDataSize := gofakeit.Number(0, 1000)
zapData := make([]byte, zapDataSize)
for i := range zapDataSize {
zapData[i] = gofakeit.Uint8()
}

// Create first transaction
bridgeTx := bridgetransactionv2.IFastBridgeV2BridgeTransactionV2{
OriginChainId: originChainId,
DestChainId: destChainId,
OriginSender: originSender,
DestRecipient: destRecipient,
OriginToken: originToken,
DestToken: destToken,
OriginAmount: originAmount,
DestAmount: destAmount,
OriginFeeAmount: originFeeAmount,
Deadline: deadline,
Nonce: nonce,
ExclusivityRelayer: exclusivityRelayer,
ExclusivityEndTime: exclusivityEndTime,
ZapNative: zapNative,
ZapData: zapData,
}

// Create second transaction with same values
tx := fastbridgev2.IFastBridgeV2BridgeTransactionV2{
OriginChainId: originChainId,
DestChainId: destChainId,
OriginSender: originSender,
DestRecipient: destRecipient,
OriginToken: originToken,
DestToken: destToken,
OriginAmount: originAmount,
DestAmount: destAmount,
OriginFeeAmount: originFeeAmount,
Deadline: deadline,
Nonce: nonce,
ExclusivityRelayer: exclusivityRelayer,
ExclusivityEndTime: exclusivityEndTime,
ZapNative: zapNative,
ZapData: zapData,
}

expectedEncoded, err := handle.EncodeV2(&bind.CallOpts{Context: i.GetTestContext()}, bridgeTx)
i.NoError(err)

encoded, err := chain.EncodeBridgeTx(tx)
i.NoError(err)

i.Equal(expectedEncoded, encoded)

// Test decoding
decodedTx, err := chain.DecodeBridgeTx(encoded)
i.NoError(err)

// Verify all fields match the original transaction
i.Equal(tx.OriginChainId, decodedTx.OriginChainId)
i.Equal(tx.DestChainId, decodedTx.DestChainId)
i.Equal(tx.OriginSender, decodedTx.OriginSender)
i.Equal(tx.DestRecipient, decodedTx.DestRecipient)
i.Equal(tx.OriginToken, decodedTx.OriginToken)
i.Equal(tx.DestToken, decodedTx.DestToken)
i.Equal(tx.OriginAmount.String(), decodedTx.OriginAmount.String())
i.Equal(tx.DestAmount.String(), decodedTx.DestAmount.String())
i.Equal(tx.OriginFeeAmount.String(), decodedTx.OriginFeeAmount.String())
i.Equal(tx.Deadline.String(), decodedTx.Deadline.String())
i.Equal(tx.Nonce.String(), decodedTx.Nonce.String())
i.Equal(tx.ExclusivityRelayer, decodedTx.ExclusivityRelayer)
i.Equal(tx.ExclusivityEndTime.String(), decodedTx.ExclusivityEndTime.String())
i.Equal(tx.ZapNative.String(), decodedTx.ZapNative.String())
i.Equal(tx.ZapData, decodedTx.ZapData)
}
113 changes: 113 additions & 0 deletions services/rfq/relayer/chain/encoding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package chain

import (
"encoding/binary"
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/synapsecns/sanguine/services/rfq/contracts/fastbridgev2"
)

const (
// Field sizes in bytes.
sizeVersion = 2
sizeChainID = 4
sizeAddress = 20
sizeUint256 = 32

// Field offsets in bytes.
offsetVersion = 0
offsetOriginChainID = offsetVersion + sizeVersion
offsetDestChainID = offsetOriginChainID + sizeChainID
offsetOriginSender = offsetDestChainID + sizeChainID
offsetDestRecipient = offsetOriginSender + sizeAddress
offsetOriginToken = offsetDestRecipient + sizeAddress
offsetDestToken = offsetOriginToken + sizeAddress
offsetOriginAmount = offsetDestToken + sizeAddress
offsetDestAmount = offsetOriginAmount + sizeUint256
offsetOriginFeeAmount = offsetDestAmount + sizeUint256
offsetDeadline = offsetOriginFeeAmount + sizeUint256
offsetNonce = offsetDeadline + sizeUint256
offsetExclusivityRelayer = offsetNonce + sizeUint256
offsetExclusivityEndTime = offsetExclusivityRelayer + sizeAddress
offsetZapNative = offsetExclusivityEndTime + sizeUint256
offsetZapData = offsetZapNative + sizeUint256
)

// Helper function to properly encode uint256.
func padUint256(b *big.Int) []byte {
// Convert big.Int to bytes
bytes := b.Bytes()
// Create 32-byte array (initialized to zeros)
result := make([]byte, 32)
// Copy bytes to right side of array (left-pad with zeros)
copy(result[32-len(bytes):], bytes)
return result
}

// EncodeBridgeTx encodes a bridge transaction into a byte array.
func EncodeBridgeTx(tx fastbridgev2.IFastBridgeV2BridgeTransactionV2) ([]byte, error) {
// Initialize with total size including ZapData
result := make([]byte, offsetZapData+len(tx.ZapData))

// Version
result[offsetVersion] = 0
result[offsetVersion+1] = 2

// Chain IDs
binary.BigEndian.PutUint32(result[offsetOriginChainID:offsetOriginChainID+sizeChainID], tx.OriginChainId)
binary.BigEndian.PutUint32(result[offsetDestChainID:offsetDestChainID+sizeChainID], tx.DestChainId)

// Addresses
copy(result[offsetOriginSender:offsetOriginSender+sizeAddress], tx.OriginSender.Bytes())
copy(result[offsetDestRecipient:offsetDestRecipient+sizeAddress], tx.DestRecipient.Bytes())
copy(result[offsetOriginToken:offsetOriginToken+sizeAddress], tx.OriginToken.Bytes())
copy(result[offsetDestToken:offsetDestToken+sizeAddress], tx.DestToken.Bytes())

// uint256 values
copy(result[offsetOriginAmount:offsetOriginAmount+sizeUint256], padUint256(tx.OriginAmount))
copy(result[offsetDestAmount:offsetDestAmount+sizeUint256], padUint256(tx.DestAmount))
copy(result[offsetOriginFeeAmount:offsetOriginFeeAmount+sizeUint256], padUint256(tx.OriginFeeAmount))
copy(result[offsetDeadline:offsetDeadline+sizeUint256], padUint256(tx.Deadline))
copy(result[offsetNonce:offsetNonce+sizeUint256], padUint256(tx.Nonce))

// Exclusivity address
copy(result[offsetExclusivityRelayer:offsetExclusivityRelayer+sizeAddress], tx.ExclusivityRelayer.Bytes())

// More uint256 values
copy(result[offsetExclusivityEndTime:offsetExclusivityEndTime+sizeUint256], padUint256(tx.ExclusivityEndTime))
copy(result[offsetZapNative:offsetZapNative+sizeUint256], padUint256(tx.ZapNative))

// Replace append with copy for ZapData
copy(result[offsetZapData:], tx.ZapData)

return result, nil
}

// DecodeBridgeTx decodes a byte array into a bridge transaction.
func DecodeBridgeTx(data []byte) (fastbridgev2.IFastBridgeV2BridgeTransactionV2, error) {
if len(data) < offsetZapData {
return fastbridgev2.IFastBridgeV2BridgeTransactionV2{}, fmt.Errorf("data too short: got %d bytes, need at least %d", len(data), offsetZapData)
}

tx := fastbridgev2.IFastBridgeV2BridgeTransactionV2{
OriginChainId: binary.BigEndian.Uint32(data[offsetOriginChainID:offsetDestChainID]),
DestChainId: binary.BigEndian.Uint32(data[offsetDestChainID:offsetOriginSender]),
OriginSender: common.BytesToAddress(data[offsetOriginSender:offsetDestRecipient]),
DestRecipient: common.BytesToAddress(data[offsetDestRecipient:offsetOriginToken]),
OriginToken: common.BytesToAddress(data[offsetOriginToken:offsetDestToken]),
DestToken: common.BytesToAddress(data[offsetDestToken:offsetOriginAmount]),
OriginAmount: new(big.Int).SetBytes(data[offsetOriginAmount:offsetDestAmount]),
DestAmount: new(big.Int).SetBytes(data[offsetDestAmount:offsetOriginFeeAmount]),
OriginFeeAmount: new(big.Int).SetBytes(data[offsetOriginFeeAmount:offsetDeadline]),
Deadline: new(big.Int).SetBytes(data[offsetDeadline:offsetNonce]),
Nonce: new(big.Int).SetBytes(data[offsetNonce:offsetExclusivityRelayer]),
ExclusivityRelayer: common.BytesToAddress(data[offsetExclusivityRelayer:offsetExclusivityEndTime]),
ExclusivityEndTime: new(big.Int).SetBytes(data[offsetExclusivityEndTime:offsetZapNative]),
ZapNative: new(big.Int).SetBytes(data[offsetZapNative:offsetZapData]),
ZapData: data[offsetZapData:],
}

return tx, nil
}
12 changes: 9 additions & 3 deletions services/rfq/relayer/pricer/fee_pricer.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/synapsecns/sanguine/core/metrics"
"github.com/synapsecns/sanguine/ethergo/submitter"
"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"
"go.opentelemetry.io/otel/attribute"
Expand Down Expand Up @@ -141,7 +142,7 @@ func (f *feePricer) GetDestinationFee(parentCtx context.Context, _, destination

// Calculate the static L2 fee if it won't be incorporated by directly estimating the relay() call
// in addZapFees().
if quoteRequest == nil || quoteRequest.Transaction.ZapData == nil || len(quoteRequest.Transaction.ZapData) == 0 {
if quoteRequest == nil || quoteRequest.Transaction.ZapNative == nil || quoteRequest.Transaction.ZapData == nil {
gasEstimate, err := f.config.GetDestGasEstimate(int(destination))
if err != nil {
return nil, fmt.Errorf("could not get dest gas estimate: %w", err)
Expand Down Expand Up @@ -183,7 +184,7 @@ func (f *feePricer) GetDestinationFee(parentCtx context.Context, _, destination
func (f *feePricer) addZapFees(ctx context.Context, destination uint32, denomToken string, quoteRequest *reldb.QuoteRequest, fee *big.Int) (*big.Int, error) {
span := trace.SpanFromContext(ctx)

if len(quoteRequest.Transaction.ZapData) > 0 {
if quoteRequest.Transaction.ZapData != nil {
gasEstimate, err := f.getZapGasEstimate(ctx, destination, quoteRequest)
if err != nil {
return nil, err
Expand Down Expand Up @@ -232,7 +233,12 @@ func (f *feePricer) getZapGasEstimate(ctx context.Context, destination uint32, q
fastBridgeV2ABI = &parsedABI
}

encodedData, err := fastBridgeV2ABI.Pack(methodName, quoteRequest.RawRequest, f.relayerAddress)
rawRequest, err := chain.EncodeBridgeTx(quoteRequest.Transaction)
if err != nil {
return 0, fmt.Errorf("could not encode quote data: %w", err)
}

encodedData, err := fastBridgeV2ABI.Pack(methodName, rawRequest, f.relayerAddress)
Comment on lines 235 to +241
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix potential data race on fastBridgeV2ABI during concurrent access.

The package-level variable fastBridgeV2ABI is accessed without synchronization, which can lead to data races if getZapGasEstimate is called concurrently. Use sync.Once or a mutex to ensure thread-safe initialization.

Apply this diff to fix the issue:

+var fastBridgeV2ABIOnce sync.Once
 // Existing variable
 var fastBridgeV2ABI *abi.ABI

 const methodName = "relay0"

 func (f *feePricer) getZapGasEstimate(ctx context.Context, destination uint32, quoteRequest *reldb.QuoteRequest) (gasEstimate uint64, err error) {
 	client, err := f.clientFetcher.GetClient(ctx, big.NewInt(int64(destination)))
 	if err != nil {
 		return 0, fmt.Errorf("could not get client: %w", err)
 	}

-	if fastBridgeV2ABI == nil {
+	fastBridgeV2ABIOnce.Do(func() {
 		parsedABI, err := abi.JSON(strings.NewReader(fastbridgev2.IFastBridgeV2MetaData.ABI))
 		if err != nil {
-			return 0, fmt.Errorf("could not parse ABI: %w", err)
+			panic(fmt.Sprintf("could not parse ABI: %v", err))
 		}
 		fastBridgeV2ABI = &parsedABI
+	})

Committable suggestion skipped: line range outside the PR's diff.

if err != nil {
return 0, fmt.Errorf("could not encode function call: %w", err)
}
Expand Down
Loading
Loading