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

Commit f6725b6

Browse files
authored
Fix estimate callGasLimit to account for value transfers (#204)
1 parent 3e70b53 commit f6725b6

File tree

4 files changed

+163
-107
lines changed

4 files changed

+163
-107
lines changed

pkg/entrypoint/execution/trace.go

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/ethereum/go-ethereum/accounts/abi/bind"
1010
"github.com/ethereum/go-ethereum/common"
1111
"github.com/ethereum/go-ethereum/common/hexutil"
12+
"github.com/ethereum/go-ethereum/core/types"
1213
"github.com/ethereum/go-ethereum/ethclient"
1314
ethRpc "github.com/ethereum/go-ethereum/rpc"
1415
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint"
@@ -19,6 +20,36 @@ import (
1920
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
2021
)
2122

23+
func parseUserOperationEvent(
24+
entryPoint common.Address,
25+
ep *entrypoint.Entrypoint,
26+
log *tracer.LogInfo,
27+
) (*entrypoint.EntrypointUserOperationEvent, error) {
28+
if log == nil {
29+
return nil, nil
30+
}
31+
32+
topics := []common.Hash{}
33+
for _, topic := range log.Topics {
34+
topics = append(topics, common.HexToHash(topic))
35+
}
36+
data, err := hexutil.Decode(log.Data)
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
ev, err := ep.ParseUserOperationEvent(types.Log{
42+
Address: entryPoint,
43+
Topics: topics,
44+
Data: data,
45+
})
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
return ev, nil
51+
}
52+
2253
func TraceSimulateHandleOp(
2354
rpc *ethRpc.Client,
2455
entryPoint common.Address,
@@ -27,20 +58,20 @@ func TraceSimulateHandleOp(
2758
customTracer string,
2859
target common.Address,
2960
data []byte,
30-
) (*reverts.ExecutionResultRevert, error) {
61+
) (*reverts.ExecutionResultRevert, *entrypoint.EntrypointUserOperationEvent, error) {
3162
ep, err := entrypoint.NewEntrypoint(entryPoint, ethclient.NewClient(rpc))
3263
if err != nil {
33-
return nil, err
64+
return nil, nil, err
3465
}
3566
auth, err := bind.NewKeyedTransactorWithChainID(utils.DummyPk, chainID)
3667
if err != nil {
37-
return nil, err
68+
return nil, nil, err
3869
}
3970
auth.GasLimit = math.MaxUint64
4071
auth.NoSend = true
4172
tx, err := ep.SimulateHandleOp(auth, entrypoint.UserOperation(*op), target, data)
4273
if err != nil {
43-
return nil, err
74+
return nil, nil, err
4475
}
4576

4677
var res tracer.BundlerErrorReturn
@@ -53,43 +84,48 @@ func TraceSimulateHandleOp(
5384
Tracer: customTracer,
5485
}
5586
if err := rpc.CallContext(context.Background(), &res, "debug_traceCall", &req, "latest", &opts); err != nil {
56-
return nil, err
87+
return nil, nil, err
5788
}
5889
outErr, err := errors.ParseHexToRpcDataError(res.Output)
5990
if err != nil {
60-
return nil, err
91+
return nil, nil, err
6192
}
6293
if res.ValidationOOG {
63-
return nil, errors.NewRPCError(errors.EXECUTION_REVERTED, "validation OOG", nil)
94+
return nil, nil, errors.NewRPCError(errors.EXECUTION_REVERTED, "validation OOG", nil)
6495
}
6596

6697
sim, simErr := reverts.NewExecutionResult(outErr)
6798
if simErr != nil {
6899
fo, foErr := reverts.NewFailedOp(outErr)
69100
if foErr != nil {
70-
return nil, fmt.Errorf("%s, %s", simErr, foErr)
101+
return nil, nil, fmt.Errorf("%s, %s", simErr, foErr)
71102
}
72-
return nil, errors.NewRPCError(errors.REJECTED_BY_EP_OR_ACCOUNT, fo.Reason, fo)
103+
return nil, nil, errors.NewRPCError(errors.REJECTED_BY_EP_OR_ACCOUNT, fo.Reason, fo)
73104
}
74105

75106
if len(res.Reverts) != 0 {
76107
data, err := hexutil.Decode(res.Reverts[len(res.Reverts)-1])
77108
if err != nil {
78-
return sim, err
109+
return sim, nil, err
79110
}
80111

81112
if len(data) == 0 {
82113
if res.ExecutionOOG {
83-
return sim, errors.NewRPCError(errors.EXECUTION_REVERTED, "execution OOG", nil)
114+
return sim, nil, errors.NewRPCError(errors.EXECUTION_REVERTED, "execution OOG", nil)
84115
}
85-
return sim, errors.NewRPCError(errors.EXECUTION_REVERTED, "execution reverted", nil)
116+
return sim, nil, errors.NewRPCError(errors.EXECUTION_REVERTED, "execution reverted", nil)
86117
}
87118

88119
reason, err := errors.DecodeRevert(data)
89120
if err != nil {
90-
return sim, err
121+
return sim, nil, err
91122
}
92-
return sim, errors.NewRPCError(errors.EXECUTION_REVERTED, reason, reason)
123+
return sim, nil, errors.NewRPCError(errors.EXECUTION_REVERTED, reason, reason)
124+
}
125+
126+
ev, err := parseUserOperationEvent(entryPoint, ep, res.UserOperationEvent)
127+
if err != nil {
128+
return nil, nil, err
93129
}
94-
return sim, nil
130+
return sim, ev, nil
95131
}

pkg/gas/estimate.go

Lines changed: 75 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"github.com/ethereum/go-ethereum/common/hexutil"
99
"github.com/ethereum/go-ethereum/rpc"
1010
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint/execution"
11-
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint/reverts"
1211
"github.com/stackup-wallet/stackup-bundler/pkg/errors"
1312
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
1413
)
@@ -25,38 +24,49 @@ func isExecutionOOG(err error) bool {
2524
return strings.Contains(err.Error(), "execution OOG")
2625
}
2726

28-
func runSimulations(rpc *rpc.Client,
27+
// EstimateGas uses the simulateHandleOp method on the EntryPoint to derive an estimate for
28+
// verificationGasLimit and callGasLimit.
29+
func EstimateGas(
30+
rpc *rpc.Client,
2931
from common.Address,
3032
op *userop.UserOperation,
33+
ov *Overhead,
3134
chainID *big.Int,
32-
tracer string) (*reverts.ExecutionResultRevert, error) {
35+
tracer string,
36+
) (verificationGas uint64, callGas uint64, err error) {
37+
// Skip if maxFeePerGas is zero.
38+
if op.MaxFeePerGas.Cmp(big.NewInt(0)) != 1 {
39+
return 0, 0, errors.NewRPCError(
40+
errors.INVALID_FIELDS,
41+
"maxFeePerGas must be more than 0",
42+
nil,
43+
)
44+
}
45+
46+
// Set the initial conditions.
3347
data, err := op.ToMap()
3448
if err != nil {
35-
return nil, err
49+
return 0, 0, err
3650
}
37-
38-
// Set MaxPriorityFeePerGas = MaxFeePerGas to simplify downstream calculations.
3951
data["maxPriorityFeePerGas"] = hexutil.EncodeBig(op.MaxFeePerGas)
40-
41-
// Setting default values for gas limits.
4252
data["verificationGasLimit"] = hexutil.EncodeBig(big.NewInt(0))
4353
data["callGasLimit"] = hexutil.EncodeBig(big.NewInt(0))
4454

45-
// Maintain an outer reference to the latest simulation error.
46-
var simErr error
47-
48-
// Find the maximal verificationGasLimit to simulate from.
55+
// Find the optimal verificationGasLimit with binary search. Setting gas price to 0 and maxing out the gas
56+
// limit here would result in certain code paths not being executed which results in an inaccurate gas
57+
// estimate.
4958
l := 0
5059
r := MaxGasLimit
60+
var simErr error
5161
for l <= r {
5262
m := (l + r) / 2
5363

5464
data["verificationGasLimit"] = hexutil.EncodeBig(big.NewInt(int64(m)))
5565
simOp, err := userop.New(data)
5666
if err != nil {
57-
return nil, err
67+
return 0, 0, err
5868
}
59-
sim, err := execution.TraceSimulateHandleOp(
69+
sim, _, err := execution.TraceSimulateHandleOp(
6070
rpc,
6171
from,
6272
simOp,
@@ -79,94 +89,72 @@ func runSimulations(rpc *rpc.Client,
7989
}
8090
// CGL is set to 0 and execution will always be OOG. Ignore it.
8191
if !isExecutionOOG(err) {
82-
return nil, err
92+
return 0, 0, err
8393
}
8494
}
8595

86-
data["verificationGasLimit"] = hexutil.EncodeBig(sim.PreOpGas)
96+
// Optimal VGL found.
97+
data["verificationGasLimit"] = hexutil.EncodeBig(
98+
big.NewInt(0).Sub(sim.PreOpGas, op.PreVerificationGas),
99+
)
87100
break
88101
}
89102
if simErr != nil && !isExecutionOOG(simErr) {
90-
return nil, simErr
103+
return 0, 0, simErr
91104
}
92105

93-
// Find the maximal callGasLimit to simulate from.
94-
l = 0
95-
r = MaxGasLimit
96-
var res *reverts.ExecutionResultRevert
97-
for l <= r {
98-
m := (l + r) / 2
99-
100-
data["callGasLimit"] = hexutil.EncodeBig(big.NewInt(int64(m)))
101-
simOp, err := userop.New(data)
102-
if err != nil {
103-
return nil, err
104-
}
105-
sim, err := execution.TraceSimulateHandleOp(
106-
rpc,
107-
from,
108-
simOp,
109-
chainID,
110-
tracer,
111-
common.Address{},
112-
[]byte{},
113-
)
114-
simErr = err
115-
if err != nil {
116-
if isPrefundNotPaid(err) {
117-
// CGL too high, go lower.
118-
r = m - 1
119-
continue
120-
}
121-
if isExecutionOOG(err) {
122-
// CGL too low, go higher.
123-
l = m + 1
124-
continue
125-
}
126-
return nil, err
127-
}
128-
129-
res = sim
130-
break
106+
// Find the optimal callGasLimit by setting the gas price to 0 and maxing out the gas limit. We don't run
107+
// into the same restrictions here as we do with verificationGasLimit.
108+
data["maxFeePerGas"] = hexutil.EncodeBig(big.NewInt(0))
109+
data["maxPriorityFeePerGas"] = hexutil.EncodeBig(big.NewInt(0))
110+
data["callGasLimit"] = hexutil.EncodeBig(big.NewInt(int64(MaxGasLimit)))
111+
simOp, err := userop.New(data)
112+
if err != nil {
113+
return 0, 0, err
131114
}
132-
if simErr != nil {
133-
return nil, simErr
115+
sim, ev, err := execution.TraceSimulateHandleOp(
116+
rpc,
117+
from,
118+
simOp,
119+
chainID,
120+
tracer,
121+
common.Address{},
122+
[]byte{},
123+
)
124+
if err != nil {
125+
return 0, 0, err
134126
}
135127

136-
return res, nil
137-
}
138-
139-
// EstimateGas uses the simulateHandleOp method on the EntryPoint to derive an estimate for
140-
// verificationGasLimit and callGasLimit.
141-
func EstimateGas(
142-
rpc *rpc.Client,
143-
from common.Address,
144-
op *userop.UserOperation,
145-
ov *Overhead,
146-
chainID *big.Int,
147-
tracer string,
148-
) (verificationGas uint64, callGas uint64, err error) {
149-
// Skip if maxFeePerGas is zero.
150-
if op.MaxFeePerGas.Cmp(big.NewInt(0)) != 1 {
151-
return 0, 0, errors.NewRPCError(
152-
errors.INVALID_FIELDS,
153-
"maxFeePerGas must be more than 0",
154-
nil,
155-
)
128+
// Calculate final values for verificationGasLimit and callGasLimit.
129+
vgl := simOp.VerificationGasLimit
130+
cgl := big.NewInt(0).
131+
Add(big.NewInt(0).Sub(ev.ActualGasUsed, sim.PreOpGas), big.NewInt(int64(ov.intrinsicFixed)))
132+
min := ov.NonZeroValueCall()
133+
if min.Cmp(cgl) >= 1 {
134+
cgl = min
156135
}
157136

158-
// Estimate gas limits using a binary search approach.
159-
sim, err := runSimulations(rpc, from, op, chainID, tracer)
137+
// Run a final simulation to check wether or not value transfers are still okay when factoring in the
138+
// expected gas cost.
139+
data["maxFeePerGas"] = hexutil.EncodeBig(op.MaxFeePerGas)
140+
data["maxPriorityFeePerGas"] = hexutil.EncodeBig(op.MaxFeePerGas)
141+
data["verificationGasLimit"] = hexutil.EncodeBig(vgl)
142+
data["callGasLimit"] = hexutil.EncodeBig(cgl)
143+
simOp, err = userop.New(data)
160144
if err != nil {
161145
return 0, 0, err
162146
}
163-
164-
// Return verificationGasLimit and callGasLimit.
165-
tg := big.NewInt(0).Div(sim.Paid, op.MaxFeePerGas)
166-
cgl := big.NewInt(0).Add(big.NewInt(0).Sub(tg, sim.PreOpGas), big.NewInt(int64(ov.intrinsicFixed)))
167-
min := ov.NonZeroValueCall()
168-
if cgl.Cmp(min) >= 1 {
169-
return sim.PreOpGas.Uint64(), cgl.Uint64(), nil
147+
_, _, err = execution.TraceSimulateHandleOp(
148+
rpc,
149+
from,
150+
simOp,
151+
chainID,
152+
tracer,
153+
common.Address{},
154+
[]byte{},
155+
)
156+
if err != nil {
157+
return 0, 0, err
170158
}
171-
return sim.PreOpGas.Uint64(), min.Uint64(), nil
159+
return vgl.Uint64(), cgl.Uint64(), nil
172160
}

0 commit comments

Comments
 (0)