Skip to content

Commit

Permalink
Merge branch 'ccip-develop' into feature/selfServeFactory
Browse files Browse the repository at this point in the history
  • Loading branch information
jhweintraub authored Sep 24, 2024
2 parents edc5c27 + ebc7094 commit 9c0d776
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 9c0d776

Please sign in to comment.