Skip to content

Commit

Permalink
[CCIP-3376] move interceptor implementation under relay/evm
Browse files Browse the repository at this point in the history
  • Loading branch information
valerii-kabisov-cll committed Sep 24, 2024
1 parent 02b6c8e commit 65d9af1
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 0 deletions.
80 changes: 80 additions & 0 deletions core/services/relay/evm/interceptors/mantle/interceptor.go
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
}
96 changes: 96 additions & 0 deletions core/services/relay/evm/interceptors/mantle/interceptor_test.go
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())
})
}
}

0 comments on commit 65d9af1

Please sign in to comment.