Skip to content

Commit 347bc2f

Browse files
committed
refactor: extract big.Int parsing into pkg/bigint to remove octal traps
Add pkg/bigint.Parse(s) that decodes a numeric string as base-10 by default and as base-16 when prefixed with 0x/0X, with whitespace trimmed and explicit errors on empty or unparseable input. Unlike big.Int.SetString with base 0, it does not silently treat leading- zero strings (e.g. "010") as octal. Refactor three callsites onto the helper: - pkg/erc4337/userop/parse.go: UserOp BigInt field decoder previously used SetString(..., 0), exposing the octal trap on user-supplied numeric fields. - core/taskengine/utils.go: ABI uint/int parameter conversion already did the same hex-or-decimal split inline; collapse to the helper while preserving the existing error format that utils_calldata_test depends on. - core/taskengine/vm_runner_contract_write.go: post-approve allowance override added in #527 used the same inline split; collapse it.
1 parent 4358d02 commit 347bc2f

5 files changed

Lines changed: 100 additions & 35 deletions

File tree

core/taskengine/utils.go

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/AvaProtocol/EigenLayer-AVS/core/taskengine/macros"
1515
"github.com/AvaProtocol/EigenLayer-AVS/core/taskengine/modules"
16+
"github.com/AvaProtocol/EigenLayer-AVS/pkg/bigint"
1617
"github.com/AvaProtocol/EigenLayer-AVS/pkg/erc20"
1718
avsproto "github.com/AvaProtocol/EigenLayer-AVS/protobuf"
1819
"github.com/dop251/goja"
@@ -705,27 +706,7 @@ func parseABIParameter(param string, abiType abi.Type) (interface{}, error) {
705706
return common.HexToAddress(param), nil
706707

707708
case abi.UintTy, abi.IntTy:
708-
// Handle big integers
709-
// Validate that the parameter is a valid number
710-
paramTrimmed := strings.TrimSpace(param)
711-
if paramTrimmed == "" {
712-
return nil, fmt.Errorf("expected numeric value, got ''")
713-
}
714-
715-
value := new(big.Int)
716-
var ok bool
717-
if strings.HasPrefix(paramTrimmed, "0x") {
718-
_, ok = value.SetString(paramTrimmed[2:], 16)
719-
} else {
720-
// Check if it's a valid decimal number (allows digits, optional negative sign)
721-
_, ok = value.SetString(paramTrimmed, 10)
722-
}
723-
724-
if !ok {
725-
return nil, fmt.Errorf("expected numeric value, got '%s'", paramTrimmed)
726-
}
727-
728-
return value, nil
709+
return bigint.Parse(param)
729710

730711
case abi.BoolTy:
731712
switch strings.ToLower(param) {

core/taskengine/vm_runner_contract_write.go

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020

2121
"github.com/AvaProtocol/EigenLayer-AVS/core/chainio/aa"
2222
"github.com/AvaProtocol/EigenLayer-AVS/core/config"
23+
"github.com/AvaProtocol/EigenLayer-AVS/pkg/bigint"
2324
"github.com/AvaProtocol/EigenLayer-AVS/pkg/byte4"
2425
"github.com/AvaProtocol/EigenLayer-AVS/pkg/eip1559"
2526
"github.com/AvaProtocol/EigenLayer-AVS/pkg/erc4337/bundler"
@@ -506,20 +507,13 @@ func (r *ContractWriteProcessor) executeMethodCall(
506507
// allowance[owner][spender] slot directly.
507508
if simSuccess && isMethodWithParams(methodName, "approve", resolvedMethodParams, 2) {
508509
rawSpender := strings.TrimSpace(resolvedMethodParams[0])
509-
rawAmount := strings.TrimSpace(resolvedMethodParams[1])
510510
if !common.IsHexAddress(rawSpender) {
511511
r.vm.logger.Debug("Skipping allowance override: invalid spender address",
512512
"raw", resolvedMethodParams[0])
513513
} else {
514514
spender := common.HexToAddress(rawSpender)
515-
// Default to base 10; honor 0x/0X prefix as hex. Avoid base 0 because
516-
// it treats leading-zero strings (e.g. "010") as octal.
517-
base := 10
518-
if strings.HasPrefix(rawAmount, "0x") || strings.HasPrefix(rawAmount, "0X") {
519-
base = 16
520-
}
521-
amount, ok := new(big.Int).SetString(rawAmount, base)
522-
if !ok || amount.Sign() < 0 {
515+
amount, parseErr := bigint.Parse(resolvedMethodParams[1])
516+
if parseErr != nil || amount.Sign() < 0 {
523517
r.vm.logger.Debug("Skipping allowance override: invalid or negative amount",
524518
"raw", resolvedMethodParams[1])
525519
} else {

pkg/bigint/parse.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Package bigint provides shared big.Int parsing helpers.
2+
package bigint
3+
4+
import (
5+
"fmt"
6+
"math/big"
7+
"strings"
8+
)
9+
10+
// Parse decodes s as a base-10 integer, or as base-16 if s has a "0x" or "0X"
11+
// prefix. Whitespace around s is trimmed. Returns an error on empty or
12+
// unparseable input.
13+
//
14+
// Unlike (*big.Int).SetString with base 0, Parse does not treat leading-zero
15+
// strings as octal: "010" parses as 10, not 8. Use this for any user-supplied
16+
// numeric string where octal interpretation would be surprising or wrong
17+
// (e.g. ERC20 amounts, RPC payloads, contract method params).
18+
func Parse(s string) (*big.Int, error) {
19+
trimmed := strings.TrimSpace(s)
20+
if trimmed == "" {
21+
return nil, fmt.Errorf("expected numeric value, got ''")
22+
}
23+
base := 10
24+
digits := trimmed
25+
if strings.HasPrefix(digits, "0x") || strings.HasPrefix(digits, "0X") {
26+
base = 16
27+
digits = digits[2:]
28+
if digits == "" {
29+
return nil, fmt.Errorf("expected numeric value, got '%s'", trimmed)
30+
}
31+
}
32+
n, ok := new(big.Int).SetString(digits, base)
33+
if !ok {
34+
return nil, fmt.Errorf("expected numeric value, got '%s'", trimmed)
35+
}
36+
return n, nil
37+
}

pkg/bigint/parse_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package bigint
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
)
7+
8+
func TestParse(t *testing.T) {
9+
cases := []struct {
10+
name string
11+
in string
12+
want string // big.Int decimal string, ignored when wantErr is set
13+
wantErr string
14+
}{
15+
{name: "zero", in: "0", want: "0"},
16+
{name: "decimal", in: "4000000", want: "4000000"},
17+
{name: "leading zero is decimal not octal", in: "010", want: "10"},
18+
{name: "negative decimal", in: "-5", want: "-5"},
19+
{name: "lowercase hex", in: "0x3D0900", want: "4000000"},
20+
{name: "uppercase hex prefix", in: "0X3d0900", want: "4000000"},
21+
{name: "hex with leading zero digits", in: "0x00ff", want: "255"},
22+
{name: "trims whitespace", in: " 42 ", want: "42"},
23+
{name: "huge value preserved", in: "100000000000000000000", want: "100000000000000000000"},
24+
25+
{name: "empty", in: "", wantErr: "expected numeric value, got ''"},
26+
{name: "whitespace only", in: " ", wantErr: "expected numeric value, got ''"},
27+
{name: "0x with no digits", in: "0x", wantErr: "expected numeric value, got '0x'"},
28+
{name: "non-numeric", in: "MAX", wantErr: "expected numeric value, got 'MAX'"},
29+
{name: "invalid hex digits", in: "0xZZZ", wantErr: "expected numeric value, got '0xZZZ'"},
30+
{name: "decimal with letters", in: "12abc", wantErr: "expected numeric value, got '12abc'"},
31+
}
32+
33+
for _, tc := range cases {
34+
t.Run(tc.name, func(t *testing.T) {
35+
got, err := Parse(tc.in)
36+
if tc.wantErr != "" {
37+
if err == nil {
38+
t.Fatalf("Parse(%q) = %v, want error %q", tc.in, got, tc.wantErr)
39+
}
40+
if err.Error() != tc.wantErr {
41+
t.Fatalf("Parse(%q) error = %q, want %q", tc.in, err.Error(), tc.wantErr)
42+
}
43+
return
44+
}
45+
if err != nil {
46+
t.Fatalf("Parse(%q) unexpected error: %v", tc.in, err)
47+
}
48+
want, _ := new(big.Int).SetString(tc.want, 10)
49+
if got.Cmp(want) != 0 {
50+
t.Fatalf("Parse(%q) = %s, want %s", tc.in, got.String(), tc.want)
51+
}
52+
})
53+
}
54+
}

pkg/erc4337/userop/parse.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"reflect"
1010
"sync"
1111

12+
"github.com/AvaProtocol/EigenLayer-AVS/pkg/bigint"
1213
"github.com/ethereum/go-ethereum/common"
1314
validator "github.com/go-playground/validator/v10"
1415
"github.com/mitchellh/mapstructure"
@@ -40,15 +41,13 @@ func decodeOpTypes(
4041

4142
// String to big.Int conversion
4243
if f == reflect.String && t == reflect.Struct {
43-
n := new(big.Int)
44-
var ok bool
4544
dataStr, ok := data.(string)
4645
if !ok {
4746
return nil, errors.New("expected string for bigInt conversion")
4847
}
49-
n, ok = n.SetString(dataStr, 0)
50-
if !ok {
51-
return nil, errors.New("bigInt conversion failed")
48+
n, err := bigint.Parse(dataStr)
49+
if err != nil {
50+
return nil, fmt.Errorf("bigInt conversion failed: %w", err)
5251
}
5352
return n, nil
5453
}

0 commit comments

Comments
 (0)