diff --git a/execute/exectypes/costly_messages.go b/execute/exectypes/costly_messages.go index 498b8223d..fba8d61e2 100644 --- a/execute/exectypes/costly_messages.go +++ b/execute/exectypes/costly_messages.go @@ -7,9 +7,10 @@ import ( "time" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-ccip/execute/internal/gas" - + "github.com/smartcontractkit/chainlink-ccip/internal/libs/mathslib" "github.com/smartcontractkit/chainlink-ccip/internal/plugintypes" readerpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" @@ -118,6 +119,8 @@ func (o *CCIPCostlyMessageObserver) Observe( return nil, fmt.Errorf("missing exec cost for message %s", msg.Header.MessageID) } if fee.Cmp(execCost) < 0 { + o.lggr.Warnw("Message is too costly to execute", "messageID", + msg.Header.MessageID.String(), "fee", fee, "execCost", execCost, "seqNum", msg.Header.SequenceNumber) costlyMessages = append(costlyMessages, msg.Header.MessageID) } } @@ -297,7 +300,10 @@ func (c *CCIPMessageFeeUSD18Calculator) MessageFeeUSD18( messageFees := make(map[cciptypes.Bytes32]plugintypes.USD18) for _, msg := range messages { - feeUSD18 := new(big.Int).Mul(linkPriceUSD.Int, msg.FeeValueJuels.Int) + feeUSD18 := new(big.Int).Div( + new(big.Int).Mul(linkPriceUSD.Int, msg.FeeValueJuels.Int), + new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil), + ) timestamp, ok := messageTimeStamps[msg.Header.MessageID] if !ok { // If a timestamp is missing we can't do fee boosting, but we still record the fee. In the worst case, the @@ -319,7 +325,10 @@ func (c *CCIPMessageFeeUSD18Calculator) MessageFeeUSD18( // At the same time, messages that are slightly underpaid will start going through after waiting for a little bit. // // wait_boosted_fee(m) = (1 + (now - m.send_time).hours * RELATIVE_BOOST_PER_WAIT_HOUR) * fee(m) -func waitBoostedFee(waitTime time.Duration, fee *big.Int, relativeBoostPerWaitHour float64) *big.Int { +func waitBoostedFee( + waitTime time.Duration, + fee *big.Int, + relativeBoostPerWaitHour float64) *big.Int { k := 1.0 + waitTime.Hours()*relativeBoostPerWaitHour boostedFee := big.NewFloat(0).Mul(big.NewFloat(k), new(big.Float).SetInt(fee)) @@ -350,14 +359,20 @@ func (c *CCIPMessageExecCostUSD18Calculator) MessageExecCostUSD18( if feeComponents.DataAvailabilityFee == nil { return nil, fmt.Errorf("missing data availability fee") } + + executionFee, daFee, err := c.getFeesUSD18(ctx, feeComponents, messages[0].Header.DestChainSelector) + if err != nil { + return nil, fmt.Errorf("unable to convert fee components to USD18: %w", err) + } + daConfig, err := c.ccipReader.GetMedianDataAvailabilityGasConfig(ctx) if err != nil { return nil, fmt.Errorf("unable to get data availability gas config: %w", err) } for _, msg := range messages { - executionCostUSD18 := c.computeExecutionCostUSD18(feeComponents.ExecutionFee, msg) - dataAvailabilityCostUSD18 := computeDataAvailabilityCostUSD18(feeComponents.DataAvailabilityFee, daConfig, msg) + executionCostUSD18 := c.computeExecutionCostUSD18(executionFee, msg) + dataAvailabilityCostUSD18 := c.computeDataAvailabilityCostUSD18(daFee, daConfig, msg) totalCostUSD18 := new(big.Int).Add(executionCostUSD18, dataAvailabilityCostUSD18) messageExecCosts[msg.Header.MessageID] = totalCostUSD18 } @@ -365,6 +380,34 @@ func (c *CCIPMessageExecCostUSD18Calculator) MessageExecCostUSD18( return messageExecCosts, nil } +func (c *CCIPMessageExecCostUSD18Calculator) getFeesUSD18( + ctx context.Context, + feeComponents types.ChainFeeComponents, + destChainSelector cciptypes.ChainSelector, +) (plugintypes.USD18, plugintypes.USD18, error) { + nativeTokenPrices := c.ccipReader.GetWrappedNativeTokenPriceUSD( + ctx, + []cciptypes.ChainSelector{destChainSelector}) + if nativeTokenPrices == nil { + return nil, nil, fmt.Errorf("unable to get native token prices") + } + nativeTokenPrice, ok := nativeTokenPrices[destChainSelector] + if !ok { + return nil, nil, fmt.Errorf("missing native token price for chain %s", destChainSelector) + } + + executionFee := mathslib.CalculateUsdPerUnitGas(feeComponents.ExecutionFee, nativeTokenPrice.Int) + dataAvailabilityFee := mathslib.CalculateUsdPerUnitGas(feeComponents.DataAvailabilityFee, nativeTokenPrice.Int) + + c.lggr.Debugw("Fee calculation", "nativeTokenPrice", nativeTokenPrice, + "feeComponents.ExecutionFee", feeComponents.ExecutionFee, + "feeComponents.DataAvailabilityFee", feeComponents.DataAvailabilityFee, + "executionFee", executionFee, + "dataAvailabilityFee", dataAvailabilityFee) + + return executionFee, dataAvailabilityFee, nil +} + // computeExecutionCostUSD18 computes the execution cost of a message in USD18s. // The cost is: // messageGas (gas) * executionFee (USD18/gas) = USD18 @@ -373,12 +416,11 @@ func (c *CCIPMessageExecCostUSD18Calculator) computeExecutionCostUSD18( message cciptypes.Message, ) plugintypes.USD18 { messageGas := new(big.Int).SetUint64(c.estimateProvider.CalculateMessageMaxGas(message)) - cost := new(big.Int).Mul(messageGas, executionFee) - return cost + return new(big.Int).Mul(messageGas, executionFee) } // computeDataAvailabilityCostUSD18 computes the data availability cost of a message in USD18s. -func computeDataAvailabilityCostUSD18( +func (c *CCIPMessageExecCostUSD18Calculator) computeDataAvailabilityCostUSD18( dataAvailabilityFee *big.Int, daConfig cciptypes.DataAvailabilityGasConfig, message cciptypes.Message, diff --git a/execute/exectypes/costly_messages_test.go b/execute/exectypes/costly_messages_test.go index 86bd9cd14..dafa6de84 100644 --- a/execute/exectypes/costly_messages_test.go +++ b/execute/exectypes/costly_messages_test.go @@ -207,7 +207,6 @@ func TestWaitBoostedFee(t *testing.T) { 0.7, }, } - for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { boosted := waitBoostedFee(tc.sendTimeDiff, tc.fee, tc.relativeBoostPerWaitHour) @@ -291,6 +290,10 @@ func TestCCIPMessageFeeE18USDCalculator_MessageFeeE18USD(t *testing.T) { } func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { + destChainSelector := ccipocr3.ChainSelector(1) + nativeTokenPrice := ccipocr3.BigInt{ + Int: new(big.Int).Mul(big.NewInt(9), new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil))} + tests := []struct { name string messages []ccipocr3.Message @@ -306,17 +309,17 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { name: "happy path, no DA cost", messages: []ccipocr3.Message{ { - Header: ccipocr3.RampMessageHeader{MessageID: b1}, + Header: ccipocr3.RampMessageHeader{MessageID: b1, DestChainSelector: destChainSelector}, }, { - Header: ccipocr3.RampMessageHeader{MessageID: b2}, + Header: ccipocr3.RampMessageHeader{MessageID: b2, DestChainSelector: destChainSelector}, }, { - Header: ccipocr3.RampMessageHeader{MessageID: b3}, + Header: ccipocr3.RampMessageHeader{MessageID: b3, DestChainSelector: destChainSelector}, }, }, messageGases: []uint64{100, 200, 300}, - executionFee: big.NewInt(100), + executionFee: new(big.Int).Mul(big.NewInt(20), new(big.Int).Exp(big.NewInt(10), big.NewInt(9), nil)), dataAvailabilityFee: big.NewInt(0), feeComponentsError: nil, daGasConfig: ccipocr3.DataAvailabilityGasConfig{ @@ -325,9 +328,9 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { DestDataAvailabilityMultiplierBps: 1, }, want: map[ccipocr3.Bytes32]plugintypes.USD18{ - b1: plugintypes.NewUSD18(10000), - b2: plugintypes.NewUSD18(20000), - b3: plugintypes.NewUSD18(30000), + b1: plugintypes.NewUSD18(18000000000000), + b2: plugintypes.NewUSD18(36000000000000), + b3: plugintypes.NewUSD18(54000000000000), }, wantErr: false, }, @@ -335,18 +338,18 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { name: "happy path, with DA cost", messages: []ccipocr3.Message{ { - Header: ccipocr3.RampMessageHeader{MessageID: b1}, + Header: ccipocr3.RampMessageHeader{MessageID: b1, DestChainSelector: destChainSelector}, }, { - Header: ccipocr3.RampMessageHeader{MessageID: b2}, + Header: ccipocr3.RampMessageHeader{MessageID: b2, DestChainSelector: destChainSelector}, }, { - Header: ccipocr3.RampMessageHeader{MessageID: b3}, + Header: ccipocr3.RampMessageHeader{MessageID: b3, DestChainSelector: destChainSelector}, }, }, messageGases: []uint64{100, 200, 300}, - executionFee: big.NewInt(100), - dataAvailabilityFee: big.NewInt(400), + executionFee: new(big.Int).Mul(big.NewInt(20), new(big.Int).Exp(big.NewInt(10), big.NewInt(9), nil)), + dataAvailabilityFee: new(big.Int).Mul(big.NewInt(100), new(big.Int).Exp(big.NewInt(10), big.NewInt(9), nil)), feeComponentsError: nil, daGasConfig: ccipocr3.DataAvailabilityGasConfig{ DestDataAvailabilityOverheadGas: 1200, @@ -354,9 +357,9 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { DestDataAvailabilityMultiplierBps: 200, }, want: map[ccipocr3.Bytes32]plugintypes.USD18{ - b1: plugintypes.NewUSD18(55200), // 10_000 (exec) + 45_200 (da) - b2: plugintypes.NewUSD18(65200), // 20_000 (exec) + 45_200 (da) - b3: plugintypes.NewUSD18(75200), // 30_000 (exec) + 45_200 (da) + b1: plugintypes.NewUSD18(119700000000000), + b2: plugintypes.NewUSD18(137700000000000), + b3: plugintypes.NewUSD18(155700000000000), }, wantErr: false, }, @@ -364,7 +367,7 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { name: "message with token amounts affects DA gas calculation", messages: []ccipocr3.Message{ { - Header: ccipocr3.RampMessageHeader{MessageID: b1}, + Header: ccipocr3.RampMessageHeader{MessageID: b1, DestChainSelector: destChainSelector}, TokenAmounts: []ccipocr3.RampTokenAmount{ { SourcePoolAddress: []byte("source_pool"), @@ -381,8 +384,8 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { }, }, messageGases: []uint64{100}, - executionFee: big.NewInt(100), - dataAvailabilityFee: big.NewInt(400), + executionFee: new(big.Int).Mul(big.NewInt(20), new(big.Int).Exp(big.NewInt(10), big.NewInt(9), nil)), + dataAvailabilityFee: new(big.Int).Mul(big.NewInt(100), new(big.Int).Exp(big.NewInt(10), big.NewInt(9), nil)), feeComponentsError: nil, daGasConfig: ccipocr3.DataAvailabilityGasConfig{ DestDataAvailabilityOverheadGas: 1000, @@ -390,7 +393,7 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { DestDataAvailabilityMultiplierBps: 200, }, want: map[ccipocr3.Bytes32]plugintypes.USD18{ - b1: plugintypes.NewUSD18(79200), // 10_000 (exec) + 69_200 (da) + b1: plugintypes.NewUSD18(173700000000000), }, wantErr: false, }, @@ -398,13 +401,13 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { name: "zero DA multiplier results in only overhead gas", messages: []ccipocr3.Message{ { - Header: ccipocr3.RampMessageHeader{MessageID: b1}, + Header: ccipocr3.RampMessageHeader{MessageID: b1, DestChainSelector: destChainSelector}, Data: []byte("some_data"), }, }, messageGases: []uint64{100}, - executionFee: big.NewInt(100), - dataAvailabilityFee: big.NewInt(400), + executionFee: new(big.Int).Mul(big.NewInt(20), new(big.Int).Exp(big.NewInt(10), big.NewInt(9), nil)), + dataAvailabilityFee: new(big.Int).Mul(big.NewInt(100), new(big.Int).Exp(big.NewInt(10), big.NewInt(9), nil)), feeComponentsError: nil, daGasConfig: ccipocr3.DataAvailabilityGasConfig{ DestDataAvailabilityOverheadGas: 1000, @@ -412,7 +415,7 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { DestDataAvailabilityMultiplierBps: 0, // Zero multiplier }, want: map[ccipocr3.Bytes32]plugintypes.USD18{ - b1: plugintypes.NewUSD18(10000), // Only exec cost, DA cost is 0 + b1: plugintypes.NewUSD18(18000000000000), // Only exec cost, DA cost is 0 }, wantErr: false, }, @@ -420,7 +423,7 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { name: "large message with multiple tokens", messages: []ccipocr3.Message{ { - Header: ccipocr3.RampMessageHeader{MessageID: b1}, + Header: ccipocr3.RampMessageHeader{MessageID: b1, DestChainSelector: destChainSelector}, TokenAmounts: []ccipocr3.RampTokenAmount{ { SourcePoolAddress: make([]byte, 100), // Large token data @@ -444,8 +447,8 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { }, }, messageGases: []uint64{100}, - executionFee: big.NewInt(100), - dataAvailabilityFee: big.NewInt(400), + executionFee: new(big.Int).Mul(big.NewInt(20), new(big.Int).Exp(big.NewInt(10), big.NewInt(9), nil)), + dataAvailabilityFee: new(big.Int).Mul(big.NewInt(100), new(big.Int).Exp(big.NewInt(10), big.NewInt(9), nil)), feeComponentsError: nil, daGasConfig: ccipocr3.DataAvailabilityGasConfig{ DestDataAvailabilityOverheadGas: 1000, @@ -453,7 +456,7 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { DestDataAvailabilityMultiplierBps: 200, }, want: map[ccipocr3.Bytes32]plugintypes.USD18{ - b1: plugintypes.NewUSD18(219600), // 10_000 (exec) + 218_600 (da) + b1: plugintypes.NewUSD18(489600000000000), }, wantErr: false, }, @@ -486,12 +489,12 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { name: "minimal message - only constant parts", messages: []ccipocr3.Message{ { - Header: ccipocr3.RampMessageHeader{MessageID: b1}, + Header: ccipocr3.RampMessageHeader{MessageID: b1, DestChainSelector: destChainSelector}, }, }, messageGases: []uint64{100}, - executionFee: big.NewInt(100), - dataAvailabilityFee: big.NewInt(400), + executionFee: new(big.Int).Mul(big.NewInt(20), new(big.Int).Exp(big.NewInt(10), big.NewInt(9), nil)), + dataAvailabilityFee: new(big.Int).Mul(big.NewInt(100), new(big.Int).Exp(big.NewInt(10), big.NewInt(9), nil)), feeComponentsError: nil, daGasConfig: ccipocr3.DataAvailabilityGasConfig{ DestDataAvailabilityOverheadGas: 1000, @@ -499,7 +502,7 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { DestDataAvailabilityMultiplierBps: 200, }, want: map[ccipocr3.Bytes32]plugintypes.USD18{ - b1: plugintypes.NewUSD18(53600), // 10_000 (exec) + 43_600 (da) + b1: plugintypes.NewUSD18(116100000000000), }, wantErr: false, }, @@ -516,6 +519,14 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { DataAvailabilityFee: tt.dataAvailabilityFee, } mockReader.EXPECT().GetDestChainFeeComponents(ctx).Return(feeComponents, tt.feeComponentsError) + mockReader.EXPECT().GetWrappedNativeTokenPriceUSD( + ctx, + []ccipocr3.ChainSelector{destChainSelector}, + ).Return( + map[ccipocr3.ChainSelector]ccipocr3.BigInt{ + destChainSelector: nativeTokenPrice, + }, + ).Maybe() if !tt.wantErr { mockReader.EXPECT().GetMedianDataAvailabilityGasConfig(ctx).Return(tt.daGasConfig, nil) } diff --git a/execute/test_utils.go b/execute/test_utils.go index e1530449e..c3f35bc88 100644 --- a/execute/test_utils.go +++ b/execute/test_utils.go @@ -4,6 +4,7 @@ import ( "context" crand "crypto/rand" "encoding/binary" + "math/big" "net/http" "net/http/httptest" "strings" @@ -386,7 +387,8 @@ type msgOption func(*cciptypes.Message) func withFeeValueJuels(fee int64) msgOption { return func(m *cciptypes.Message) { - m.FeeValueJuels = cciptypes.NewBigIntFromInt64(fee) + juels := new(big.Int).Mul(big.NewInt(fee), new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)) + m.FeeValueJuels = cciptypes.NewBigInt(juels) } }