diff --git a/.gitignore b/.gitignore index 981cd2b1d5..2d53786ef2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ temp coverage.json *.tsbuildinfo +integration-tests/result/output.xml + env.yml env.yaml .aws-sam/ diff --git a/integration-tests/test/eth-l2/boba_aa_sponsoring_fee.spec.ts b/integration-tests/test/eth-l2/boba_aa_sponsoring_fee.spec.ts index 6f375fecff..a0a5fe26cc 100644 --- a/integration-tests/test/eth-l2/boba_aa_sponsoring_fee.spec.ts +++ b/integration-tests/test/eth-l2/boba_aa_sponsoring_fee.spec.ts @@ -171,5 +171,59 @@ describe('Sponsoring Tx\n', async () => { expect(postUserBalance).to.eq(preUserBalance) expect(postPaymasterDeposit).to.eq(prePaymasterDeposit.sub(logEP.args.actualGasCost)) }) + + it('should not be able to submit a userOp to the bundler and trigger tx when signature expired', async () => { + const validUntil = (await env.l2Provider.getBlock('latest')).timestamp - 300 + const validAfter = (await env.l2Provider.getBlock('latest')).timestamp - 600 + const op = await accountAPI.createSignedUserOp({ + target: recipient.address, + data: recipient.interface.encodeFunctionData('something', ['hello']), + }) + + // add preverificaiton gas to account for paymaster signature + op.paymasterAndData = hexConcat([VerifyingPaymaster.address, defaultAbiCoder.encode(['uint48', 'uint48'], [validUntil, validAfter]), '0x' + '00'.repeat(65)]) + op.preVerificationGas = BigNumber.from(await op.preVerificationGas).add(3000) + const hash = await VerifyingPaymaster.getHash(op, validUntil, validAfter) + const sig = await offchainSigner.signMessage(utils.arrayify(hash)) + + op.paymasterAndData = hexConcat([VerifyingPaymaster.address, defaultAbiCoder.encode(['uint48', 'uint48'], [validUntil, validAfter]), sig]) + const res = await VerifyingPaymaster.parsePaymasterAndData(op.paymasterAndData) + + expect(res.signature).to.eq(sig) + expect(res.validAfter).to.eq(validAfter) + expect(res.validUntil).to.eq(validUntil) + signedOp = await accountAPI.signUserOp(op) + + await expect(bundlerProvider.sendUserOpToBundler(signedOp)).to.be.rejectedWith( + Error, /expires too soon/ + ) + }) + + it('should not be able to submit a userOp to the bundler and trigger tx when signature is not valid yet', async () => { + const validUntil = (await env.l2Provider.getBlock('latest')).timestamp + 800 + const validAfter = (await env.l2Provider.getBlock('latest')).timestamp + 600 + const op = await accountAPI.createSignedUserOp({ + target: recipient.address, + data: recipient.interface.encodeFunctionData('something', ['hello']), + }) + + // add preverificaiton gas to account for paymaster signature + op.paymasterAndData = hexConcat([VerifyingPaymaster.address, defaultAbiCoder.encode(['uint48', 'uint48'], [validUntil, validAfter]), '0x' + '00'.repeat(65)]) + op.preVerificationGas = BigNumber.from(await op.preVerificationGas).add(3000) + const hash = await VerifyingPaymaster.getHash(op, validUntil, validAfter) + const sig = await offchainSigner.signMessage(utils.arrayify(hash)) + + op.paymasterAndData = hexConcat([VerifyingPaymaster.address, defaultAbiCoder.encode(['uint48', 'uint48'], [validUntil, validAfter]), sig]) + const res = await VerifyingPaymaster.parsePaymasterAndData(op.paymasterAndData) + + expect(res.signature).to.eq(sig) + expect(res.validAfter).to.eq(validAfter) + expect(res.validUntil).to.eq(validUntil) + signedOp = await accountAPI.signUserOp(op) + + await expect(bundlerProvider.sendUserOpToBundler(signedOp)).to.be.rejectedWith( + Error, /not valid yet/ + ) + }) }) }) diff --git a/packages/boba/bundler/src/modules/Types.ts b/packages/boba/bundler/src/modules/Types.ts index d97c4d8340..e71f0f7fda 100644 --- a/packages/boba/bundler/src/modules/Types.ts +++ b/packages/boba/bundler/src/modules/Types.ts @@ -12,6 +12,7 @@ export enum ValidationErrors { InsufficientStake = -32505, UnsupportedSignatureAggregator = -32506, InvalidSignature = -32507, + NotValidYet = -32508, } export enum ExecutionErrors { diff --git a/packages/boba/bundler/src/modules/ValidationManager.ts b/packages/boba/bundler/src/modules/ValidationManager.ts index 79706e394e..a154fc90af 100644 --- a/packages/boba/bundler/src/modules/ValidationManager.ts +++ b/packages/boba/bundler/src/modules/ValidationManager.ts @@ -162,17 +162,20 @@ export class ValidationManager { 'Invalid UserOp signature or paymaster signature', ValidationErrors.InvalidSignature ) -//TODO -// requireCond( -// res.returnInfo.validUntil == null || -// res.returnInfo.validUntil + 30 < Date.now() / 1000, -// 'expires too soon', -// ValidationErrors.ExpiresShortly -// ) -console.log(`res.aggregatorInfo ${res.aggregatorInfo}`) -console.log(res.aggregatorInfo.addr) -console.log(res.aggregatorInfo.stake) -console.log(res.aggregatorInfo.unstakeDelaySec) + requireCond( + res.returnInfo.validUntil == null || + res.returnInfo.validUntil > (Date.now() / 1000) + 30, + 'expires too soon', + ValidationErrors.ExpiresShortly, + ) + requireCond( + res.returnInfo.validAfter == null || + // not adding "buffer" here + res.returnInfo.validAfter < Date.now() / 1000, + 'not valid yet', + ValidationErrors.NotValidYet, + ) + if ( res.aggregatorInfo.addr !== AddressZero && !BigNumber.from(0).eq(res.aggregatorInfo.stake) && diff --git a/packages/boba/bundler/test/ValidateManager.test.ts b/packages/boba/bundler/test/ValidateManager.test.ts index 142938d2d8..0a38211834 100644 --- a/packages/boba/bundler/test/ValidateManager.test.ts +++ b/packages/boba/bundler/test/ValidateManager.test.ts @@ -25,6 +25,7 @@ import { isGeth } from '../src/utils' import { TestRecursionAccount__factory } from '../dist/src/types/factories/contracts/tests/TestRecursionAccount__factory' // import { resolveNames } from './testUtils' import { UserOperation } from '../src/modules/Types' +import { BytesLike } from "ethers"; const cEmptyUserOp: UserOperation = { sender: AddressZero, @@ -51,9 +52,9 @@ describe('#ValidationManager', () => { let rulesAccount: TestRulesAccount let storageAccount: TestStorageAccount - async function testUserOp (validateRule: string = '', pmRule?: string, initFunc?: string, factoryAddress = opcodeFactory.address): Promise { - const userOp = await createTestUserOp(validateRule, pmRule, initFunc, factoryAddress) - return { userOp, ...await vm.validateUserOp(userOp) } + async function testUserOp (validateRule: string = '', pmRule?: string, initFunc?: string, factoryAddress = opcodeFactory.address, paymasterData?: BytesLike[]): Promise { + const userOp = await createTestUserOp(validateRule, pmRule, initFunc, factoryAddress, paymasterData) + return { userOp, ...await vm.validateUserOp(userOp) } } async function testExistingUserOp (validateRule: string = '', pmRule = ''): Promise { @@ -75,7 +76,7 @@ describe('#ValidationManager', () => { } } - async function createTestUserOp (validateRule: string = '', pmRule?: string, initFunc?: string, factoryAddress = opcodeFactory.address): Promise { + async function createTestUserOp (validateRule: string = '', pmRule?: string, initFunc?: string, factoryAddress = opcodeFactory.address, paymasterData?: BytesLike[]): Promise { if (initFunc === undefined) { initFunc = opcodeFactory.interface.encodeFunctionData('create', ['']) } @@ -84,7 +85,11 @@ describe('#ValidationManager', () => { factoryAddress, initFunc ]) - const paymasterAndData = pmRule == null ? '0x' : hexConcat([paymaster.address, Buffer.from(pmRule)]) + + if (!paymasterData || !paymasterData.length) { + paymasterData.push(Buffer.from(pmRule)) + } + const paymasterAndData = pmRule == null ? '0x' : hexConcat([paymaster.address, ...paymasterData ]) const signature = hexlify(Buffer.from(validateRule)) const callinitCodeForAddr = await provider.call({ to: factoryAddress, @@ -184,6 +189,24 @@ describe('#ValidationManager', () => { .catch(e => e) ).to.match(/unstaked account accessed/) }) + it('should fail with expired signature in validation', async () => { + const currBlockTimestamp = (await provider.getBlock()).timestamp + const validUntil = currBlockTimestamp - 300 + const validAfter = currBlockTimestamp - 600 + const paymasterData = [defaultAbiCoder.encode(['uint48', 'uint48'], [validUntil, validAfter]), '0x' + '00'.repeat(65)] + + expect(await testUserOp('expired-sig', undefined, undefined, undefined, paymasterData) + .catch(e => e.message)).to.match(/expires too soon/) + }) + it('should fail with signature that is only valid in future in validation', async () => { + const currBlockTimestamp = (await provider.getBlock()).timestamp + const validUntil = currBlockTimestamp + 800 + const validAfter = currBlockTimestamp + 600 + const paymasterData = [defaultAbiCoder.encode(['uint48', 'uint48'], [validUntil, validAfter]), '0x' + '00'.repeat(65)] + + expect(await testUserOp('not-yet-valid-sig', undefined, undefined, undefined, paymasterData) + .catch(e => e.message)).to.match(/not valid yet/) + }) it('account succeeds referencing its own balance (after wallet creation)', async () => { await testExistingUserOp('balance-self')