Skip to content

Commit

Permalink
[CCIP-3376] Add component into CCIP price estimators to account for c… (
Browse files Browse the repository at this point in the history
#1446)

[https://smartcontract-it.atlassian.net/browse/CCIP-3376](https://smartcontract-it.atlassian.net/browse/CCIP-3376)

The CCIP gas limit is set by customers. This cannot be changed because
this limit is directly used within the contracts (which expect limits in
terms of ETH). Also, the L2 gas price returned from our current
estimators for Mantle is in MNT. Therefore, the final formula needed for
CCIP to calculate L2 transaction fees is the following:
gasLimit_EVM * (gasPrice_Mantle * TokenRatio)
Therefore, we need to multiply the gas price returned from our gas
estimators by tokenRatio for CCIP. This should happen in the CCIP price
estimator. Currently, we have no component here for chain-specific
logic, so we will need to add this functionality.

---------

Co-authored-by: Matt Yang <[email protected]>
  • Loading branch information
valerii-kabisov-cll and matYang authored Sep 24, 2024
1 parent c8753d1 commit ebc7094
Show file tree
Hide file tree
Showing 14 changed files with 725 additions and 9 deletions.
7 changes: 6 additions & 1 deletion .mockery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ packages:
filename: optimism_portal2_interface.go
outpkg: mock_optimism_portal_2
interfaces:
OptimismPortal2Interface:
OptimismPortal2Interface:
github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/optimism_dispute_game_factory:
config:
dir: core/gethwrappers/liquiditymanager/mocks/mock_optimism_dispute_game_factory/
Expand Down Expand Up @@ -503,6 +503,11 @@ packages:
USDCReader:
config:
filename: usdc_reader_mock.go
github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig:
interfaces:
GasPriceInterceptor:
config:
filename: gas_price_interceptor_mock.go
github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/batchreader:
config:
filename: token_pool_batched_reader_mock.go
Expand Down
37 changes: 36 additions & 1 deletion core/services/ocr2/plugins/ccip/estimatorconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package estimatorconfig
import (
"context"
"errors"
"math/big"

"github.com/smartcontractkit/chainlink-common/pkg/types/ccip"
)
Expand All @@ -13,11 +14,18 @@ import (
// fields for the daGasEstimator from the encapsulated onRampReader.
type FeeEstimatorConfigProvider interface {
SetOnRampReader(reader ccip.OnRampReader)
AddGasPriceInterceptor(GasPriceInterceptor)
ModifyGasPriceComponents(ctx context.Context, execGasPrice, daGasPrice *big.Int) (modExecGasPrice, modDAGasPrice *big.Int, err error)
GetDataAvailabilityConfig(ctx context.Context) (destDataAvailabilityOverheadGas, destGasPerDataAvailabilityByte, destDataAvailabilityMultiplierBps int64, err error)
}

type GasPriceInterceptor interface {
ModifyGasPriceComponents(ctx context.Context, execGasPrice, daGasPrice *big.Int) (modExecGasPrice, modDAGasPrice *big.Int, err error)
}

type FeeEstimatorConfigService struct {
onRampReader ccip.OnRampReader
onRampReader ccip.OnRampReader
gasPriceInterceptors []GasPriceInterceptor
}

func NewFeeEstimatorConfigService() *FeeEstimatorConfigService {
Expand Down Expand Up @@ -47,3 +55,30 @@ func (c *FeeEstimatorConfigService) GetDataAvailabilityConfig(ctx context.Contex
int64(cfg.DestDataAvailabilityMultiplierBps),
err
}

// AddGasPriceInterceptor adds price interceptors that can modify gas price.
func (c *FeeEstimatorConfigService) AddGasPriceInterceptor(gpi GasPriceInterceptor) {
if gpi != nil {
c.gasPriceInterceptors = append(c.gasPriceInterceptors, gpi)
}
}

// ModifyGasPriceComponents applies gasPrice interceptors and returns modified gasPrice.
func (c *FeeEstimatorConfigService) ModifyGasPriceComponents(ctx context.Context, gasPrice, daGasPrice *big.Int) (*big.Int, *big.Int, error) {
if len(c.gasPriceInterceptors) == 0 {
return gasPrice, daGasPrice, nil
}

// values are mutable, it is necessary to copy the values to protect the arguments from modification.
cpGasPrice := new(big.Int).Set(gasPrice)
cpDAGasPrice := new(big.Int).Set(daGasPrice)

var err error
for _, interceptor := range c.gasPriceInterceptors {
if cpGasPrice, cpDAGasPrice, err = interceptor.ModifyGasPriceComponents(ctx, cpGasPrice, cpDAGasPrice); err != nil {
return nil, nil, err
}
}

return cpGasPrice, cpDAGasPrice, nil
}
64 changes: 64 additions & 0 deletions core/services/ocr2/plugins/ccip/estimatorconfig/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package estimatorconfig_test
import (
"context"
"errors"
"math/big"
"testing"

"github.com/stretchr/testify/require"

mocks2 "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig/mocks"

"github.com/smartcontractkit/chainlink-common/pkg/types/ccip"
"github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig"
"github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks"
Expand Down Expand Up @@ -43,3 +46,64 @@ func TestFeeEstimatorConfigService(t *testing.T) {
_, _, _, err = svc.GetDataAvailabilityConfig(ctx)
require.Error(t, err)
}

func TestModifyGasPriceComponents(t *testing.T) {
t.Run("success modification", func(t *testing.T) {
svc := estimatorconfig.NewFeeEstimatorConfigService()
ctx := context.Background()

initialExecGasPrice, initialDaGasPrice := big.NewInt(10), big.NewInt(1)

gpi1 := mocks2.NewGasPriceInterceptor(t)
svc.AddGasPriceInterceptor(gpi1)

// change in first interceptor
firstModExecGasPrice, firstModDaGasPrice := big.NewInt(5), big.NewInt(2)
gpi1.On("ModifyGasPriceComponents", ctx, initialExecGasPrice, initialDaGasPrice).
Return(firstModExecGasPrice, firstModDaGasPrice, nil)

gpi2 := mocks2.NewGasPriceInterceptor(t)
svc.AddGasPriceInterceptor(gpi2)

// change in second iterceptor
secondModExecGasPrice, secondModDaGasPrice := big.NewInt(50), big.NewInt(20)
gpi2.On("ModifyGasPriceComponents", ctx, firstModExecGasPrice, firstModDaGasPrice).
Return(secondModExecGasPrice, secondModDaGasPrice, nil)

// has to return second interceptor values
resGasPrice, resDAGasPrice, err := svc.ModifyGasPriceComponents(ctx, initialExecGasPrice, initialDaGasPrice)
require.NoError(t, err)
require.Equal(t, secondModExecGasPrice.Int64(), resGasPrice.Int64())
require.Equal(t, secondModDaGasPrice.Int64(), resDAGasPrice.Int64())
})

t.Run("error modification", func(t *testing.T) {
svc := estimatorconfig.NewFeeEstimatorConfigService()
ctx := context.Background()

initialExecGasPrice, initialDaGasPrice := big.NewInt(10), big.NewInt(1)
gpi1 := mocks2.NewGasPriceInterceptor(t)
svc.AddGasPriceInterceptor(gpi1)
gpi1.On("ModifyGasPriceComponents", ctx, initialExecGasPrice, initialDaGasPrice).
Return(nil, nil, errors.New("test"))

// has to return second interceptor values
_, _, err := svc.ModifyGasPriceComponents(ctx, initialExecGasPrice, initialDaGasPrice)
require.Error(t, err)
})

t.Run("without interceptors", func(t *testing.T) {
svc := estimatorconfig.NewFeeEstimatorConfigService()
ctx := context.Background()

initialExecGasPrice, initialDaGasPrice := big.NewInt(10), big.NewInt(1)

// has to return second interceptor values
resGasPrice, resDAGasPrice, err := svc.ModifyGasPriceComponents(ctx, initialExecGasPrice, initialDaGasPrice)
require.NoError(t, err)

// values should not be modified
require.Equal(t, initialExecGasPrice, resGasPrice)
require.Equal(t, initialDaGasPrice, resDAGasPrice)
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package mantle

import (
"context"
"fmt"
"math/big"
"strings"
"time"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"

evmClient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups"
)

const (
// tokenRatio is not volatile and can be requested not often.
tokenRatioUpdateInterval = 60 * time.Minute
// tokenRatio fetches the tokenRatio used for Mantle's gas price calculation
tokenRatioMethod = "tokenRatio"
mantleTokenRatioAbiString = `[{"inputs":[],"name":"tokenRatio","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]`
)

type Interceptor struct {
client evmClient.Client
tokenRatioCallData []byte
tokenRatio *big.Int
tokenRatioLastUpdate time.Time
}

func NewInterceptor(_ context.Context, client evmClient.Client) (*Interceptor, error) {
// Encode calldata for tokenRatio method
tokenRatioMethodAbi, err := abi.JSON(strings.NewReader(mantleTokenRatioAbiString))
if err != nil {
return nil, fmt.Errorf("failed to parse GasPriceOracle %s() method ABI for Mantle; %v", tokenRatioMethod, err)
}
tokenRatioCallData, err := tokenRatioMethodAbi.Pack(tokenRatioMethod)
if err != nil {
return nil, fmt.Errorf("failed to parse GasPriceOracle %s() calldata for Mantle; %v", tokenRatioMethod, err)
}

return &Interceptor{
client: client,
tokenRatioCallData: tokenRatioCallData,
}, nil
}

// ModifyGasPriceComponents returns modified gasPrice.
func (i *Interceptor) ModifyGasPriceComponents(ctx context.Context, execGasPrice, daGasPrice *big.Int) (*big.Int, *big.Int, error) {
if time.Since(i.tokenRatioLastUpdate) > tokenRatioUpdateInterval {
mantleTokenRatio, err := i.getMantleTokenRatio(ctx)
if err != nil {
return nil, nil, err
}

i.tokenRatio, i.tokenRatioLastUpdate = mantleTokenRatio, time.Now()
}

// multiply daGasPrice and execGas price by tokenRatio
newExecGasPrice := new(big.Int).Mul(execGasPrice, i.tokenRatio)
newDAGasPrice := new(big.Int).Mul(daGasPrice, i.tokenRatio)
return newExecGasPrice, newDAGasPrice, nil
}

// getMantleTokenRatio Requests and returns a token ratio value for the Mantle chain.
func (i *Interceptor) getMantleTokenRatio(ctx context.Context) (*big.Int, error) {
precompile := common.HexToAddress(rollups.OPGasOracleAddress)
tokenRatio, err := i.client.CallContract(ctx, ethereum.CallMsg{
To: &precompile,
Data: i.tokenRatioCallData,
}, nil)

if err != nil {
return nil, fmt.Errorf("getMantleTokenRatio call failed: %w", err)
}

return new(big.Int).SetBytes(tokenRatio), nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package mantle

import (
"context"
"math/big"
"testing"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks"
)

func TestInterceptor(t *testing.T) {
ethClient := mocks.NewClient(t)
ctx := context.Background()

tokenRatio := big.NewInt(10)
interceptor, err := NewInterceptor(ctx, ethClient)
require.NoError(t, err)

// request token ratio
ethClient.On("CallContract", ctx, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).
Return(common.BigToHash(tokenRatio).Bytes(), nil).Once()

modExecGasPrice, modDAGasPrice, err := interceptor.ModifyGasPriceComponents(ctx, big.NewInt(1), big.NewInt(1))
require.NoError(t, err)
require.Equal(t, int64(10), modExecGasPrice.Int64())
require.Equal(t, int64(10), modDAGasPrice.Int64())

// second call won't invoke eth client
modExecGasPrice, modDAGasPrice, err = interceptor.ModifyGasPriceComponents(ctx, big.NewInt(2), big.NewInt(1))
require.NoError(t, err)
require.Equal(t, int64(20), modExecGasPrice.Int64())
require.Equal(t, int64(10), modDAGasPrice.Int64())
}

func TestModifyGasPriceComponents(t *testing.T) {
testCases := map[string]struct {
execGasPrice *big.Int
daGasPrice *big.Int
tokenRatio *big.Int
resultExecGasPrice *big.Int
resultDAGasPrice *big.Int
}{
"regular": {
execGasPrice: big.NewInt(1000),
daGasPrice: big.NewInt(100),
resultExecGasPrice: big.NewInt(2000),
resultDAGasPrice: big.NewInt(200),
tokenRatio: big.NewInt(2),
},
"zero DAGasPrice": {
execGasPrice: big.NewInt(1000),
daGasPrice: big.NewInt(0),
resultExecGasPrice: big.NewInt(5000),
resultDAGasPrice: big.NewInt(0),
tokenRatio: big.NewInt(5),
},
"zero ExecGasPrice": {
execGasPrice: big.NewInt(0),
daGasPrice: big.NewInt(10),
resultExecGasPrice: big.NewInt(0),
resultDAGasPrice: big.NewInt(50),
tokenRatio: big.NewInt(5),
},
"zero token ratio": {
execGasPrice: big.NewInt(15),
daGasPrice: big.NewInt(10),
resultExecGasPrice: big.NewInt(0),
resultDAGasPrice: big.NewInt(0),
tokenRatio: big.NewInt(0),
},
}

for tcName, tc := range testCases {
t.Run(tcName, func(t *testing.T) {
ethClient := mocks.NewClient(t)
ctx := context.Background()

interceptor, err := NewInterceptor(ctx, ethClient)
require.NoError(t, err)

// request token ratio
ethClient.On("CallContract", ctx, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).
Return(common.BigToHash(tc.tokenRatio).Bytes(), nil).Once()

modExecGasPrice, modDAGasPrice, err := interceptor.ModifyGasPriceComponents(ctx, tc.execGasPrice, tc.daGasPrice)
require.NoError(t, err)
require.Equal(t, tc.resultExecGasPrice.Int64(), modExecGasPrice.Int64())
require.Equal(t, tc.resultDAGasPrice.Int64(), modDAGasPrice.Int64())
})
}
}
Loading

0 comments on commit ebc7094

Please sign in to comment.