Skip to content
This repository was archived by the owner on Oct 20, 2024. It is now read-only.

Commit ed1d2f6

Browse files
authored
Add validateInitCode to userOp checks (#67)
1 parent 1b2b732 commit ed1d2f6

File tree

12 files changed

+291
-87
lines changed

12 files changed

+291
-87
lines changed

internal/testutils/constants.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package testutils
2+
3+
import "math/big"
4+
5+
var (
6+
OneETH = big.NewInt(1000000000000000000)
7+
DefaultUnstakeDelaySec = uint32(86400)
8+
)

internal/testutils/opmock.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package testutils
22

3-
import "github.com/stackup-wallet/stackup-bundler/pkg/userop"
3+
import (
4+
"github.com/ethereum/go-ethereum/common"
5+
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
6+
)
47

58
var (
69
MockUserOpData = map[string]any{
@@ -16,7 +19,7 @@ var (
1619
"preVerificationGas": "0xc650",
1720
"signature": "0xa925dcc5e5131636e244d4405334c25f034ebdd85c0cb12e8cdb13c15249c2d466d0bade18e2cafd3513497f7f968dcbb63e519acd9b76dcae7acd61f11aa8421b",
1821
}
19-
MockByteCode = "0x6080604052"
22+
MockByteCode = common.Hex2Bytes("6080604052")
2023
)
2124

2225
// Returns a valid initial userOperation for an EIP-4337 account.

pkg/modules/checks/initcode.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package checks
2+
3+
import (
4+
"errors"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
8+
)
9+
10+
// ValidateInitCode checks if initCode is not empty and gets the factory address. If factory address is valid
11+
// it calls a generic function that can retrieve the stake from the EntryPoint.
12+
func ValidateInitCode(op *userop.UserOperation, gs GetStakeFunc) error {
13+
if len(op.InitCode) == 0 {
14+
return nil
15+
}
16+
17+
f := op.GetFactory()
18+
if f == common.HexToAddress("0x") {
19+
return errors.New("initCode: does not contain a valid address")
20+
}
21+
22+
_, err := gs(f)
23+
if err != nil {
24+
return err
25+
}
26+
27+
return nil
28+
}

pkg/modules/checks/initcode_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package checks
2+
3+
import (
4+
"testing"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
"github.com/stackup-wallet/stackup-bundler/internal/testutils"
8+
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint"
9+
)
10+
11+
// TestInitCodeDNE calls checks.ValidateInitCode where initCode does not exist. Expect nil.
12+
func TestInitCodeDNE(t *testing.T) {
13+
op := testutils.MockValidInitUserOp()
14+
op.InitCode = []byte{}
15+
err := ValidateInitCode(op, func(f common.Address) (*entrypoint.IStakeManagerDepositInfo, error) {
16+
return nil, nil
17+
})
18+
19+
if err != nil {
20+
t.Fatalf(`got err %v, want nil`, err)
21+
}
22+
}
23+
24+
// TestInitCodeContainsAddress calls checks.ValidateInitCode where initCode exist without a valid address.
25+
// Expect error.
26+
func TestInitCodeContainsAddress(t *testing.T) {
27+
op := testutils.MockValidInitUserOp()
28+
op.InitCode = []byte("1234")
29+
err := ValidateInitCode(op, func(f common.Address) (*entrypoint.IStakeManagerDepositInfo, error) {
30+
return nil, nil
31+
})
32+
33+
if err == nil {
34+
t.Fatalf("got nil, want err")
35+
}
36+
}
37+
38+
// TestGetStakeFuncReceivesFactory calls checks.ValidateInitCode where initCode exist and calls getStakeFunc
39+
// with the correct factory address.
40+
func TestGetStakeFuncReceivesFactory(t *testing.T) {
41+
op := testutils.MockValidInitUserOp()
42+
isCalled := false
43+
_ = ValidateInitCode(op, func(f common.Address) (*entrypoint.IStakeManagerDepositInfo, error) {
44+
if f != op.GetFactory() {
45+
t.Fatalf("got %s, want %s", f.String(), op.GetFactory())
46+
}
47+
48+
isCalled = true
49+
return nil, nil
50+
})
51+
52+
if !isCalled {
53+
t.Fatalf("getStakeFunc was not called")
54+
}
55+
}
56+
57+
// TestInitCodeExists calls checks.ValidateInitCode where valid initCode does exist. Expect nil.
58+
func TestInitCodeExists(t *testing.T) {
59+
op := testutils.MockValidInitUserOp()
60+
err := ValidateInitCode(op, func(f common.Address) (*entrypoint.IStakeManagerDepositInfo, error) {
61+
return nil, nil
62+
})
63+
64+
if err != nil {
65+
t.Fatalf(`got err %v, want nil`, err)
66+
}
67+
}

pkg/modules/checks/sender.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
package checks
22

33
import (
4-
"context"
54
"errors"
65

7-
"github.com/ethereum/go-ethereum/ethclient"
86
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
97
)
108

11-
// Checks that the sender is an existing contract, or the initCode is not empty (but not both)
12-
func checkSender(eth *ethclient.Client, op *userop.UserOperation) error {
13-
bytecode, err := eth.CodeAt(context.Background(), op.Sender, nil)
9+
// ValidateSender accepts a userOp and a generic function that can retrieve the bytecode of the sender.
10+
// Either the sender is deployed (non-zero length bytecode) or the initCode is not empty (but not both).
11+
func ValidateSender(op *userop.UserOperation, gc GetCodeFunc) error {
12+
bytecode, err := gc(op.Sender)
1413
if err != nil {
1514
return err
1615
}

pkg/modules/checks/sender_test.go

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,68 +3,60 @@ package checks
33
import (
44
"testing"
55

6-
"github.com/ethereum/go-ethereum/ethclient"
6+
"github.com/ethereum/go-ethereum/common"
77
"github.com/stackup-wallet/stackup-bundler/internal/testutils"
88
)
99

10-
// TestSenderExistAndInitCodeDNE calls checks.CheckSender where sender contract exist and initCode does not.
11-
// Expect nil.
10+
// TestSenderExistAndInitCodeDNE calls checks.ValidateSender where sender contract exist and initCode does
11+
// not. Expect nil.
1212
func TestSenderExistAndInitCodeDNE(t *testing.T) {
13-
server := testutils.EthMock(testutils.MethodMocks{
14-
"eth_getCode": testutils.MockByteCode,
15-
})
16-
defer server.Close()
17-
18-
eth, _ := ethclient.Dial(server.URL)
1913
op := testutils.MockValidInitUserOp()
2014
op.InitCode = []byte{}
21-
if err := checkSender(eth, op); err != nil {
15+
err := ValidateSender(op, func(s common.Address) ([]byte, error) {
16+
return testutils.MockByteCode, nil
17+
})
18+
19+
if err != nil {
2220
t.Fatalf(`got err %v, want nil`, err)
2321
}
2422
}
2523

26-
// TestSenderAndInitCodeExist calls checks.CheckSender where sender contract and initCode exist. Expect
24+
// TestSenderAndInitCodeExist calls checks.ValidateSender where sender contract and initCode exist. Expect
2725
// error.
2826
func TestSenderAndInitCodeExist(t *testing.T) {
29-
server := testutils.EthMock(testutils.MethodMocks{
30-
"eth_getCode": testutils.MockByteCode,
27+
op := testutils.MockValidInitUserOp()
28+
err := ValidateSender(op, func(s common.Address) ([]byte, error) {
29+
return testutils.MockByteCode, nil
3130
})
32-
defer server.Close()
3331

34-
eth, _ := ethclient.Dial(server.URL)
35-
op := testutils.MockValidInitUserOp()
36-
if err := checkSender(eth, op); err == nil {
32+
if err == nil {
3733
t.Fatalf(`got nil, want err`)
3834
}
3935
}
4036

41-
// TestSenderDNEAndInitCodeExist calls checks.CheckSender where sender contract does not exist and initCode
42-
// does. Expect nil.
37+
// TestSenderDNEAndInitCodeExist calls checks.ValidateSender where sender contract does not exist and
38+
// initCode does. Expect nil.
4339
func TestSenderDNEAndInitCodeExist(t *testing.T) {
44-
server := testutils.EthMock(testutils.MethodMocks{
45-
"eth_getCode": "0x",
40+
op := testutils.MockValidInitUserOp()
41+
err := ValidateSender(op, func(s common.Address) ([]byte, error) {
42+
return []byte{}, nil
4643
})
47-
defer server.Close()
4844

49-
eth, _ := ethclient.Dial(server.URL)
50-
op := testutils.MockValidInitUserOp()
51-
if err := checkSender(eth, op); err != nil {
45+
if err != nil {
5246
t.Fatalf(`got err %v, want nil`, err)
5347
}
5448
}
5549

56-
// TestSenderAndInitCodeDNE calls checks.CheckSender where sender contract and initCode does not exist.
50+
// TestSenderAndInitCodeDNE calls checks.ValidateSender where sender contract and initCode does not exist.
5751
// Expect error.
5852
func TestSenderAndInitCodeDNE(t *testing.T) {
59-
server := testutils.EthMock(testutils.MethodMocks{
60-
"eth_getCode": "0x",
61-
})
62-
defer server.Close()
63-
64-
eth, _ := ethclient.Dial(server.URL)
6553
op := testutils.MockValidInitUserOp()
6654
op.InitCode = []byte{}
67-
if err := checkSender(eth, op); err == nil {
55+
err := ValidateSender(op, func(s common.Address) ([]byte, error) {
56+
return []byte{}, nil
57+
})
58+
59+
if err == nil {
6860
t.Fatalf(`got nil, want err`)
6961
}
7062
}

pkg/modules/checks/standalone.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,15 @@ func (s *Standalone) ValidateOpValues() modules.UserOpHandlerFunc {
3939
return err
4040
}
4141

42+
getCode := getCodeWithEthClient(s.eth)
43+
getStake, err := getStakeWithEthClient(ctx, s.eth)
44+
if err != nil {
45+
return err
46+
}
47+
4248
g := new(errgroup.Group)
43-
g.Go(func() error { return checkSender(s.eth, ctx.UserOp) })
49+
g.Go(func() error { return ValidateSender(ctx.UserOp, getCode) })
50+
g.Go(func() error { return ValidateInitCode(ctx.UserOp, getStake) })
4451
g.Go(func() error { return checkVerificationGas(s.maxVerificationGas, ctx.UserOp) })
4552
g.Go(func() error { return checkPaymasterAndData(s.eth, ep, ctx.UserOp) })
4653
g.Go(func() error { return checkCallGasLimit(ctx.UserOp) })

pkg/modules/checks/utils.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package checks
2+
3+
import (
4+
"context"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
"github.com/ethereum/go-ethereum/ethclient"
8+
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint"
9+
"github.com/stackup-wallet/stackup-bundler/pkg/modules"
10+
)
11+
12+
// GetCodeFunc provides a general interface for retrieving the bytecode for a given address.
13+
type GetCodeFunc = func(addr common.Address) ([]byte, error)
14+
15+
// GetStakeFunc provides a general interface for retrieving the EntryPoint stake for a given address.
16+
type GetStakeFunc = func(entity common.Address) (*entrypoint.IStakeManagerDepositInfo, error)
17+
18+
// getCodeWithEthClient returns a GetCodeFunc that uses an eth client to call eth_getCode.
19+
func getCodeWithEthClient(eth *ethclient.Client) GetCodeFunc {
20+
return func(addr common.Address) ([]byte, error) {
21+
return eth.CodeAt(context.Background(), addr, nil)
22+
}
23+
}
24+
25+
// getStakeWithEthClient returns a GetStakeFunc that uses an EntryPoint binding to get stake info and adds it
26+
// to the current context.
27+
func getStakeWithEthClient(ctx *modules.UserOpHandlerCtx, eth *ethclient.Client) (GetStakeFunc, error) {
28+
ep, err := entrypoint.NewEntrypoint(ctx.EntryPoint, eth)
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
return func(addr common.Address) (*entrypoint.IStakeManagerDepositInfo, error) {
34+
dep, err := ep.GetDepositInfo(nil, addr)
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
ctx.AddDepositInfo(addr, &dep)
40+
return &dep, nil
41+
}, nil
42+
}

pkg/modules/context.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,21 @@ import (
44
"math/big"
55

66
"github.com/ethereum/go-ethereum/common"
7+
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint"
78
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
89
)
910

11+
// BatchHandlerCtx is the object passed to BatchHandler functions during the Bundler's Run process. It
12+
// also contains a Data field for adding arbitrary key-value pairs to the context. These values will be
13+
// logged by the Bundler at the end of each run.
14+
type BatchHandlerCtx struct {
15+
Batch []*userop.UserOperation
16+
PendingRemoval []*userop.UserOperation
17+
EntryPoint common.Address
18+
ChainID *big.Int
19+
Data map[string]any
20+
}
21+
1022
// NewBatchHandlerContext creates a new BatchHandlerCtx using a copy of the given batch.
1123
func NewBatchHandlerContext(batch []*userop.UserOperation, entryPoint common.Address, chainID *big.Int) *BatchHandlerCtx {
1224
var copy []*userop.UserOperation
@@ -21,11 +33,52 @@ func NewBatchHandlerContext(batch []*userop.UserOperation, entryPoint common.Add
2133
}
2234
}
2335

36+
// MarkOpIndexForRemoval will remove the op by index from the batch and add it to the pending removal array.
37+
// This should be used for ops that are not to be included on-chain and dropped from the mempool.
38+
func (c *BatchHandlerCtx) MarkOpIndexForRemoval(index int) {
39+
batch := []*userop.UserOperation{}
40+
var op *userop.UserOperation
41+
for i, curr := range c.Batch {
42+
if i == index {
43+
op = curr
44+
} else {
45+
batch = append(batch, curr)
46+
}
47+
}
48+
if op == nil {
49+
return
50+
}
51+
52+
c.Batch = batch
53+
c.PendingRemoval = append(c.PendingRemoval, op)
54+
}
55+
56+
// UserOpHandlerCtx is the object passed to UserOpHandler functions during the Client's SendUserOperation
57+
// process.
58+
type UserOpHandlerCtx struct {
59+
UserOp *userop.UserOperation
60+
EntryPoint common.Address
61+
ChainID *big.Int
62+
deposits map[common.Address]*entrypoint.IStakeManagerDepositInfo
63+
}
64+
2465
// NewUserOpHandlerContext creates a new UserOpHandlerCtx using a given op.
2566
func NewUserOpHandlerContext(op *userop.UserOperation, entryPoint common.Address, chainID *big.Int) *UserOpHandlerCtx {
2667
return &UserOpHandlerCtx{
2768
UserOp: op,
2869
EntryPoint: entryPoint,
2970
ChainID: chainID,
71+
deposits: make(map[common.Address]*entrypoint.IStakeManagerDepositInfo),
3072
}
3173
}
74+
75+
// AddDepositInfo adds any entity's EntryPoint stake info to the current context.
76+
func (c *UserOpHandlerCtx) AddDepositInfo(entity common.Address, dep *entrypoint.IStakeManagerDepositInfo) {
77+
c.deposits[entity] = dep
78+
}
79+
80+
// GetDepositInfo retrieves any entity's EntryPoint stake info from the current context if it was previously
81+
// added. Otherwise returns nil
82+
func (c *UserOpHandlerCtx) GetDepositInfo(entity common.Address) *entrypoint.IStakeManagerDepositInfo {
83+
return c.deposits[entity]
84+
}

0 commit comments

Comments
 (0)