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

Commit 51cc55e

Browse files
authored
Account for callData reverts due to transfers greater than sender balance (#384)
1 parent 5c8b5b8 commit 51cc55e

5 files changed

Lines changed: 163 additions & 13 deletions

File tree

e2e/test/execution.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,38 @@ describe("During the execution phase", () => {
9595
expect(event?.args.success).toBe(true);
9696
});
9797

98+
describe("With transfer amounts more than sender balance", () => {
99+
test("Sender cannot create a UserOperation by default", async () => {
100+
expect.assertions(1);
101+
try {
102+
const balance = await provider.getBalance(acc.getSender());
103+
const response = await client.sendUserOperation(
104+
acc.execute(acc.getSender(), balance.mul(2), "0x")
105+
);
106+
await response.wait();
107+
} catch (error: any) {
108+
expect(error?.error.code).toBe(errorCodes.executionReverted);
109+
}
110+
});
111+
112+
test("Sender can cause a failed UserOperation with state overrides", async () => {
113+
const balance = await provider.getBalance(acc.getSender());
114+
const response = await client.sendUserOperation(
115+
acc.execute(acc.getSender(), balance.mul(2), "0x"),
116+
{
117+
stateOverrides: {
118+
[acc.getSender()]: {
119+
balance: balance.mul(3).toHexString(),
120+
},
121+
},
122+
}
123+
);
124+
const event = await response.wait();
125+
126+
expect(event?.args.success).toBe(false);
127+
});
128+
});
129+
98130
test("Sender can transfer max balance of ERC20 token", async () => {
99131
const balance = await testToken.balanceOf(acc.getSender());
100132
const response = await client.sendUserOperation(

pkg/client/client.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -194,17 +194,12 @@ func (i *Client) EstimateUserOperationGas(
194194
hash := userOp.GetUserOpHash(epAddr, i.chainID)
195195
l = l.WithValues("userop_hash", hash)
196196

197-
// Parse state override set. If paymaster is not included and sender overrides are not set, default to
198-
// overriding sender balance to max uint96. This ensures gas estimation is not blocked by insufficient
199-
// funds.
197+
// Parse state override set.
200198
sos, err := state.ParseOverrideData(os)
201199
if err != nil {
202200
l.Error(err, "eth_estimateUserOperationGas error")
203201
return nil, err
204202
}
205-
if userOp.GetPaymaster() == common.HexToAddress("0x") {
206-
sos = state.WithMaxBalanceOverride(userOp.Sender, sos)
207-
}
208203

209204
// Override op with suggested gas prices if maxFeePerGas is 0. This allows for more reliable gas
210205
// estimations upstream. The default balance override also ensures simulations won't revert on

pkg/gas/estimate.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@ func EstimateGas(in *EstimateInput) (verificationGas uint64, callGas uint64, err
8989
data["verificationGasLimit"] = hexutil.EncodeBig(big.NewInt(0))
9090
data["callGasLimit"] = hexutil.EncodeBig(big.NewInt(0))
9191

92+
// If paymaster is not included and sender overrides are not set, default to overriding sender balance to
93+
// max uint96. This ensures gas estimation is not blocked by insufficient funds.
94+
sosCpy, err := state.Copy(in.Sos)
95+
if err != nil {
96+
return 0, 0, err
97+
}
98+
if in.Op.GetPaymaster() == common.HexToAddress("0x") {
99+
sosCpy = state.WithMaxBalanceOverride(in.Op.Sender, sosCpy)
100+
}
101+
92102
// Find the optimal verificationGasLimit with binary search. A gas price of 0 may result in certain
93103
// upstream code paths in the EVM to not be executed which can affect the reliability of gas estimates. In
94104
// this case, consider calling the EstimateGas function after setting the gas price on the UserOperation.
@@ -108,7 +118,7 @@ func EstimateGas(in *EstimateInput) (verificationGas uint64, callGas uint64, err
108118
Rpc: in.Rpc,
109119
EntryPoint: in.EntryPoint,
110120
Op: simOp,
111-
Sos: in.Sos,
121+
Sos: sosCpy,
112122
ChainID: in.ChainID,
113123
})
114124
simErr = err
@@ -136,8 +146,9 @@ func EstimateGas(in *EstimateInput) (verificationGas uint64, callGas uint64, err
136146
f = (f * (100 + baseVGLBuffer)) / 100
137147
data["verificationGasLimit"] = hexutil.EncodeBig(big.NewInt(int64(f)))
138148

139-
// Find the optimal callGasLimit by setting the gas price to 0 and maxing out the gas limit. We don't run
140-
// into the same restrictions here as we do with verificationGasLimit.
149+
// Find the optimal callGasLimit by setting the gas price to 0 and maxing out the gas limit. We use the
150+
// original state override to account for insufficient balance reverts unless the caller has explicitly
151+
// overridden the balance too.
141152
data["maxFeePerGas"] = hexutil.EncodeBig(big.NewInt(0))
142153
data["maxPriorityFeePerGas"] = hexutil.EncodeBig(big.NewInt(0))
143154
data["callGasLimit"] = hexutil.EncodeBig(in.MaxGasLimit)
@@ -165,8 +176,7 @@ func EstimateGas(in *EstimateInput) (verificationGas uint64, callGas uint64, err
165176
cgl = in.Ov.NonZeroValueCall()
166177
}
167178

168-
// Run a final simulation to check wether or not value transfers are still okay when factoring in the
169-
// expected gas cost.
179+
// Run a final simulation using actual gas fees and to confirm one-shot tracing is ok.
170180
data["maxFeePerGas"] = hexutil.EncodeBig(in.Op.MaxFeePerGas)
171181
data["maxPriorityFeePerGas"] = hexutil.EncodeBig(in.Op.MaxFeePerGas)
172182
data["verificationGasLimit"] = hexutil.EncodeBig(vgl)
@@ -179,7 +189,7 @@ func EstimateGas(in *EstimateInput) (verificationGas uint64, callGas uint64, err
179189
Rpc: in.Rpc,
180190
EntryPoint: in.EntryPoint,
181191
Op: simOp,
182-
Sos: in.Sos,
192+
Sos: sosCpy,
183193
ChainID: in.ChainID,
184194
Tracer: in.Tracer,
185195
})
@@ -204,7 +214,7 @@ func EstimateGas(in *EstimateInput) (verificationGas uint64, callGas uint64, err
204214
Rpc: in.Rpc,
205215
EntryPoint: in.EntryPoint,
206216
Op: simOp,
207-
Sos: in.Sos,
217+
Sos: sosCpy,
208218
ChainID: in.ChainID,
209219
Tracer: in.Tracer,
210220
})

pkg/state/copy.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package state
2+
3+
import (
4+
"encoding/json"
5+
)
6+
7+
// Copy creates a deep copy of a given OverrideSet.
8+
func Copy(os OverrideSet) (OverrideSet, error) {
9+
cpy := OverrideSet{}
10+
for k, v := range os {
11+
b, err := json.Marshal(v)
12+
if err != nil {
13+
return nil, err
14+
}
15+
16+
oa := OverrideAccount{}
17+
err = json.Unmarshal(b, &oa)
18+
if err != nil {
19+
return nil, err
20+
}
21+
22+
cpy[k] = oa
23+
}
24+
25+
return cpy, nil
26+
}

pkg/state/copy_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package state
2+
3+
import (
4+
"testing"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
)
8+
9+
func TestCopyNil(t *testing.T) {
10+
if os, err := Copy(nil); err != nil {
11+
t.Fatalf("got %v, want nil", err)
12+
} else if os == nil {
13+
t.Fatal("got nil return value")
14+
} else if len(os) != 0 {
15+
t.Fatalf("got length %v, want 0", len(os))
16+
}
17+
}
18+
19+
func TestCopyNewRef(t *testing.T) {
20+
acc := common.HexToAddress("0x0000000000000000000000000000000000000000")
21+
stateOvKey := common.HexToHash("0xdead")
22+
stateOvVal := common.HexToHash("0xbeef")
23+
data := map[string]any{
24+
acc.Hex(): map[string]any{
25+
"nonce": "0x1",
26+
"code": "0x01",
27+
"balance": "0x1",
28+
"state": map[string]any{
29+
stateOvKey.String(): stateOvVal.String(),
30+
},
31+
"stateDiff": map[string]any{
32+
stateOvKey.String(): stateOvVal.String(),
33+
},
34+
},
35+
}
36+
if os, err := ParseOverrideData(data); err != nil {
37+
t.Fatalf("got %v, want nil", err)
38+
} else if osCpy, cpyErr := Copy(os); err != nil {
39+
t.Fatalf("got %v, want nil", cpyErr)
40+
} else if os[acc].Nonce == osCpy[acc].Nonce ||
41+
os[acc].Code == osCpy[acc].Code ||
42+
os[acc].Balance == osCpy[acc].Balance ||
43+
os[acc].State == osCpy[acc].State ||
44+
os[acc].StateDiff == osCpy[acc].StateDiff {
45+
t.Fatal("OverrideAccount contains identical references")
46+
}
47+
}
48+
49+
func TestCopySameVal(t *testing.T) {
50+
acc := common.HexToAddress("0x0000000000000000000000000000000000000000")
51+
stateOvKey := common.HexToHash("0xdead")
52+
stateOvVal := common.HexToHash("0xbeef")
53+
data := map[string]any{
54+
acc.Hex(): map[string]any{
55+
"nonce": "0x1",
56+
"code": "0x01",
57+
"balance": "0x1",
58+
"state": map[string]any{
59+
stateOvKey.String(): stateOvVal.String(),
60+
},
61+
"stateDiff": map[string]any{
62+
stateOvKey.String(): stateOvVal.String(),
63+
},
64+
},
65+
}
66+
os, err := ParseOverrideData(data)
67+
if err != nil {
68+
t.Fatalf("got %v, want nil", err)
69+
}
70+
71+
osCpy, cpyErr := Copy(os)
72+
if cpyErr != nil {
73+
t.Fatalf("got %v, want nil", cpyErr)
74+
}
75+
76+
state := *os[acc].State
77+
stateCpy := *osCpy[acc].State
78+
stateDiff := *os[acc].StateDiff
79+
stateDiffCpy := *osCpy[acc].StateDiff
80+
if os[acc].Nonce.String() != osCpy[acc].Nonce.String() ||
81+
os[acc].Code.String() != osCpy[acc].Code.String() ||
82+
os[acc].Balance.String() != osCpy[acc].Balance.String() ||
83+
state[stateOvKey] != stateCpy[stateOvKey] ||
84+
stateDiff[stateOvKey] != stateDiffCpy[stateOvKey] {
85+
t.Fatal("OverrideAccount contains different values")
86+
}
87+
}

0 commit comments

Comments
 (0)