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

[CCIP-3376] Add component into CCIP price estimators to account for c… #1446

Merged
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
645b85b
[CCIP-3376] Add component into CCIP price estimators to account for c…
valerii-kabisov-cll Sep 18, 2024
d641f1c
[CCIP-3376] refactoring
valerii-kabisov-cll Sep 18, 2024
3f5565c
[CCIP-3376] refactoring, renaming
valerii-kabisov-cll Sep 18, 2024
bf83c5d
Merge branch 'ccip-develop' into CCIP-3376-custom-mantle-price-estimator
matYang Sep 19, 2024
d3b8acc
test chain type avail
matYang Sep 19, 2024
44265ce
[CCIP-3376] refactoring, renaming
valerii-kabisov-cll Sep 19, 2024
7b8576f
[CCIP-3376] typo fix
valerii-kabisov-cll Sep 19, 2024
c63f97d
[CCIP-3376] fix comment
valerii-kabisov-cll Sep 19, 2024
143a126
[CCIP-3376] fix
valerii-kabisov-cll Sep 19, 2024
24d05c4
[CCIP-3376] reformat
valerii-kabisov-cll Sep 20, 2024
5fe2674
[CCIP-3376] tests and refactoring
valerii-kabisov-cll Sep 20, 2024
34f06f9
[CCIP-3376] tests
valerii-kabisov-cll Sep 20, 2024
91e5301
[CCIP-3376] lint
valerii-kabisov-cll Sep 20, 2024
21c82b7
[CCIP-3376] lint
valerii-kabisov-cll Sep 20, 2024
88e272d
[CCIP-3376] tests
valerii-kabisov-cll Sep 20, 2024
2a8902a
Merge branch 'ccip-develop' into CCIP-3376-custom-mantle-price-estimator
valerii-kabisov-cll Sep 20, 2024
dd0077d
[CCIP-3376] fix calculation formula
valerii-kabisov-cll Sep 21, 2024
4fb8579
Merge remote-tracking branch 'origin/CCIP-3376-custom-mantle-price-es…
valerii-kabisov-cll Sep 21, 2024
ad48192
[CCIP-3376] refactoring
valerii-kabisov-cll Sep 23, 2024
5ffa2ab
[CCIP-3376] token ratio decimals
valerii-kabisov-cll Sep 23, 2024
bd2a850
[CCIP-3376] fix test
valerii-kabisov-cll Sep 23, 2024
fbbbb43
[CCIP-3376] typo fix
valerii-kabisov-cll Sep 23, 2024
c271962
[CCIP-3376] revert
valerii-kabisov-cll Sep 23, 2024
f1ec066
Merge branch 'ccip-develop' into CCIP-3376-custom-mantle-price-estimator
valerii-kabisov-cll Sep 23, 2024
e9ba06a
[CCIP-3376] linter
valerii-kabisov-cll Sep 23, 2024
5afd6f5
[CCIP-3376] refactoring
valerii-kabisov-cll Sep 24, 2024
02b6c8e
[CCIP-3376] lint
valerii-kabisov-cll Sep 24, 2024
65d9af1
[CCIP-3376] move interceptor implementation under relay/evm
valerii-kabisov-cll Sep 24, 2024
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
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 {
valerii-kabisov-cll marked this conversation as resolved.
Show resolved Hide resolved
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,81 @@
package mantle

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

"github.com/ethereum/go-ethereum"
valerii-kabisov-cll marked this conversation as resolved.
Show resolved Hide resolved
"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
valerii-kabisov-cll marked this conversation as resolved.
Show resolved Hide resolved
// 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 {
// Encode calldata for tokenRatio method
tokenRatioMethodAbi, err := abi.JSON(strings.NewReader(mantleTokenRatioAbiString))
if err != nil {
log.Panicf("failed to parse GasPriceOracle %s() method ABI for Mantle; %v", tokenRatioMethod, err)
valerii-kabisov-cll marked this conversation as resolved.
Show resolved Hide resolved
}
tokenRatioCallData, err := tokenRatioMethodAbi.Pack(tokenRatioMethod)
if err != nil {
log.Panicf("failed to parse GasPriceOracle %s() calldata for Mantle; %v", tokenRatioMethod, err)
}

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

// 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can't just call contracts directly from the plugin logic, can we @dimkouv . I think it should belong to some reader to stay compliant with loops

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this logic is hidden behind the custom provider interface it should still be compliant with loops.

if err != nil {
return nil, nil, err
}

i.tokenRatio, i.tokenRatioLastUpdate = mantleTokenRatio, time.Now()
valerii-kabisov-cll marked this conversation as resolved.
Show resolved Hide resolved
}

// 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{
valerii-kabisov-cll marked this conversation as resolved.
Show resolved Hide resolved
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,94 @@
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 := NewInterceptor(ctx, ethClient)

// 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 := NewInterceptor(ctx, ethClient)

// 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
Loading