Skip to content

Commit

Permalink
Contract Reader Handle Empty Bytes (#15688)
Browse files Browse the repository at this point in the history
In some cases, a contract call will return an empty result `0x`, which decodes to an empty set of bytes. The codec
can't decode this empty set except in specific cases where a slice of bytes is the expected result.

This commit adds a short-circuit to decoding an empty result where the return value is unaltered.
  • Loading branch information
EasterTheBunny authored Dec 16, 2024
1 parent a6b3b0b commit beb82ec
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 26 deletions.
6 changes: 5 additions & 1 deletion core/services/relay/evm/chain_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,11 @@ func (cr *chainReader) GetLatestValue(ctx context.Context, readName string, conf
ptrToValue, isValue := returnVal.(*values.Value)
if !isValue {
_, err = binding.GetLatestValueWithHeadData(ctx, common.HexToAddress(address), confidenceLevel, params, returnVal)
return err
if err != nil {
return err
}

return nil
}

contractType, err := cr.CreateContractType(readName, false)
Expand Down
54 changes: 29 additions & 25 deletions core/services/relay/evm/read/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,32 +241,36 @@ func (c *defaultEvmBatchCaller) unpackBatchResults(
return nil, callErr
}

if err = c.codec.Decode(
ctx,
packedBytes,
call.ReturnVal,
codec.WrapItemType(call.ContractName, call.ReadName, false),
); err != nil {
if len(packedBytes) == 0 {
callErr := newErrorFromCall(
fmt.Errorf("%w: %w: %s", types.ErrInternal, errEmptyOutput, err.Error()),
call, block, batchReadType,
)

callErr.Result = &hexEncodedOutputs[idx]

results[idx].err = callErr
} else {
callErr := newErrorFromCall(
fmt.Errorf("%w: codec decode result: %s", types.ErrInvalidType, err.Error()),
call, block, batchReadType,
)

callErr.Result = &hexEncodedOutputs[idx]
results[idx].err = callErr
}
// the codec can't do anything with no bytes, so skip decoding and allow
// the result to be the empty struct or value
if len(packedBytes) > 0 {
if err = c.codec.Decode(
ctx,
packedBytes,
call.ReturnVal,
codec.WrapItemType(call.ContractName, call.ReadName, false),
); err != nil {
if len(packedBytes) == 0 {
callErr := newErrorFromCall(
fmt.Errorf("%w: %w: %s", types.ErrInternal, errEmptyOutput, err.Error()),
call, block, batchReadType,
)

callErr.Result = &hexEncodedOutputs[idx]

results[idx].err = callErr
} else {
callErr := newErrorFromCall(
fmt.Errorf("%w: codec decode result: %s", types.ErrInvalidType, err.Error()),
call, block, batchReadType,
)

callErr.Result = &hexEncodedOutputs[idx]
results[idx].err = callErr
}

continue
continue
}
}

results[idx].returnVal = call.ReturnVal
Expand Down
9 changes: 9 additions & 0 deletions core/services/relay/evm/read/method.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package read

import (
"context"
"errors"
"fmt"
"math/big"
"sync"
Expand All @@ -22,6 +23,8 @@ import (
evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client"
)

var ErrEmptyContractReturnValue = errors.New("the contract return value was empty")

type MethodBinding struct {
// read-only properties
contractName string
Expand Down Expand Up @@ -173,6 +176,12 @@ func (b *MethodBinding) GetLatestValueWithHeadData(ctx context.Context, addr com
return nil, callErr
}

// there may be cases where the contract value has not been set and the RPC returns with a value of 0x
// which is a set of empty bytes. there is no need for the codec to run in this case.
if len(bytes) == 0 {
return block.ToChainAgnosticHead(), nil
}

if err = b.codec.Decode(ctx, bytes, returnVal, codec.WrapItemType(b.contractName, b.method, false)); err != nil {
callErr := newErrorFromCall(
fmt.Errorf("%w: decode return data: %s", commontypes.ErrInvalidType, err.Error()),
Expand Down

0 comments on commit beb82ec

Please sign in to comment.