diff --git a/.changeset/giant-cats-run.md b/.changeset/giant-cats-run.md new file mode 100644 index 00000000000..22a0cde247d --- /dev/null +++ b/.changeset/giant-cats-run.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#internal Add tokenDestGasOverhead to CalculateMessageMaxGas diff --git a/core/capabilities/ccip/ccipevm/gas_helpers.go b/core/capabilities/ccip/ccipevm/gas_helpers.go index f6b8348f29a..52e3d033d2a 100644 --- a/core/capabilities/ccip/ccipevm/gas_helpers.go +++ b/core/capabilities/ccip/ccipevm/gas_helpers.go @@ -1,6 +1,7 @@ package ccipevm import ( + "fmt" "math" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" @@ -52,15 +53,33 @@ func bytesForMsgTokens(numTokens int) int { // CalculateMessageMaxGas computes the maximum gas overhead for a message. func (gp EstimateProvider) CalculateMessageMaxGas(msg cciptypes.Message) uint64 { + maxGas, err := gp.CalculateMessageMaxGasWithError(msg) + if err != nil { + panic(err) + } + return maxGas +} + +// CalculateMessageMaxGasWithError computes the maximum gas overhead for a message. +// TODO: Add destGasOverhead, see: +// https://github.com/smartcontractkit/chainlink/blob/23452266132228234312947660374fb393e96f1a/contracts/src/v0.8/ccip/FeeQuoter.sol#L97 +func (gp EstimateProvider) CalculateMessageMaxGasWithError(msg cciptypes.Message) (uint64, error) { numTokens := len(msg.TokenAmounts) var data []byte = msg.Data dataLength := len(data) - // TODO: update interface to return error? - // Although this decoding should never fail. messageGasLimit, err := decodeExtraArgsV1V2(msg.ExtraArgs) if err != nil { - panic(err) + return 0, fmt.Errorf("failed to decode extra args: %w", err) + } + + var totalTokenDestGasOverhead uint64 + for _, rampTokenAmount := range msg.TokenAmounts { + tokenDestGasOverhead, err := decodeTokenDestGasOverhead(rampTokenAmount.DestExecData) + if err != nil { + return 0, fmt.Errorf("failed to decode token dest gas overhead: %w", err) + } + totalTokenDestGasOverhead += uint64(tokenDestGasOverhead) } messageBytes := ConstantMessagePartBytes + @@ -85,5 +104,6 @@ func (gp EstimateProvider) CalculateMessageMaxGas(msg cciptypes.Message) uint64 SupportsInterfaceCheck + adminRegistryOverhead + rateLimiterOverhead + - PerTokenOverheadGas*uint64(numTokens) + PerTokenOverheadGas*uint64(numTokens) + + totalTokenDestGasOverhead, nil } diff --git a/core/capabilities/ccip/ccipevm/gas_helpers_test.go b/core/capabilities/ccip/ccipevm/gas_helpers_test.go index 23afd084d80..bcc87867407 100644 --- a/core/capabilities/ccip/ccipevm/gas_helpers_test.go +++ b/core/capabilities/ccip/ccipevm/gas_helpers_test.go @@ -6,15 +6,17 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" ) func Test_calculateMessageMaxGas(t *testing.T) { type args struct { - dataLen int - numTokens int - extraArgs []byte + dataLen int + numTokens int + extraArgs []byte + tokenGasOverhead uint32 } tests := []struct { name string @@ -23,33 +25,48 @@ func Test_calculateMessageMaxGas(t *testing.T) { }{ { name: "base", - args: args{dataLen: 5, numTokens: 2, extraArgs: makeExtraArgsV1(200_000)}, - want: 1_022_264, + args: args{dataLen: 5, numTokens: 2, extraArgs: makeExtraArgsV1(200_000), tokenGasOverhead: 10}, + want: 1_022_284, }, { name: "large", - args: args{dataLen: 1000, numTokens: 1000, extraArgs: makeExtraArgsV1(200_000)}, - want: 346_677_520, + args: args{dataLen: 1000, numTokens: 1000, extraArgs: makeExtraArgsV1(200_000), tokenGasOverhead: 1}, + want: 346_678_520, }, { name: "overheadGas test 1", - args: args{dataLen: 0, numTokens: 0, extraArgs: makeExtraArgsV1(200_000)}, + args: args{dataLen: 0, numTokens: 0, extraArgs: makeExtraArgsV1(200_000), tokenGasOverhead: 100}, want: 319_920, }, { name: "overheadGas test 2", - args: args{dataLen: len([]byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}), numTokens: 1, extraArgs: makeExtraArgsV1(200_000)}, - want: 675_948, + args: args{ + dataLen: len([]byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}), + numTokens: 1, + extraArgs: makeExtraArgsV1(200_000), + tokenGasOverhead: 2, + }, + want: 675_950, }, { name: "allowOOO set to true makes no difference to final gas estimate", - args: args{dataLen: 5, numTokens: 2, extraArgs: makeExtraArgsV2(200_000, true)}, - want: 1_022_264, + args: args{ + dataLen: 5, + numTokens: 2, + extraArgs: makeExtraArgsV2(200_000, true), + tokenGasOverhead: 100, + }, + want: 1_022_464, }, { name: "allowOOO set to false makes no difference to final gas estimate", - args: args{dataLen: 5, numTokens: 2, extraArgs: makeExtraArgsV2(200_000, false)}, - want: 1_022_264, + args: args{ + dataLen: 5, + numTokens: 2, + extraArgs: makeExtraArgsV2(200_000, false), + tokenGasOverhead: 100, + }, + want: 1_022_464, }, } @@ -57,7 +74,7 @@ func Test_calculateMessageMaxGas(t *testing.T) { t.Run(tt.name, func(t *testing.T) { msg := ccipocr3.Message{ Data: make([]byte, tt.args.dataLen), - TokenAmounts: make([]ccipocr3.RampTokenAmount, tt.args.numTokens), + TokenAmounts: getTokenAmounts(t, tt.args.numTokens, tt.args.tokenGasOverhead), ExtraArgs: tt.args.extraArgs, } ep := EstimateProvider{} @@ -72,36 +89,40 @@ func Test_calculateMessageMaxGas(t *testing.T) { // are combined to one function. func TestCalculateMaxGas(t *testing.T) { tests := []struct { - name string - numRequests int - dataLength int - numberOfTokens int - extraArgs []byte - want uint64 + name string + numRequests int + dataLength int + numberOfTokens int + extraArgs []byte + tokenGasOverhead uint32 + want uint64 }{ { - name: "maxGasOverheadGas 1", - numRequests: 6, - dataLength: 0, - numberOfTokens: 0, - extraArgs: makeExtraArgsV1(200_000), - want: 322_992, + name: "maxGasOverheadGas 1", + numRequests: 6, + dataLength: 0, + numberOfTokens: 0, + extraArgs: makeExtraArgsV1(200_000), + tokenGasOverhead: 10, + want: 322_992, }, { - name: "maxGasOverheadGas 2", - numRequests: 3, - dataLength: len([]byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}), - numberOfTokens: 1, - extraArgs: makeExtraArgsV1(200_000), - want: 678_508, + name: "maxGasOverheadGas 2", + numRequests: 3, + dataLength: len([]byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}), + numberOfTokens: 1, + extraArgs: makeExtraArgsV1(200_000), + tokenGasOverhead: 10, + want: 678_518, }, { - name: "v2 extra args", - numRequests: 3, - dataLength: len([]byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}), - numberOfTokens: 1, - extraArgs: makeExtraArgsV2(200_000, true), - want: 678_508, + name: "v2 extra args", + numRequests: 3, + dataLength: len([]byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}), + numberOfTokens: 1, + extraArgs: makeExtraArgsV2(200_000, true), + tokenGasOverhead: 10, + want: 678_518, }, } @@ -109,7 +130,7 @@ func TestCalculateMaxGas(t *testing.T) { t.Run(tt.name, func(t *testing.T) { msg := ccipocr3.Message{ Data: make([]byte, tt.dataLength), - TokenAmounts: make([]ccipocr3.RampTokenAmount, tt.numberOfTokens), + TokenAmounts: getTokenAmounts(t, tt.numberOfTokens, tt.tokenGasOverhead), ExtraArgs: tt.extraArgs, } ep := EstimateProvider{} @@ -155,3 +176,16 @@ func makeExtraArgsV2(gasLimit uint64, allowOOO bool) []byte { extraArgs = append(extraArgs, allowOOOBytes...) return extraArgs } + +func getTokenAmounts(t *testing.T, numTokens int, tokenGasOverhead uint32) []ccipocr3.RampTokenAmount { + tokenDestGasOverhead, err := TokenDestGasOverheadABI.Pack(tokenGasOverhead) + require.NoError(t, err) + + tokenAmounts := make([]ccipocr3.RampTokenAmount, numTokens) + for i := 0; i < numTokens; i++ { + tokenAmounts[i] = ccipocr3.RampTokenAmount{ + DestExecData: tokenDestGasOverhead, + } + } + return tokenAmounts +} diff --git a/core/capabilities/ccip/ccipevm/helpers.go b/core/capabilities/ccip/ccipevm/helpers.go index 4cfd64b7c65..3406421b915 100644 --- a/core/capabilities/ccip/ccipevm/helpers.go +++ b/core/capabilities/ccip/ccipevm/helpers.go @@ -8,6 +8,15 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" ) +var ( + abiUint32 = ABITypeOrPanic("uint32") + TokenDestGasOverheadABI = abi.Arguments{ + { + Type: abiUint32, + }, + } +) + func decodeExtraArgsV1V2(extraArgs []byte) (gasLimit *big.Int, err error) { if len(extraArgs) < 4 { return nil, fmt.Errorf("extra args too short: %d, should be at least 4 (i.e the extraArgs tag)", len(extraArgs)) @@ -43,3 +52,24 @@ func abiEncodeMethodInputs(abiDef abi.ABI, inputs ...interface{}) ([]byte, error } return packed[4:], nil // remove the method selector } + +func ABITypeOrPanic(t string) abi.Type { + abiType, err := abi.NewType(t, "", nil) + if err != nil { + panic(err) + } + return abiType +} + +// Decodes the given bytes into a uint32, based on the encoding of destGasAmount in FeeQuoter.sol +func decodeTokenDestGasOverhead(destExecData []byte) (uint32, error) { + ifaces, err := TokenDestGasOverheadABI.UnpackValues(destExecData) + if err != nil { + return 0, fmt.Errorf("abi decode TokenDestGasOverheadABI: %w", err) + } + _, ok := ifaces[0].(uint32) + if !ok { + return 0, fmt.Errorf("expected uint32, got %T", ifaces[0]) + } + return ifaces[0].(uint32), nil +}