66 mockToSmartAccount,
77 mockGetPaymasterData,
88 mockGetPaymasterStubData,
9+ mockPrepareUserOperation,
910} = vi . hoisted ( ( ) => ( {
1011 mockCreateSmartAccountClient : vi . fn ( ) . mockReturnValue ( {
1112 sendTransaction : vi . fn ( ) ,
@@ -19,6 +20,7 @@ const {
1920 } ) ,
2021 mockGetPaymasterData : vi . fn ( ) . mockResolvedValue ( { paymaster : '0xPaymaster' , paymasterData : '0xdata' } ) ,
2122 mockGetPaymasterStubData : vi . fn ( ) . mockResolvedValue ( { paymaster : '0xPaymaster' , paymasterData : '0xstubdata' } ) ,
23+ mockPrepareUserOperation : vi . fn ( ) ,
2224} ) ) ;
2325
2426vi . mock ( 'permissionless' , ( ) => ( {
@@ -30,6 +32,7 @@ vi.mock('viem/account-abstraction', () => ({
3032 entryPoint07Abi : [ ] ,
3133 entryPoint07Address : '0x0000000071727De22E5E9d8BAf0edAc6f37da032' ,
3234 getUserOperationHash : vi . fn ( ) . mockReturnValue ( '0x' + '00' . repeat ( 32 ) ) ,
35+ prepareUserOperation : mockPrepareUserOperation ,
3336} ) ) ;
3437
3538vi . mock ( 'viem' , async ( importOriginal ) => {
@@ -53,7 +56,7 @@ vi.mock('@azeth/common/abis', () => ({
5356 AzethAccountAbi : [ ] ,
5457} ) ) ;
5558
56- import { createAzethSmartAccountClient } from '../../src/utils/userop.js' ;
59+ import { createAzethSmartAccountClient , applyVerificationGasBuffer } from '../../src/utils/userop.js' ;
5760import type { PublicClient , WalletClient , Transport , Chain , Account } from 'viem' ;
5861
5962function mockPublicClient ( overrides : Record < string , unknown > = { } ) : PublicClient < Transport , Chain > {
@@ -212,4 +215,63 @@ describe('createAzethSmartAccountClient', () => {
212215 } ) ,
213216 ) . rejects . toThrow ( 'bundlerUrl is required' ) ;
214217 } ) ;
218+
219+ // F-2: native value-spends deterministically AA26'd because the SDK took the
220+ // bundler's verificationGasLimit estimate verbatim, leaving no headroom for
221+ // GuardianModule's state-dependent validation gas (cold daily-spend SSTORE,
222+ // epoch reset). The client must install a prepareUserOperation hook that
223+ // buffers verificationGasLimit for EVERY UserOp.
224+ it ( 'installs a prepareUserOperation hook that buffers verificationGasLimit (F-2)' , async ( ) => {
225+ mockPrepareUserOperation . mockResolvedValue ( {
226+ sender : TEST_SMART_ACCOUNT ,
227+ nonce : 1n ,
228+ callData : '0x' ,
229+ callGasLimit : 50_000n ,
230+ verificationGasLimit : 100_000n ,
231+ preVerificationGas : 40_000n ,
232+ maxFeePerGas : 1n ,
233+ maxPriorityFeePerGas : 1n ,
234+ signature : '0x' ,
235+ } ) ;
236+
237+ await createAzethSmartAccountClient ( {
238+ publicClient : mockPublicClient ( ) ,
239+ walletClient : mockWalletClient ( ) ,
240+ smartAccountAddress : TEST_SMART_ACCOUNT ,
241+ bundlerUrl : TEST_BUNDLER_URL ,
242+ } ) ;
243+
244+ const callArgs = mockCreateSmartAccountClient . mock . calls [ 0 ] [ 0 ] ;
245+ expect ( callArgs . userOperation ) . toBeDefined ( ) ;
246+ expect ( callArgs . userOperation . prepareUserOperation ) . toBeTypeOf ( 'function' ) ;
247+
248+ // Invoke the installed hook: it must delegate to viem's prepareUserOperation
249+ // and return the SAME op with only verificationGasLimit scaled up 1.5x.
250+ const fakeClient = { } as never ;
251+ const fakeParams = { } as never ;
252+ const prepared = await callArgs . userOperation . prepareUserOperation ( fakeClient , fakeParams ) ;
253+
254+ expect ( mockPrepareUserOperation ) . toHaveBeenCalledWith ( fakeClient , fakeParams ) ;
255+ expect ( prepared . verificationGasLimit ) . toBe ( 150_000n ) ; // 100k * 3/2
256+ // Every other field passes through untouched.
257+ expect ( prepared . callGasLimit ) . toBe ( 50_000n ) ;
258+ expect ( prepared . preVerificationGas ) . toBe ( 40_000n ) ;
259+ expect ( prepared . nonce ) . toBe ( 1n ) ;
260+ } ) ;
261+ } ) ;
262+
263+ describe ( 'applyVerificationGasBuffer' , ( ) => {
264+ it ( 'scales the estimate by 1.5x (the AA26 fix for F-2)' , ( ) => {
265+ // The exact verificationGasLimit (101136) the bundler returned for the
266+ // transfer that deterministically reverted with AA26 in the MCP test.
267+ expect ( applyVerificationGasBuffer ( 101_136n ) ) . toBe ( 151_704n ) ;
268+ expect ( applyVerificationGasBuffer ( 100_000n ) ) . toBe ( 150_000n ) ;
269+ expect ( applyVerificationGasBuffer ( 0n ) ) . toBe ( 0n ) ;
270+ } ) ;
271+
272+ it ( 'always returns at least the input (headroom is never negative)' , ( ) => {
273+ for ( const estimate of [ 1n , 12_345n , 80_000n , 101_136n , 500_000n ] ) {
274+ expect ( applyVerificationGasBuffer ( estimate ) ) . toBeGreaterThanOrEqual ( estimate ) ;
275+ }
276+ } ) ;
215277} ) ;
0 commit comments