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

Commit 751f3e8

Browse files
authored
Add check to validate pending UserOps (#74)
1 parent f37296c commit 751f3e8

File tree

4 files changed

+187
-33
lines changed

4 files changed

+187
-33
lines changed

pkg/client/client.go

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -80,39 +80,6 @@ func (i *Client) SendUserOperation(op map[string]any, ep string) (string, error)
8080
hash := userOp.GetUserOpHash(epAddr, i.chainID)
8181
l = l.WithValues("userop_hash", hash)
8282

83-
// Check mempool for duplicates and only replace under the following circumstances:
84-
//
85-
// 1. the nonce remains the same
86-
// 2. the new maxPriorityFeePerGas is higher
87-
// 3. the new maxFeePerGas is increased equally
88-
// memOp, err := i.mempool.GetOp(epAddr, userOp.Sender)
89-
// if err != nil {
90-
// l.Error(err, "eth_sendUserOperation error")
91-
// return "", err
92-
// }
93-
// if memOp != nil {
94-
// if memOp.Nonce.Cmp(memOp.Nonce) != 0 {
95-
// err := errors.New("sender: Has userOp in mempool with a different nonce")
96-
// l.Error(err, "eth_sendUserOperation error")
97-
// return "", err
98-
// }
99-
100-
// if memOp.MaxPriorityFeePerGas.Cmp(memOp.MaxPriorityFeePerGas) <= 0 {
101-
// err := errors.New("sender: Has userOp in mempool with same or higher priority fee")
102-
// l.Error(err, "eth_sendUserOperation error")
103-
// return "", err
104-
// }
105-
106-
// diff := big.NewInt(0)
107-
// mf := big.NewInt(0)
108-
// diff.Sub(memOp.MaxPriorityFeePerGas, memOp.MaxPriorityFeePerGas)
109-
// if memOp.MaxFeePerGas.Cmp(mf.Add(memOp.MaxFeePerGas, diff)) != 0 {
110-
// err := errors.New("sender: Replaced userOp must have an equally higher max fee")
111-
// l.Error(err, "eth_sendUserOperation error")
112-
// return "", err
113-
// }
114-
// }
115-
11683
// Fetch any pending UserOperations in the mempool by the same sender
11784
penOps, err := i.mempool.GetOps(epAddr, userOp.Sender)
11885
if err != nil {

pkg/modules/checks/pendingops.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package checks
2+
3+
import (
4+
"errors"
5+
"math/big"
6+
7+
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
8+
)
9+
10+
// ValidatePendingOps checks the pending UserOperations by the same sender and only passes if:
11+
//
12+
// 1. Sender doesn't have another UserOperation already present in the pool.
13+
// 2. It replaces an existing UserOperation with same nonce and higher fee.
14+
// 3. Sender is staked and is allowed multiple UserOperations in the pool.
15+
func ValidatePendingOps(op *userop.UserOperation, penOps []*userop.UserOperation, gs GetStakeFunc) error {
16+
dep, err := gs(op.Sender)
17+
if err != nil {
18+
return err
19+
}
20+
21+
if len(penOps) > 0 {
22+
var oldOp *userop.UserOperation
23+
for _, penOp := range penOps {
24+
if op.Nonce.Cmp(penOp.Nonce) == 0 {
25+
oldOp = penOp
26+
}
27+
}
28+
29+
if oldOp != nil {
30+
if op.MaxPriorityFeePerGas.Cmp(oldOp.MaxPriorityFeePerGas) <= 0 {
31+
return errors.New(
32+
"pending ops: sender has op in mempool with same or higher priority fee",
33+
)
34+
}
35+
36+
diff := big.NewInt(0).Sub(op.MaxPriorityFeePerGas, oldOp.MaxPriorityFeePerGas)
37+
mf := big.NewInt(0).Add(oldOp.MaxFeePerGas, diff)
38+
if op.MaxFeePerGas.Cmp(mf) != 0 {
39+
return errors.New("pending ops: replaced op must have an equally higher max fee")
40+
}
41+
} else if !dep.Staked {
42+
return errors.New(
43+
"pending ops: sender must be staked to have multiple ops in the mempool",
44+
)
45+
}
46+
}
47+
return nil
48+
}

pkg/modules/checks/pendingops_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package checks
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
7+
"github.com/ethereum/go-ethereum/common"
8+
"github.com/stackup-wallet/stackup-bundler/internal/testutils"
9+
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
10+
)
11+
12+
// TestNoPendingOps calls checks.ValidatePendingOps with no pending UserOperations. Expect nil.
13+
func TestNoPendingOps(t *testing.T) {
14+
penOps := []*userop.UserOperation{}
15+
op := testutils.MockValidInitUserOp()
16+
err := ValidatePendingOps(op, penOps, testutils.MockGetNotStakeZeroDeposit)
17+
18+
if err != nil {
19+
t.Fatalf("got err %v, want nil", err)
20+
}
21+
}
22+
23+
// TestPendingOpsNotStaked calls checks.ValidatePendingOps with pending UserOperations but sender is not
24+
// staked. Expect error.
25+
func TestPendingOpsNotStaked(t *testing.T) {
26+
penOp := testutils.MockValidInitUserOp()
27+
penOps := []*userop.UserOperation{penOp}
28+
op := testutils.MockValidInitUserOp()
29+
op.Nonce = big.NewInt(0).Add(penOp.Nonce, common.Big1)
30+
err := ValidatePendingOps(op, penOps, testutils.MockGetNotStakeZeroDeposit)
31+
32+
if err == nil {
33+
t.Fatal("got nil, want err")
34+
}
35+
}
36+
37+
// TestPendingOpsStaked calls checks.ValidatePendingOps with pending UserOperations but sender is staked.
38+
// Expect nil.
39+
func TestPendingOpsStaked(t *testing.T) {
40+
penOp := testutils.MockValidInitUserOp()
41+
penOps := []*userop.UserOperation{penOp}
42+
op := testutils.MockValidInitUserOp()
43+
op.Nonce = big.NewInt(0).Add(penOp.Nonce, common.Big1)
44+
err := ValidatePendingOps(op, penOps, testutils.MockGetStakeZeroDeposit)
45+
46+
if err != nil {
47+
t.Fatalf("got err %v, want nil", err)
48+
}
49+
}
50+
51+
// TestReplaceOp calls checks.ValidatePendingOps with a valid UserOperation that replaces a pending
52+
// UserOperation. Expect nil.
53+
func TestReplaceOp(t *testing.T) {
54+
penOp := testutils.MockValidInitUserOp()
55+
penOps := []*userop.UserOperation{penOp}
56+
op := testutils.MockValidInitUserOp()
57+
op.MaxPriorityFeePerGas = big.NewInt(0).Add(penOp.MaxPriorityFeePerGas, common.Big1)
58+
op.MaxFeePerGas = big.NewInt(0).Add(penOp.MaxFeePerGas, common.Big1)
59+
err := ValidatePendingOps(op, penOps, testutils.MockGetNotStakeZeroDeposit)
60+
61+
if err != nil {
62+
t.Fatalf("got err %v, want nil", err)
63+
}
64+
}
65+
66+
// TestReplaceOpLowerMPF calls checks.ValidatePendingOps with a UserOperation that replaces a pending
67+
// UserOperation but has a lower MaxPriorityFeePerGas. Expect error.
68+
func TestReplaceOpLowerMPF(t *testing.T) {
69+
penOp := testutils.MockValidInitUserOp()
70+
penOps := []*userop.UserOperation{penOp}
71+
op := testutils.MockValidInitUserOp()
72+
op.MaxPriorityFeePerGas = big.NewInt(0).Sub(penOp.MaxPriorityFeePerGas, common.Big1)
73+
err := ValidatePendingOps(op, penOps, testutils.MockGetNotStakeZeroDeposit)
74+
75+
if err == nil {
76+
t.Fatal("got nil, want err")
77+
}
78+
}
79+
80+
// TestReplaceOpEqualMPF calls checks.ValidatePendingOps with a UserOperation that replaces a pending
81+
// UserOperation but has an equal MaxPriorityFeePerGas. Expect error.
82+
func TestReplaceOpEqualMPF(t *testing.T) {
83+
penOp := testutils.MockValidInitUserOp()
84+
penOps := []*userop.UserOperation{penOp}
85+
op := testutils.MockValidInitUserOp()
86+
op.MaxPriorityFeePerGas = big.NewInt(0).Add(penOp.MaxPriorityFeePerGas, common.Big0)
87+
err := ValidatePendingOps(op, penOps, testutils.MockGetNotStakeZeroDeposit)
88+
89+
if err == nil {
90+
t.Fatal("got nil, want err")
91+
}
92+
}
93+
94+
// TestReplaceOpNotEqualIncMF calls checks.ValidatePendingOps with a UserOperation that replaces a pending
95+
// UserOperation but does not have an equally increasing MaxFeePerGas. Expect error.
96+
func TestReplaceOpNotEqualIncMF(t *testing.T) {
97+
penOp := testutils.MockValidInitUserOp()
98+
penOps := []*userop.UserOperation{penOp}
99+
op := testutils.MockValidInitUserOp()
100+
op.MaxPriorityFeePerGas = big.NewInt(0).Add(penOp.MaxPriorityFeePerGas, common.Big2)
101+
op.MaxFeePerGas = big.NewInt(0).Add(penOp.MaxFeePerGas, common.Big1)
102+
err := ValidatePendingOps(op, penOps, testutils.MockGetNotStakeZeroDeposit)
103+
104+
if err == nil {
105+
t.Fatal("got nil, want err")
106+
}
107+
}
108+
109+
// TestReplaceOpSameMF calls checks.ValidatePendingOps with a UserOperation that replaces a pending
110+
// UserOperation but does not increase MaxFeePerGas. Expect error.
111+
func TestReplaceOpSameMF(t *testing.T) {
112+
penOp := testutils.MockValidInitUserOp()
113+
penOps := []*userop.UserOperation{penOp}
114+
op := testutils.MockValidInitUserOp()
115+
op.MaxPriorityFeePerGas = big.NewInt(0).Add(penOp.MaxPriorityFeePerGas, common.Big1)
116+
op.MaxFeePerGas = big.NewInt(0).Add(penOp.MaxFeePerGas, common.Big0)
117+
err := ValidatePendingOps(op, penOps, testutils.MockGetNotStakeZeroDeposit)
118+
119+
if err == nil {
120+
t.Fatal("got nil, want err")
121+
}
122+
}
123+
124+
// TestReplaceOpDecMF calls checks.ValidatePendingOps with a UserOperation that replaces a pending
125+
// UserOperation but has a decreasing MaxFeePerGas. Expect error.
126+
func TestReplaceOpDecMF(t *testing.T) {
127+
penOp := testutils.MockValidInitUserOp()
128+
penOps := []*userop.UserOperation{penOp}
129+
op := testutils.MockValidInitUserOp()
130+
op.MaxPriorityFeePerGas = big.NewInt(0).Add(penOp.MaxPriorityFeePerGas, common.Big1)
131+
op.MaxFeePerGas = big.NewInt(0).Sub(penOp.MaxFeePerGas, common.Big1)
132+
err := ValidatePendingOps(op, penOps, testutils.MockGetNotStakeZeroDeposit)
133+
134+
if err == nil {
135+
t.Fatal("got nil, want err")
136+
}
137+
}

pkg/modules/checks/standalone.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ 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+
penOps := ctx.GetPendingOps()
3738
gc := getCodeWithEthClient(s.eth)
3839
gt := getGasTipWithEthClient(s.eth)
3940
gs, err := getStakeWithEthClient(ctx, s.eth)
@@ -48,6 +49,7 @@ func (s *Standalone) ValidateOpValues() modules.UserOpHandlerFunc {
4849
g.Go(func() error { return ValidatePaymasterAndData(ctx.UserOp, gc, gs) })
4950
g.Go(func() error { return ValidateCallGasLimit(ctx.UserOp) })
5051
g.Go(func() error { return ValidateFeePerGas(ctx.UserOp, gt) })
52+
g.Go(func() error { return ValidatePendingOps(ctx.UserOp, penOps, gs) })
5153

5254
return g.Wait()
5355
}

0 commit comments

Comments
 (0)