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

Commit 4902e01

Browse files
authored
Validate entity storage access (#76)
1 parent a03d6ce commit 4902e01

File tree

13 files changed

+600
-42
lines changed

13 files changed

+600
-42
lines changed

internal/config/values.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type Values struct {
2323
Beneficiary string
2424

2525
// Undocumented variables.
26+
DebugMode bool
2627
GinMode string
2728
BundlerCollectorTracer string
2829
}
@@ -45,6 +46,7 @@ func GetValues() *Values {
4546
viper.SetDefault("erc4337_bundler_data_directory", "/tmp/stackup_bundler")
4647
viper.SetDefault("erc4337_bundler_supported_entry_points", "0x1306b01bC3e4AD202612D3843387e94737673F53")
4748
viper.SetDefault("erc4337_bundler_max_verification_gas", 1500000)
49+
viper.SetDefault("erc4337_bundler_debug_mode", false)
4850
viper.SetDefault("erc4337_bundler_gin_mode", gin.ReleaseMode)
4951

5052
// Read in from .env file if available
@@ -118,6 +120,7 @@ func GetValues() *Values {
118120
supportedEntryPoints := envArrayToAddressSlice(viper.GetString("erc4337_bundler_supported_entry_points"))
119121
beneficiary := viper.GetString("erc4337_bundler_beneficiary")
120122
maxVerificationGas := big.NewInt(int64(viper.GetInt("erc4337_bundler_max_verification_gas")))
123+
debugMode := viper.GetBool("erc4337_bundler_debug_mode")
121124
ginMode := viper.GetString("erc4337_bundler_gin_mode")
122125
return &Values{
123126
PrivateKey: privateKey,
@@ -127,6 +130,7 @@ func GetValues() *Values {
127130
SupportedEntryPoints: supportedEntryPoints,
128131
Beneficiary: beneficiary,
129132
MaxVerificationGas: maxVerificationGas,
133+
DebugMode: debugMode,
130134
GinMode: ginMode,
131135
BundlerCollectorTracer: bct,
132136
}

internal/start/private.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ func PrivateMode() {
9191
paymaster.IncOpsSeen(),
9292
)
9393

94+
// init Debug
95+
var d *client.Debug
96+
if conf.DebugMode {
97+
d = client.NewDebug(eoa, eth, mem, chain, conf.SupportedEntryPoints[0], beneficiary)
98+
}
99+
94100
// Init Bundler
95101
b := bundler.New(mem, chain, conf.SupportedEntryPoints)
96102
b.UseLogger(logr)
@@ -120,7 +126,7 @@ func PrivateMode() {
120126
r.POST(
121127
"/",
122128
relayer.FilterByClientID(),
123-
jsonrpc.Controller(client.NewRpcAdapter(c)),
129+
jsonrpc.Controller(client.NewRpcAdapter(c, d)),
124130
relayer.MapUserOpHashToClientID(),
125131
)
126132
if err := r.Run(fmt.Sprintf(":%d", conf.Port)); err != nil {

internal/utils/stack.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package utils
2+
3+
type Stack[T any] struct {
4+
keys []T
5+
}
6+
7+
func NewStack[T any]() *Stack[T] {
8+
return &Stack[T]{nil}
9+
}
10+
11+
func (stack *Stack[T]) Push(key T) {
12+
stack.keys = append(stack.keys, key)
13+
}
14+
15+
func (stack *Stack[T]) Top() (T, bool) {
16+
var x T
17+
if len(stack.keys) > 0 {
18+
x = stack.keys[len(stack.keys)-1]
19+
return x, true
20+
}
21+
return x, false
22+
}
23+
24+
func (stack *Stack[T]) Pop() (T, bool) {
25+
var x T
26+
if len(stack.keys) > 0 {
27+
x, stack.keys = stack.keys[len(stack.keys)-1], stack.keys[:len(stack.keys)-1]
28+
return x, true
29+
}
30+
return x, false
31+
}
32+
33+
func (stack *Stack[T]) IsEmpty() bool {
34+
return len(stack.keys) == 0
35+
}

pkg/client/client.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"math/big"
77

88
"github.com/ethereum/go-ethereum/common"
9+
"github.com/ethereum/go-ethereum/common/hexutil"
910
"github.com/go-logr/logr"
1011
"github.com/stackup-wallet/stackup-bundler/internal/logger"
1112
"github.com/stackup-wallet/stackup-bundler/pkg/mempool"
@@ -119,5 +120,5 @@ func (i *Client) SupportedEntryPoints() ([]string, error) {
119120
// ChainID implements the method call for eth_chainId. It returns the current chainID used by the client.
120121
// This method is used to validate that the client's chainID is in sync with the caller.
121122
func (i *Client) ChainID() (string, error) {
122-
return i.chainID.String(), nil
123+
return hexutil.EncodeBig(i.chainID), nil
123124
}

pkg/client/debug.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package client
2+
3+
import (
4+
"errors"
5+
"math/big"
6+
7+
"github.com/ethereum/go-ethereum/common"
8+
"github.com/ethereum/go-ethereum/ethclient"
9+
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint"
10+
"github.com/stackup-wallet/stackup-bundler/pkg/mempool"
11+
"github.com/stackup-wallet/stackup-bundler/pkg/signer"
12+
)
13+
14+
// Debug exposes methods used for testing the bundler. These should not be made available in production.
15+
type Debug struct {
16+
eoa *signer.EOA
17+
eth *ethclient.Client
18+
mempool *mempool.Mempool
19+
chainID *big.Int
20+
entrypoint common.Address
21+
beneficiary common.Address
22+
}
23+
24+
func NewDebug(
25+
eoa *signer.EOA,
26+
eth *ethclient.Client,
27+
mempool *mempool.Mempool,
28+
chainID *big.Int,
29+
entrypoint common.Address,
30+
beneficiary common.Address,
31+
) *Debug {
32+
return &Debug{eoa, eth, mempool, chainID, entrypoint, beneficiary}
33+
}
34+
35+
// SendBundleNow forces the bundler to build and execute a bundle from the mempool as handleOps() transaction.
36+
func (d *Debug) SendBundleNow() (string, error) {
37+
batch, err := d.mempool.BundleOps(d.entrypoint)
38+
if err != nil {
39+
return "", err
40+
}
41+
42+
est, revert, err := entrypoint.EstimateHandleOpsGas(
43+
d.eoa,
44+
d.eth,
45+
d.chainID,
46+
d.entrypoint,
47+
batch,
48+
d.beneficiary,
49+
)
50+
if err != nil {
51+
return "", err
52+
} else if revert != nil {
53+
return "", errors.New("debug: bad batch during estimate")
54+
}
55+
56+
txn, revert, err := entrypoint.HandleOps(
57+
d.eoa,
58+
d.eth,
59+
d.chainID,
60+
d.entrypoint,
61+
batch,
62+
d.beneficiary,
63+
est,
64+
big.NewInt((int64(est))),
65+
big.NewInt((int64(est))),
66+
)
67+
if err != nil {
68+
return "", err
69+
} else if revert != nil {
70+
return "", errors.New("debug: bad batch during call")
71+
}
72+
73+
return txn.Hash().String(), nil
74+
}

pkg/client/rpc.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package client
22

3+
import (
4+
"errors"
5+
)
6+
37
// RpcAdapter is an adapter for routing JSON-RPC method calls to the correct client functions.
48
type RpcAdapter struct {
59
client *Client
10+
debug *Debug
611
}
712

813
// NewRpcAdapter initializes a new RpcAdapter which can be used with a JSON-RPC server.
9-
func NewRpcAdapter(client *Client) *RpcAdapter {
10-
return &RpcAdapter{client}
14+
func NewRpcAdapter(client *Client, debug *Debug) *RpcAdapter {
15+
return &RpcAdapter{client, debug}
1116
}
1217

1318
// Eth_sendUserOperation routes eth_sendUserOperation method calls to *Client.SendUserOperation.
@@ -24,3 +29,12 @@ func (r *RpcAdapter) Eth_supportedEntryPoints() ([]string, error) {
2429
func (r *RpcAdapter) Eth_chainId() (string, error) {
2530
return r.client.ChainID()
2631
}
32+
33+
// Debug_bundler_sendBundleNow routes eth_chainId method calls to *Debug.SendBundleNow.
34+
func (r *RpcAdapter) Debug_bundler_sendBundleNow() (string, error) {
35+
if r.debug == nil {
36+
return "", errors.New("rpc: debug mode is not enabled")
37+
}
38+
39+
return r.debug.SendBundleNow()
40+
}

pkg/entrypoint/methods.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package entrypoint
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/ethereum/go-ethereum/accounts/abi"
8+
"github.com/ethereum/go-ethereum/common/hexutil"
9+
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
10+
)
11+
12+
var (
13+
bytes32, _ = abi.NewType("bytes32", "", nil)
14+
uint256, _ = abi.NewType("uint256", "", nil)
15+
bytes, _ = abi.NewType("bytes", "", nil)
16+
validatePaymasterUserOpMethod = abi.NewMethod(
17+
"validatePaymasterUserOp",
18+
"validatePaymasterUserOp",
19+
abi.Function,
20+
"",
21+
false,
22+
false,
23+
abi.Arguments{
24+
{Name: "userOp", Type: userop.UserOpType},
25+
{Name: "userOpHash", Type: bytes32},
26+
{Name: "maxCost", Type: uint256},
27+
},
28+
abi.Arguments{
29+
{Name: "context", Type: bytes},
30+
{Name: "deadline", Type: uint256},
31+
},
32+
)
33+
validatePaymasterUserOpSelector = hexutil.Encode(validatePaymasterUserOpMethod.ID)
34+
)
35+
36+
type validatePaymasterUserOpOutput struct {
37+
Context []byte
38+
}
39+
40+
func decodeValidatePaymasterUserOpOutput(ret any) (*validatePaymasterUserOpOutput, error) {
41+
hex, ok := ret.(string)
42+
if !ok {
43+
return nil, errors.New("validatePaymasterUserOp: cannot assert type: hex is not of type string")
44+
}
45+
data, err := hexutil.Decode(hex)
46+
if err != nil {
47+
return nil, fmt.Errorf("validatePaymasterUserOp: %s", err)
48+
}
49+
50+
args, err := validatePaymasterUserOpMethod.Outputs.Unpack(data)
51+
if err != nil {
52+
return nil, fmt.Errorf("validatePaymasterUserOp: %s", err)
53+
}
54+
if len(args) != 2 {
55+
return nil, fmt.Errorf(
56+
"validatePaymasterUserOp: invalid args length: expected 2, got %d",
57+
len(args),
58+
)
59+
}
60+
61+
ctx, ok := args[0].([]byte)
62+
if !ok {
63+
return nil, errors.New("validatePaymasterUserOp: cannot assert type: hex is not of type string")
64+
}
65+
66+
return &validatePaymasterUserOpOutput{
67+
Context: ctx,
68+
}, nil
69+
}

0 commit comments

Comments
 (0)