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

Convert Gas price in USD #290

Merged
merged 10 commits into from
Nov 5, 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
64 changes: 59 additions & 5 deletions execute/exectypes/costly_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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),
0xnogo marked this conversation as resolved.
Show resolved Hide resolved
)
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
Expand Down Expand Up @@ -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)
Expand All @@ -350,21 +359,64 @@ 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
}

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
Expand All @@ -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
Expand Down
74 changes: 43 additions & 31 deletions execute/exectypes/costly_messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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{
Expand All @@ -325,46 +329,46 @@ 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,
},
{
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,
DestGasPerDataAvailabilityByte: 10,
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,
},
{
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"),
Expand All @@ -381,46 +385,46 @@ 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,
DestGasPerDataAvailabilityByte: 10,
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,
},
{
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,
DestGasPerDataAvailabilityByte: 10,
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,
},
{
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
Expand All @@ -444,16 +448,16 @@ 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,
DestGasPerDataAvailabilityByte: 10,
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,
},
Expand Down Expand Up @@ -486,20 +490,20 @@ 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,
DestGasPerDataAvailabilityByte: 10,
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,
},
Expand All @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions execute/plugin_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
4 changes: 3 additions & 1 deletion execute/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
crand "crypto/rand"
"encoding/binary"
"math/big"
"net/http"
"net/http/httptest"
"strings"
Expand Down Expand Up @@ -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)
}
}

Expand Down
4 changes: 4 additions & 0 deletions internal/plugintypes/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Loading