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

Commit 58b77a6

Browse files
authored
call debug_traceCall and filter for base forbidden opcodes (#48)
1 parent e30ecea commit 58b77a6

File tree

4 files changed

+156
-18
lines changed

4 files changed

+156
-18
lines changed

internal/start/private.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
badger "github.com/dgraph-io/badger/v3"
1111
"github.com/ethereum/go-ethereum/common"
1212
"github.com/ethereum/go-ethereum/ethclient"
13+
"github.com/ethereum/go-ethereum/rpc"
1314
"github.com/gin-contrib/cors"
1415
"github.com/gin-gonic/gin"
1516
"github.com/stackup-wallet/stackup-bundler/internal/config"
@@ -59,11 +60,13 @@ func PrivateMode() {
5960
defer db.Close()
6061
runDBGarbageCollection(db)
6162

62-
eth, err := ethclient.Dial(conf.EthClientUrl)
63+
rpc, err := rpc.Dial(conf.EthClientUrl)
6364
if err != nil {
6465
log.Fatal(err)
6566
}
6667

68+
eth := ethclient.NewClient(rpc)
69+
6770
chain, err := eth.ChainID(context.Background())
6871
if err != nil {
6972
log.Fatal(err)
@@ -74,7 +77,7 @@ func PrivateMode() {
7477
log.Fatal(err)
7578
}
7679

77-
check := checks.New(eth, conf.MaxVerificationGas)
80+
check := checks.New(rpc, conf.MaxVerificationGas)
7881
relayer := relay.New(db, eoa, eth, chain, beneficiary, logr)
7982
paymaster := paymaster.New(db)
8083

pkg/entrypoint/handleops.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
1616
)
1717

18-
func ToAbiType(batch []*userop.UserOperation) []UserOperation {
18+
func toAbiType(batch []*userop.UserOperation) []UserOperation {
1919
ops := []UserOperation{}
2020
for _, op := range batch {
2121
ops = append(ops, UserOperation(*op))
@@ -46,7 +46,7 @@ func EstimateHandleOpsGas(
4646
auth.GasLimit = math.MaxUint64
4747
auth.NoSend = true
4848

49-
tx, err := ep.HandleOps(auth, ToAbiType(batch), beneficiary)
49+
tx, err := ep.HandleOps(auth, toAbiType(batch), beneficiary)
5050
if err != nil {
5151
return 0, nil, err
5252
}
@@ -99,7 +99,7 @@ func HandleOps(
9999
auth.GasTipCap = tip
100100
auth.GasFeeCap = maxFee
101101

102-
txn, err = ep.HandleOps(auth, ToAbiType(batch), beneficiary)
102+
txn, err = ep.HandleOps(auth, toAbiType(batch), beneficiary)
103103
if err != nil {
104104
revert, err := newFailedOpRevert(err)
105105
if err != nil {

pkg/entrypoint/simulation.go

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,70 @@
11
package entrypoint
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
7+
"math"
8+
"math/big"
69

10+
mapset "github.com/deckarep/golang-set/v2"
11+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
712
"github.com/ethereum/go-ethereum/common"
13+
"github.com/ethereum/go-ethereum/common/hexutil"
14+
"github.com/ethereum/go-ethereum/crypto"
815
"github.com/ethereum/go-ethereum/ethclient"
16+
"github.com/ethereum/go-ethereum/rpc"
917
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
1018
)
1119

20+
var (
21+
// A dummy private key used to build *bind.TransactOpts for simulation.
22+
dummyPk, _ = crypto.GenerateKey()
23+
24+
// A marker to delimit between account and paymaster simulation.
25+
markerOpCode = "NUMBER"
26+
27+
// All opcodes executed at this depth are from the EntryPoint and allowed.
28+
allowedDepth = float64(1)
29+
30+
// The gas opcode is only allowed if followed immediately by callOpcodes.
31+
// gasOpCode = "GAS"
32+
33+
// Only one create2 opcode is allowed if these two conditions are met:
34+
// 1. op.initcode.length != 0
35+
// 2. During account simulation (i.e. before markerOpCode)
36+
// create2OpCode = "CREATE2"
37+
38+
// List of opcodes related to CALL.
39+
// callOpcodes = mapset.NewSet(
40+
// "CALL",
41+
// "DELEGATECALL",
42+
// "CALLCODE",
43+
// "STATICCALL",
44+
// )
45+
46+
// List of opcodes not allowed during simulation for depth > allowedDepth (i.e. account, paymaster, or
47+
// contracts called by them).
48+
baseForbiddenOpCodes = mapset.NewSet(
49+
"GASPRICE",
50+
"GASLIMIT",
51+
"DIFFICULTY",
52+
"TIMESTAMP",
53+
"BASEFEE",
54+
"BLOCKHASH",
55+
"NUMBER",
56+
"SELFBALANCE",
57+
"BALANCE",
58+
"ORIGIN",
59+
"CREATE",
60+
"COINBASE",
61+
)
62+
)
63+
1264
// SimulateValidation makes a static call to Entrypoint.simulateValidation(userop) and returns the
1365
// results without any state changes.
14-
func SimulateValidation(eth *ethclient.Client, entryPoint common.Address, op *userop.UserOperation) (*SimulationResultRevert, error) {
15-
ep, err := NewEntrypoint(entryPoint, eth)
66+
func SimulateValidation(rpc *rpc.Client, entryPoint common.Address, op *userop.UserOperation) (*SimulationResultRevert, error) {
67+
ep, err := NewEntrypoint(entryPoint, ethclient.NewClient(rpc))
1668
if err != nil {
1769
return nil, err
1870
}
@@ -33,6 +85,82 @@ func SimulateValidation(eth *ethclient.Client, entryPoint common.Address, op *us
3385
return nil, errors.New(fo.Reason)
3486
}
3587

36-
// TODO: Trace forbidden opcodes
3788
return sim, nil
3889
}
90+
91+
type structLog struct {
92+
Depth float64 `json:"depth"`
93+
Gas float64 `json:"gas"`
94+
GasCost float64 `json:"gasCost"`
95+
Op string `json:"op"`
96+
Pc float64 `json:"pc"`
97+
Stack []string `json:"stack"`
98+
}
99+
100+
type traceCallRes struct {
101+
Failed bool `json:"failed"`
102+
Gas float64 `json:"gas"`
103+
ReturnValue []byte `json:"returnValue"`
104+
StructLogs []structLog `json:"structLogs"`
105+
}
106+
107+
type traceCallReq struct {
108+
From common.Address `json:"from"`
109+
To common.Address `json:"to"`
110+
Data hexutil.Bytes `json:"data"`
111+
}
112+
113+
type traceCallOpts struct {
114+
DisableStorage bool `json:"disableStorage"`
115+
DisableMemory bool `json:"disableMemory"`
116+
}
117+
118+
// TraceSimulateValidation makes a debug_traceCall to Entrypoint.simulateValidation(userop) and returns the
119+
// results without any state changes.
120+
func TraceSimulateValidation(rpc *rpc.Client, entryPoint common.Address, op *userop.UserOperation, chainID *big.Int) error {
121+
ep, err := NewEntrypoint(entryPoint, ethclient.NewClient(rpc))
122+
if err != nil {
123+
return err
124+
}
125+
auth, err := bind.NewKeyedTransactorWithChainID(dummyPk, chainID)
126+
if err != nil {
127+
return err
128+
}
129+
auth.GasLimit = math.MaxUint64
130+
auth.NoSend = true
131+
tx, err := ep.SimulateValidation(auth, UserOperation(*op))
132+
if err != nil {
133+
return err
134+
}
135+
136+
var res traceCallRes
137+
req := traceCallReq{
138+
From: common.HexToAddress("0x"),
139+
To: entryPoint,
140+
Data: tx.Data(),
141+
}
142+
opts := traceCallOpts{
143+
DisableStorage: false,
144+
DisableMemory: false,
145+
}
146+
if err := rpc.CallContext(context.Background(), &res, "debug_traceCall", &req, "latest", &opts); err != nil {
147+
return err
148+
}
149+
150+
simFor := "account"
151+
for _, sl := range res.StructLogs {
152+
if sl.Depth == allowedDepth && sl.Op == markerOpCode {
153+
simFor = "paymaster"
154+
}
155+
156+
if sl.Depth == allowedDepth {
157+
continue
158+
}
159+
160+
if baseForbiddenOpCodes.Contains(sl.Op) {
161+
return fmt.Errorf("%s: uses forbidden opcode %s", simFor, sl.Op)
162+
}
163+
}
164+
165+
return nil
166+
}

pkg/modules/checks/standalone.go

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/ethereum/go-ethereum/common"
99
"github.com/ethereum/go-ethereum/ethclient"
10+
"github.com/ethereum/go-ethereum/rpc"
1011
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint"
1112
"github.com/stackup-wallet/stackup-bundler/pkg/modules"
1213
"golang.org/x/sync/errgroup"
@@ -16,14 +17,16 @@ import (
1617
// intended for bundlers that are independent of an Ethereum node and hence relies on a given ethClient to
1718
// query blockchain state.
1819
type Standalone struct {
20+
rpc *rpc.Client
1921
eth *ethclient.Client
2022
maxVerificationGas *big.Int
2123
}
2224

2325
// New returns a Standalone instance with methods that can be used in Client and Bundler modules to perform
2426
// standard checks as specified in EIP-4337.
25-
func New(eth *ethclient.Client, maxVerificationGas *big.Int) *Standalone {
26-
return &Standalone{eth, maxVerificationGas}
27+
func New(rpc *rpc.Client, maxVerificationGas *big.Int) *Standalone {
28+
eth := ethclient.NewClient(rpc)
29+
return &Standalone{rpc, eth, maxVerificationGas}
2730
}
2831

2932
// ValidateOpValues returns a UserOpHandler that runs through some first line sanity checks for new UserOps
@@ -42,19 +45,23 @@ func (s *Standalone) ValidateOpValues() modules.UserOpHandlerFunc {
4245
g.Go(func() error { return checkCallGasLimit(ctx.UserOp) })
4346
g.Go(func() error { return checkFeePerGas(s.eth, ctx.UserOp) })
4447

45-
if err := g.Wait(); err != nil {
46-
return err
47-
}
48-
return nil
48+
return g.Wait()
4949
}
5050
}
5151

52-
// SimulateOp returns a UserOpHandler that runs through simulation of new UserOps with the EntryPoint. This
53-
// should be done after all validations are complete.
52+
// SimulateOp returns a UserOpHandler that runs through simulation of new UserOps with the EntryPoint.
5453
func (s *Standalone) SimulateOp() modules.UserOpHandlerFunc {
5554
return func(ctx *modules.UserOpHandlerCtx) error {
56-
_, err := entrypoint.SimulateValidation(s.eth, ctx.EntryPoint, ctx.UserOp)
57-
return err
55+
g := new(errgroup.Group)
56+
g.Go(func() error {
57+
_, err := entrypoint.SimulateValidation(s.rpc, ctx.EntryPoint, ctx.UserOp)
58+
return err
59+
})
60+
g.Go(func() error {
61+
return entrypoint.TraceSimulateValidation(s.rpc, ctx.EntryPoint, ctx.UserOp, ctx.ChainID)
62+
})
63+
64+
return g.Wait()
5865
}
5966
}
6067

0 commit comments

Comments
 (0)