diff --git a/execute/exectypes/costly_messages.go b/execute/exectypes/costly_messages.go index 498b8223d..699717742 100644 --- a/execute/exectypes/costly_messages.go +++ b/execute/exectypes/costly_messages.go @@ -10,6 +10,8 @@ import ( "github.com/smartcontractkit/chainlink-ccip/execute/internal/gas" + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-ccip/internal/plugintypes" readerpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" @@ -297,7 +299,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 @@ -334,12 +339,16 @@ type CCIPMessageExecCostUSD18Calculator struct { estimateProvider gas.EstimateProvider } -// MessageExecCostUSD18 returns a map from message ID to the message's estimated execution cost in USD18s. +// getFeesUSD18 converts the fee components (ExecutionFee and DataAvailabilityFee) from native token units +// to USD with 18 decimals (USD18). func (c *CCIPMessageExecCostUSD18Calculator) MessageExecCostUSD18( ctx context.Context, messages []cciptypes.Message, ) (map[cciptypes.Bytes32]plugintypes.USD18, error) { messageExecCosts := make(map[cciptypes.Bytes32]plugintypes.USD18) + + // Retrieve the fee components from the destination chain. + // feeComponents.ExecutionFee and feeComponents.DataAvailabilityFee are in native token units. feeComponents, err := c.ccipReader.GetDestChainFeeComponents(ctx) if err != nil { return nil, fmt.Errorf("unable to get fee components: %w", err) @@ -350,14 +359,28 @@ func (c *CCIPMessageExecCostUSD18Calculator) MessageExecCostUSD18( if feeComponents.DataAvailabilityFee == nil { return nil, fmt.Errorf("missing data availability fee") } + if len(messages) == 0 { + return messageExecCosts, nil + } + + // Calculate execution fee in USD18 by multiplying the execution fee (in native tokens) by the native token price. + // feeComponents.ExecutionFee is in native tokens, nativeTokenPrice is in USD18, so the result is scaled by 1e18. + 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) + } + + // Calculate da fee in USD18 by multiplying the data availability fee (in native tokens) by the native token price. + // feeComponents.DataAvailabilityFee is in native tokens, nativeTokenPrice is in USD18, + // so the result is scaled by 1e18. 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 := computeDataAvailabilityCostUSD18(daFee, daConfig, msg) totalCostUSD18 := new(big.Int).Add(executionCostUSD18, dataAvailabilityCostUSD18) messageExecCosts[msg.Header.MessageID] = totalCostUSD18 } @@ -365,6 +388,35 @@ 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) + } + + if (feeComponents.ExecutionFee).Cmp(big.NewInt(0)) == 0 { + return big.NewInt(0), big.NewInt(0), nil + } + executionFee := new(big.Int).Div(nativeTokenPrice.Int, feeComponents.ExecutionFee) + + if (feeComponents.DataAvailabilityFee).Cmp(big.NewInt(0)) == 0 { + return executionFee, big.NewInt(0), nil + } + dataAvailabilityFee := new(big.Int).Div(nativeTokenPrice.Int, feeComponents.DataAvailabilityFee) + + return executionFee, dataAvailabilityFee, nil +} + // computeExecutionCostUSD18 computes the execution cost of a message in USD18s. // The cost is: // messageGas (gas) * executionFee (USD18/gas) = USD18 @@ -388,7 +440,9 @@ func computeDataAvailabilityCostUSD18( } messageGas := calculateMessageMaxDAGas(message, daConfig) - return big.NewInt(0).Mul(messageGas, dataAvailabilityFee) + cost := big.NewInt(0).Mul(messageGas, dataAvailabilityFee) + + return cost } // calculateMessageMaxDAGas calculates the total DA gas needed for a CCIP message diff --git a/execute/exectypes/costly_messages_test.go b/execute/exectypes/costly_messages_test.go index 86bd9cd14..1ab6c2580 100644 --- a/execute/exectypes/costly_messages_test.go +++ b/execute/exectypes/costly_messages_test.go @@ -291,6 +291,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 +310,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 +329,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(45000000000), // 5_000_000_000 * 9 (price conversion) + b2: plugintypes.NewUSD18(90000000000), // 10_000_000_000 * 9 + b3: plugintypes.NewUSD18(135000000000), // 15_000_000_000 * 9 }, wantErr: false, }, @@ -335,18 +339,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 +358,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(55170000000), // 4.5e10 (exec) + 1.017e10 (da) + b2: plugintypes.NewUSD18(100170000000), // 9e10 (exec) + 1.017e10 (da) + b3: plugintypes.NewUSD18(145170000000), // 135e10 (exec) + 1.017e10 (da) }, wantErr: false, }, @@ -364,7 +368,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 +385,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 +394,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(60570000000), // 4.5e10 (exec) + 1.557e10 (da) }, wantErr: false, }, @@ -398,13 +402,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 +416,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(45000000000), // Only exec cost, DA cost is 0 }, wantErr: false, }, @@ -420,7 +424,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 +448,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 +457,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(92160000000), // 4.5e10 (exec) + 4.716e10 (da) }, wantErr: false, }, @@ -486,12 +490,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 +503,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(54810000000), // 4.5e10 (exec) + 0.981e10 (da) }, wantErr: false, }, @@ -516,6 +520,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/plugin_e2e_test.go b/execute/plugin_e2e_test.go index 44fe9db4f..d8b9ba61c 100644 --- a/execute/plugin_e2e_test.go +++ b/execute/plugin_e2e_test.go @@ -76,6 +76,7 @@ func Test_ExcludingCostlyMessages(t *testing.T) { tm := timeMachine{now: messageTimestamp} intTest := SetupSimpleTest(t, srcSelector, dstSelector) + intTest.WithMessages(messages, 1000, messageTimestamp) intTest.WithCustomFeeBoosting(1.0, tm.Now, map[cciptypes.Bytes32]plugintypes.USD18{ messages[0].Header.MessageID: plugintypes.NewUSD18(40000), 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) } } diff --git a/internal/plugintypes/common.go b/internal/plugintypes/common.go index b44cfe669..04a9f3821 100644 --- a/internal/plugintypes/common.go +++ b/internal/plugintypes/common.go @@ -36,3 +36,7 @@ type USD18 = *big.Int func NewUSD18(value int64) USD18 { return big.NewInt(value) } + +func NewUSD18FromUSD(value int64) USD18 { + return new(big.Int).Mul(big.NewInt(value), new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)) +}