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

Commit 0763a82

Browse files
authored
Update PaymasterAndData check to latest EIP changes (#69)
1 parent 3c3cacb commit 0763a82

File tree

9 files changed

+215
-79
lines changed

9 files changed

+215
-79
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
![](https://i.imgur.com/t0P3vWU.png)
22

3+
![GitHub release (latest by date)](https://img.shields.io/github/v/release/stackup-wallet/stackup-bundler)
4+
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/stackup-wallet/stackup-bundler/pipeline.yml?branch=main)
5+
36
# Getting started
47

58
A modular Go implementation of an ERC-4337 Bundler.

internal/testutils/checksmock.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package testutils
2+
3+
import (
4+
"github.com/ethereum/go-ethereum/common"
5+
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint"
6+
)
7+
8+
func MockGetCode(addr common.Address) ([]byte, error) {
9+
return MockByteCode, nil
10+
}
11+
12+
func MockGetCodeZero(addr common.Address) ([]byte, error) {
13+
return []byte{}, nil
14+
}
15+
16+
func MockGetStake(addr common.Address) (*entrypoint.IStakeManagerDepositInfo, error) {
17+
return StakedDepositInfo, nil
18+
}
19+
20+
func MockGetStakeZeroDeposit(addr common.Address) (*entrypoint.IStakeManagerDepositInfo, error) {
21+
return StakedZeroDepositInfo, nil
22+
}
23+
24+
func MockGetNotStake(addr common.Address) (*entrypoint.IStakeManagerDepositInfo, error) {
25+
return NonStakedDepositInfo, nil
26+
}
27+
28+
func MockGetNotStakeZeroDeposit(addr common.Address) (*entrypoint.IStakeManagerDepositInfo, error) {
29+
return NonStakedZeroDepositInfo, nil
30+
}

internal/testutils/constants.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,41 @@
11
package testutils
22

3-
import "math/big"
3+
import (
4+
"math/big"
5+
"time"
6+
7+
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint"
8+
)
49

510
var (
611
OneETH = big.NewInt(1000000000000000000)
712
DefaultUnstakeDelaySec = uint32(86400)
13+
StakedDepositInfo = &entrypoint.IStakeManagerDepositInfo{
14+
Deposit: big.NewInt(OneETH.Int64()),
15+
Staked: true,
16+
Stake: big.NewInt(OneETH.Int64()),
17+
UnstakeDelaySec: DefaultUnstakeDelaySec,
18+
WithdrawTime: uint64(time.Now().Unix()),
19+
}
20+
StakedZeroDepositInfo = &entrypoint.IStakeManagerDepositInfo{
21+
Deposit: big.NewInt(0),
22+
Staked: true,
23+
Stake: big.NewInt(OneETH.Int64()),
24+
UnstakeDelaySec: DefaultUnstakeDelaySec,
25+
WithdrawTime: uint64(time.Now().Unix()),
26+
}
27+
NonStakedDepositInfo = &entrypoint.IStakeManagerDepositInfo{
28+
Deposit: big.NewInt(OneETH.Int64()),
29+
Staked: false,
30+
Stake: big.NewInt(0),
31+
UnstakeDelaySec: uint32(0),
32+
WithdrawTime: uint64(0),
33+
}
34+
NonStakedZeroDepositInfo = &entrypoint.IStakeManagerDepositInfo{
35+
Deposit: big.NewInt(0),
36+
Staked: false,
37+
Stake: big.NewInt(0),
38+
UnstakeDelaySec: uint32(0),
39+
WithdrawTime: uint64(0),
40+
}
841
)

pkg/modules/checks/paymaster.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+
"errors"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
8+
)
9+
10+
// ValidatePaymasterAndData checks the paymasterAndData is either zero bytes or the first 20 bytes contain an
11+
// address that
12+
//
13+
// 1. currently has nonempty code on chain
14+
// 2. has a sufficient deposit to pay for the UserOperation
15+
func ValidatePaymasterAndData(op *userop.UserOperation, gc GetCodeFunc, gs GetStakeFunc) error {
16+
if len(op.PaymasterAndData) == 0 {
17+
return nil
18+
}
19+
20+
if len(op.PaymasterAndData) < common.AddressLength {
21+
return errors.New("PaymasterAndData: invalid length")
22+
}
23+
24+
pm := op.GetPaymaster()
25+
bytecode, err := gc(pm)
26+
if err != nil {
27+
return err
28+
}
29+
if len(bytecode) == 0 {
30+
return errors.New("paymaster: code not deployed")
31+
}
32+
33+
dep, err := gs(pm)
34+
if err != nil {
35+
return err
36+
}
37+
if dep.Deposit.Cmp(op.GetMaxPrefund()) < 0 {
38+
return errors.New("paymaster: not enough deposit to cover max prefund")
39+
}
40+
41+
return nil
42+
}

pkg/modules/checks/paymaster_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package checks
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stackup-wallet/stackup-bundler/internal/testutils"
7+
)
8+
9+
// TestNilPaymasterAndData calls checks.ValidatePaymasterAndData with no paymaster set. Expects nil.
10+
func TestNilPaymasterAndData(t *testing.T) {
11+
op := testutils.MockValidInitUserOp()
12+
op.PaymasterAndData = []byte{}
13+
err := ValidatePaymasterAndData(op, testutils.MockGetCodeZero, testutils.MockGetNotStakeZeroDeposit)
14+
15+
if err != nil {
16+
t.Fatalf("got err %v, want nil", err)
17+
}
18+
}
19+
20+
// TestBadPaymasterAndData calls checks.ValidatePaymasterAndData with invalid paymaster set. Expects error.
21+
func TestBadPaymasterAndData(t *testing.T) {
22+
op := testutils.MockValidInitUserOp()
23+
op.PaymasterAndData = []byte("1234")
24+
err := ValidatePaymasterAndData(op, testutils.MockGetCodeZero, testutils.MockGetNotStakeZeroDeposit)
25+
26+
if err == nil {
27+
t.Fatal("got nil, want err")
28+
}
29+
}
30+
31+
// TestZeroByteCodePaymasterAndData calls checks.ValidatePaymasterAndData with paymaster contract not
32+
// deployed. Expects error.
33+
func TestZeroByteCodePaymasterAndData(t *testing.T) {
34+
op := testutils.MockValidInitUserOp()
35+
op.PaymasterAndData = op.Sender.Bytes()
36+
err := ValidatePaymasterAndData(op, testutils.MockGetCodeZero, testutils.MockGetNotStakeZeroDeposit)
37+
38+
if err == nil {
39+
t.Fatal("got nil, want err")
40+
}
41+
}
42+
43+
// TestNonStakedZeroDepositPaymasterAndData calls checks.ValidatePaymasterAndData with paymaster that is not
44+
// staked with zero deposit. Expects error.
45+
func TestNonStakedZeroDepositPaymasterAndData(t *testing.T) {
46+
op := testutils.MockValidInitUserOp()
47+
op.PaymasterAndData = op.Sender.Bytes()
48+
err := ValidatePaymasterAndData(op, testutils.MockGetCode, testutils.MockGetNotStakeZeroDeposit)
49+
50+
if err == nil {
51+
t.Fatal("got nil, want err")
52+
}
53+
}
54+
55+
// TestZeroDepositPaymasterAndData calls checks.ValidatePaymasterAndData with paymaster that is staked but
56+
// with zero deposit. Expects error.
57+
func TestZeroDepositPaymasterAndData(t *testing.T) {
58+
op := testutils.MockValidInitUserOp()
59+
op.PaymasterAndData = op.Sender.Bytes()
60+
err := ValidatePaymasterAndData(op, testutils.MockGetCode, testutils.MockGetStakeZeroDeposit)
61+
62+
if err == nil {
63+
t.Fatal("got nil, want err")
64+
}
65+
}
66+
67+
// TestNotStakedPaymasterAndData calls checks.ValidatePaymasterAndData with paymaster that is not staked and
68+
// has sufficient deposit. Expects nil.
69+
func TestNotStakedPaymasterAndData(t *testing.T) {
70+
op := testutils.MockValidInitUserOp()
71+
op.PaymasterAndData = op.Sender.Bytes()
72+
err := ValidatePaymasterAndData(op, testutils.MockGetCode, testutils.MockGetNotStake)
73+
74+
if err != nil {
75+
t.Fatalf("got %v, want nil", err)
76+
}
77+
}
78+
79+
// TestPaymasterAndData calls checks.ValidatePaymasterAndData with paymaster that is staked and has sufficient
80+
// deposit. Expects nil.
81+
func TestPaymasterAndData(t *testing.T) {
82+
op := testutils.MockValidInitUserOp()
83+
op.PaymasterAndData = op.Sender.Bytes()
84+
err := ValidatePaymasterAndData(op, testutils.MockGetCode, testutils.MockGetStake)
85+
86+
if err != nil {
87+
t.Fatalf("got err %v, want nil", err)
88+
}
89+
}

pkg/modules/checks/sanitychecks.go

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,13 @@ package checks
22

33
import (
44
"context"
5-
"errors"
65
"fmt"
76

8-
"github.com/ethereum/go-ethereum/common"
97
"github.com/ethereum/go-ethereum/ethclient"
10-
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint"
118
"github.com/stackup-wallet/stackup-bundler/pkg/gas"
129
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
1310
)
1411

15-
// Checks the paymasterAndData is either zero bytes or the first 20 bytes contain an address that
16-
//
17-
// 1. is not the zero address
18-
// 2. currently has nonempty code on chain
19-
// 3. has registered and staked
20-
// 4. has a sufficient deposit to pay for the UserOperation
21-
func checkPaymasterAndData(eth *ethclient.Client, ep *entrypoint.Entrypoint, op *userop.UserOperation) error {
22-
if len(op.PaymasterAndData) == 0 {
23-
return nil
24-
}
25-
26-
if len(op.PaymasterAndData) < common.AddressLength {
27-
return errors.New("PaymasterAndData: invalid length")
28-
}
29-
30-
address := op.GetPaymaster()
31-
if address == common.HexToAddress("0x") {
32-
return errors.New("paymaster: cannot be the zero address")
33-
}
34-
35-
bytecode, err := eth.CodeAt(context.Background(), address, nil)
36-
if err != nil {
37-
return err
38-
}
39-
if len(bytecode) == 0 {
40-
return errors.New("paymaster: code not deployed")
41-
}
42-
43-
dep, err := ep.GetDepositInfo(nil, address)
44-
if err != nil {
45-
return err
46-
}
47-
if !dep.Staked {
48-
return errors.New("paymaster: not staked on the entrypoint")
49-
}
50-
if dep.Deposit.Cmp(op.GetMaxPrefund()) < 0 {
51-
return errors.New("paymaster: not enough deposit to cover max prefund")
52-
}
53-
54-
return nil
55-
}
56-
5712
// Checks the callGasLimit is at least the cost of a CALL with non-zero value.
5813
func checkCallGasLimit(op *userop.UserOperation) error {
5914
cg := gas.NewDefaultOverhead().NonZeroValueCall()

pkg/modules/checks/sender_test.go

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package checks
33
import (
44
"testing"
55

6-
"github.com/ethereum/go-ethereum/common"
76
"github.com/stackup-wallet/stackup-bundler/internal/testutils"
87
)
98

@@ -12,9 +11,7 @@ import (
1211
func TestSenderExistAndInitCodeDNE(t *testing.T) {
1312
op := testutils.MockValidInitUserOp()
1413
op.InitCode = []byte{}
15-
err := ValidateSender(op, func(s common.Address) ([]byte, error) {
16-
return testutils.MockByteCode, nil
17-
})
14+
err := ValidateSender(op, testutils.MockGetCode)
1815

1916
if err != nil {
2017
t.Fatalf(`got err %v, want nil`, err)
@@ -25,9 +22,7 @@ func TestSenderExistAndInitCodeDNE(t *testing.T) {
2522
// error.
2623
func TestSenderAndInitCodeExist(t *testing.T) {
2724
op := testutils.MockValidInitUserOp()
28-
err := ValidateSender(op, func(s common.Address) ([]byte, error) {
29-
return testutils.MockByteCode, nil
30-
})
25+
err := ValidateSender(op, testutils.MockGetCode)
3126

3227
if err == nil {
3328
t.Fatalf(`got nil, want err`)
@@ -38,9 +33,7 @@ func TestSenderAndInitCodeExist(t *testing.T) {
3833
// initCode does. Expect nil.
3934
func TestSenderDNEAndInitCodeExist(t *testing.T) {
4035
op := testutils.MockValidInitUserOp()
41-
err := ValidateSender(op, func(s common.Address) ([]byte, error) {
42-
return []byte{}, nil
43-
})
36+
err := ValidateSender(op, testutils.MockGetCodeZero)
4437

4538
if err != nil {
4639
t.Fatalf(`got err %v, want nil`, err)
@@ -52,9 +45,7 @@ func TestSenderDNEAndInitCodeExist(t *testing.T) {
5245
func TestSenderAndInitCodeDNE(t *testing.T) {
5346
op := testutils.MockValidInitUserOp()
5447
op.InitCode = []byte{}
55-
err := ValidateSender(op, func(s common.Address) ([]byte, error) {
56-
return []byte{}, nil
57-
})
48+
err := ValidateSender(op, testutils.MockGetCodeZero)
5849

5950
if err == nil {
6051
t.Fatalf(`got nil, want err`)

pkg/modules/checks/standalone.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,17 @@ func New(rpc *rpc.Client, maxVerificationGas *big.Int, tracer string) *Standalon
3434
// received by the Client. This should be one of the first modules executed by the Client.
3535
func (s *Standalone) ValidateOpValues() modules.UserOpHandlerFunc {
3636
return func(ctx *modules.UserOpHandlerCtx) error {
37-
ep, err := entrypoint.NewEntrypoint(ctx.EntryPoint, s.eth)
38-
if err != nil {
39-
return err
40-
}
41-
42-
getCode := getCodeWithEthClient(s.eth)
43-
getStake, err := getStakeWithEthClient(ctx, s.eth)
37+
gc := getCodeWithEthClient(s.eth)
38+
gs, err := getStakeWithEthClient(ctx, s.eth)
4439
if err != nil {
4540
return err
4641
}
4742

4843
g := new(errgroup.Group)
49-
g.Go(func() error { return ValidateSender(ctx.UserOp, getCode) })
50-
g.Go(func() error { return ValidateInitCode(ctx.UserOp, getStake) })
44+
g.Go(func() error { return ValidateSender(ctx.UserOp, gc) })
45+
g.Go(func() error { return ValidateInitCode(ctx.UserOp, gs) })
5146
g.Go(func() error { return ValidateVerificationGas(ctx.UserOp, s.maxVerificationGas) })
52-
g.Go(func() error { return checkPaymasterAndData(s.eth, ep, ctx.UserOp) })
47+
g.Go(func() error { return ValidatePaymasterAndData(ctx.UserOp, gc, gs) })
5348
g.Go(func() error { return checkCallGasLimit(ctx.UserOp) })
5449
g.Go(func() error { return checkFeePerGas(s.eth, ctx.UserOp) })
5550

@@ -66,7 +61,13 @@ func (s *Standalone) SimulateOp() modules.UserOpHandlerFunc {
6661
return err
6762
})
6863
g.Go(func() error {
69-
return entrypoint.TraceSimulateValidation(s.rpc, ctx.EntryPoint, ctx.UserOp, ctx.ChainID, s.tracer)
64+
return entrypoint.TraceSimulateValidation(
65+
s.rpc,
66+
ctx.EntryPoint,
67+
ctx.UserOp,
68+
ctx.ChainID,
69+
s.tracer,
70+
)
7071
})
7172

7273
return g.Wait()

pkg/modules/context_test.go

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ package modules
33
import (
44
"math/big"
55
"testing"
6-
"time"
76

87
"github.com/ethereum/go-ethereum/common"
98
"github.com/stackup-wallet/stackup-bundler/internal/testutils"
10-
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint"
119
)
1210

1311
// TestAddDepositInfoToCtx verifies that stake info can be added to a context and later retrieved.
@@ -16,13 +14,7 @@ func TestAddDepositInfoToCtx(t *testing.T) {
1614
ctx := NewUserOpHandlerContext(op, common.HexToAddress("0x"), big.NewInt(1))
1715

1816
entity := op.GetFactory()
19-
dep := &entrypoint.IStakeManagerDepositInfo{
20-
Deposit: big.NewInt(testutils.OneETH.Int64()),
21-
Staked: true,
22-
Stake: big.NewInt(testutils.OneETH.Int64()),
23-
UnstakeDelaySec: testutils.DefaultUnstakeDelaySec,
24-
WithdrawTime: uint64(time.Now().Unix()),
25-
}
17+
dep := testutils.StakedDepositInfo
2618
ctx.AddDepositInfo(entity, dep)
2719

2820
if ctx.GetDepositInfo(entity) != dep {

0 commit comments

Comments
 (0)