Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Increase branch Coverage #108

Merged
merged 3 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions 4337/src/utils/execution.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AddressLike, BigNumberish, Contract, ContractTransaction, Signer, TransactionResponse, Wallet, ethers } from 'ethers'
import { BigNumberish, Contract, ContractTransaction, Signer, TransactionResponse, Wallet, ethers } from 'ethers'
import { Safe } from '../../typechain-types'

export const EIP_DOMAIN = {
Expand Down Expand Up @@ -128,7 +128,7 @@ export const buildSignatureBytes = (signatures: SafeSignature[]): string => {
return signatureBytes
}

export const logGas = async (message: string, tx: Promise<TransactionResponse>, skip?: boolean): Promise<any> => {
export const logGas = async (message: string, tx: Promise<TransactionResponse>, skip?: boolean): Promise<TransactionResponse> => {
return tx.then(async (result) => {
const receipt = await result.wait()
if (!skip) console.log(` Used ${receipt!.gasUsed} gas for >${message}<`)
Expand Down
6 changes: 3 additions & 3 deletions 4337/src/utils/safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export class Safe4337Operation {
console.log(this.signatures)
}

static async build(provider: JsonRpcProvider, safe: Safe4337, action: MetaTransaction, globalConfig: GlobalConfig): Promise<Safe4337Operation> {
static async build(provider: RpcProvider, safe: Safe4337, action: MetaTransaction, globalConfig: GlobalConfig): Promise<Safe4337Operation> {
const initCode = await safe.isDeployed() ? "0x" : safe.getInitCode()
const nonce = (await callInterface(provider, globalConfig.entryPoint, "getNonce", [safe.address, 0]))[0]
const estimateOperation = {
Expand Down Expand Up @@ -241,7 +241,7 @@ export class Safe4337 {
public address: string;
private globalConfig: GlobalConfig;
private safeConfig: SafeConfig | undefined;
private provider: JsonRpcProvider | undefined;
private provider: RpcProvider | undefined;

constructor(address: string, globalConfig: GlobalConfig, safeConfig?: SafeConfig) {
if (safeConfig) {
Expand All @@ -253,7 +253,7 @@ export class Safe4337 {
this.safeConfig = safeConfig
}

connect(provider: JsonRpcProvider): Safe4337 {
connect(provider: RpcProvider): Safe4337 {
this.provider = provider;
return this;
}
Expand Down
113 changes: 113 additions & 0 deletions 4337/test/eip4337/EIP4337Module.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { expect } from 'chai'
import { deployments, ethers } from 'hardhat'
import { Signer } from 'ethers'
import { getTestSafe, getSimple4337Module, getEntryPoint } from '../utils/setup'
import { buildSignatureBytes, signHash, logGas } from '../../src/utils/execution'
import {
buildSafeUserOp,
calculateSafeOperationHash,
buildUserOperationFromSafeUserOperation,
buildSafeUserOpTransaction,
signSafeOp,
} from '../../src/utils/userOp'
import { chainId } from '../utils/encoding'

describe('Simple4337Module', () => {
const setupTests = deployments.createFixture(async ({ deployments }) => {
await deployments.fixture()

const [user, untrusted] = await ethers.getSigners()
const entryPoint = await getEntryPoint()
const module = await getSimple4337Module()
const makeSafeModule = async (user: Signer) => {
const safe = await getTestSafe(user, await module.getAddress(), await module.getAddress())
return await ethers.getContractAt('Simple4337Module', await safe.getAddress())
}
const safeModule = await makeSafeModule(user)

return {
user,
untrusted,
entryPoint,
validator: module,
safeModule,
makeSafeModule,
}
})

describe('validateUserOp', () => {
it('should revert when validating user ops for a different Safe', async () => {
const { user, entryPoint, validator, safeModule, makeSafeModule } = await setupTests()

const safeOp = buildSafeUserOpTransaction(await safeModule.getAddress(), user.address, 0, '0x', '0', await entryPoint.getAddress())
const safeOpHash = calculateSafeOperationHash(await validator.getAddress(), safeOp, await chainId())
const signature = buildSignatureBytes([await signHash(user, safeOpHash)])
const userOp = buildUserOperationFromSafeUserOperation({
safeAddress: await safeModule.getAddress(),
safeOp,
signature,
})

const entryPointImpersonator = await ethers.getSigner(await entryPoint.getAddress())
const safeFromEntryPoint = safeModule.connect(entryPointImpersonator)
expect(await safeFromEntryPoint.validateUserOp.staticCall(userOp, ethers.ZeroHash, 0)).to.eq(0)

const otherSafe = (await makeSafeModule(user)).connect(entryPointImpersonator)
await expect(otherSafe.validateUserOp.staticCall(userOp, ethers.ZeroHash, 0)).to.be.revertedWith('Invalid Caller')
})

it('should revert when calling an unsupported Safe method', async () => {
const { user, untrusted, entryPoint, validator, safeModule } = await setupTests()

const abi = ['function addOwnerWithThreshold(address owner, uint256 threshold) external']
const callData = new ethers.Interface(abi).encodeFunctionData('addOwnerWithThreshold', [untrusted.address, 1])
const safeOp = buildSafeUserOp({
safe: await safeModule.getAddress(),
callData,
nonce: '0',
entryPoint: await entryPoint.getAddress(),
})
const safeOpHash = calculateSafeOperationHash(await validator.getAddress(), safeOp, await chainId())
const signature = buildSignatureBytes([await signHash(user, safeOpHash)])
const userOp = buildUserOperationFromSafeUserOperation({
safeAddress: await safeModule.getAddress(),
safeOp,
signature,
})

await expect(safeModule.validateUserOp(userOp, ethers.ZeroHash, 0)).to.be.revertedWith('Unsupported execution function id')
})

it('should revert when not called from the trusted entrypoint', async () => {
const { user, untrusted, entryPoint, validator, safeModule } = await setupTests()
const safeOp = buildSafeUserOpTransaction(await safeModule.getAddress(), user.address, 0, '0x', '0', await entryPoint.getAddress())
const safeOpHash = calculateSafeOperationHash(await validator.getAddress(), safeOp, await chainId())
const signature = buildSignatureBytes([await signHash(user, safeOpHash)])
const userOp = buildUserOperationFromSafeUserOperation({
safeAddress: await safeModule.getAddress(),
safeOp,
signature,
})

await expect(safeModule.connect(untrusted).validateUserOp(userOp, ethers.ZeroHash, 0)).to.be.revertedWith('Unsupported entry point')
})
})

describe('execUserOp', () => {
it('should revert when not called from the trusted entrypoint', async () => {
const { untrusted, safeModule } = await setupTests()
await expect(safeModule.connect(untrusted).executeUserOp(ethers.ZeroAddress, 0, '0x', 0)).to.be.revertedWith(
'Unsupported entry point',
)
})
})

describe('execUserOpWithErrorString', () => {
it('should revert when not called from the trusted entrypoint', async () => {
const { untrusted, safeModule } = await setupTests()
await expect(safeModule.connect(untrusted).executeUserOpWithErrorString(ethers.ZeroAddress, 0, '0x', 0)).to.be.revertedWith(
'Unsupported entry point',
)
})
})
})
23 changes: 19 additions & 4 deletions 4337/test/eip4337/EIP4337ModuleExisting.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '../../src/utils/userOp'
import { chainId } from '../utils/encoding'

describe('EIP4337Module - Existing Safe', async () => {
describe('EIP4337Module - Existing Safe', () => {
const setupTests = deployments.createFixture(async ({ deployments }) => {
await deployments.fixture()

Expand Down Expand Up @@ -49,7 +49,6 @@ describe('EIP4337Module - Existing Safe', async () => {
})

describe('execTransaction - existing account', () => {

it('should revert with invalid signature', async () => {
const { user1, safe, entryPoint } = await setupTests()

Expand Down Expand Up @@ -128,6 +127,22 @@ describe('EIP4337Module - Existing Safe', async () => {
expect(emittedRevert).to.be.true
})

it('executeUserOpWithErrorString should execute contract calls', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I actually forgot to do in the executeUserOpWithErrorString PR is to add tests for it to the EIP4337ModuleNew.spec.ts 🙈 would be amazing if you add them

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added - the revert propagation test was also missing which I added (asserting that the Safe gets deployed even if the user op reverts).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const { user1, safe, validator, entryPoint } = await setupTests()

await user1.sendTransaction({to: await safe.getAddress(), value: ethers.parseEther("1.0")})
expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther("1.0"))
const safeOp = buildSafeUserOpTransaction(await safe.getAddress(), user1.address, ethers.parseEther("0.5"), "0x", '0', await entryPoint.getAddress(), false, true)
const safeOpHash = calculateSafeOperationHash(await validator.getAddress(), safeOp, await chainId())
const signature = buildSignatureBytes([await signHash(user1, safeOpHash)])
const userOp = buildUserOperationFromSafeUserOperation({safeAddress: await safe.getAddress(), safeOp, signature})
await logGas(
"Execute UserOp without fee payment",
entryPoint.executeUserOp(userOp, 0)
)
expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther("0.5"))
})

it('executeUserOpWithErrorString reverts on failure and bubbles up the revert reason', async () => {
const { user1, safe, validator, entryPoint } = await setupTests()
const reverterContract = await ethers.getContractFactory("TestReverter").then(factory => factory.deploy())
Expand All @@ -143,8 +158,8 @@ describe('EIP4337Module - Existing Safe', async () => {
const transaction = await entryPoint.executeUserOp(userOp, ethers.parseEther("0.000001")).then((tx: any) => tx.wait())
const logs = transaction.logs.map((log: any) => entryPoint.interface.parseLog(log))
const emittedRevert = logs.find((l: any) => l.name === "UserOpReverted")
const decodedError = ethers.AbiCoder.defaultAbiCoder().decode(["string"], `0x${emittedRevert.args.reason.slice(10)}`)
expect(decodedError[0]).to.equal("You called a function that always reverts")
const [decodedError] = ethers.AbiCoder.defaultAbiCoder().decode(["string"], `0x${emittedRevert.args.reason.slice(10)}`)
expect(decodedError).to.equal("You called a function that always reverts")
})
})
})
95 changes: 83 additions & 12 deletions 4337/test/eip4337/EIP4337ModuleNew.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import {
import { chainId } from '../utils/encoding'
import { Safe4337 } from '../../src/utils/safe'

describe('EIP4337Module - Newly deployed safe', async () => {
describe('EIP4337Module - Newly deployed safe', () => {
const setupTests = deployments.createFixture(async ({ deployments }) => {
await deployments.fixture()

const [user1] = await ethers.getSigners()
const entryPoint = await getEntryPoint()
const module = await getSimple4337Module()
Expand All @@ -33,7 +33,7 @@ describe('EIP4337Module - Newly deployed safe', async () => {

return {
user1,
safe,
safe: safe.connect(ethers.provider),
proxyFactory,
addModulesLib,
validator: module,
Expand All @@ -42,7 +42,6 @@ describe('EIP4337Module - Newly deployed safe', async () => {
})

describe('execTransaction - new account', () => {

it('should revert with invalid signature', async () => {
const { user1, safe, entryPoint } = await setupTests()

Expand All @@ -51,8 +50,8 @@ describe('EIP4337Module - Newly deployed safe', async () => {
const safeOp = buildSafeUserOpTransaction(safe.address, user1.address, ethers.parseEther("0.5"), "0x", '0', await entryPoint.getAddress())
const signature = buildSignatureBytes([await signSafeOp(user1, user1.address, safeOp, await chainId())])
const userOp = buildUserOperationFromSafeUserOperation({
safeAddress: safe.address,
safeOp,
safeAddress: safe.address,
safeOp,
signature,
initCode: safe.getInitCode()
})
Expand All @@ -68,8 +67,8 @@ describe('EIP4337Module - Newly deployed safe', async () => {
const safeOp = buildSafeUserOpTransaction(safe.address, user1.address, ethers.parseEther("0.5"), "0x", '0', await entryPoint.getAddress())
const signature = buildSignatureBytes([await signSafeOp(user1, await validator.getAddress(), safeOp, await chainId())])
const userOp = buildUserOperationFromSafeUserOperation({
safeAddress: safe.address,
safeOp,
safeAddress: safe.address,
safeOp,
signature,
initCode: safe.getInitCode()
})
Expand All @@ -88,8 +87,8 @@ describe('EIP4337Module - Newly deployed safe', async () => {
const safeOp = buildSafeUserOpTransaction(safe.address, user1.address, ethers.parseEther("0.5"), "0x", '0', await entryPoint.getAddress())
const signature = buildSignatureBytes([await signSafeOp(user1, await validator.getAddress(), safeOp, await chainId())])
const userOp = buildUserOperationFromSafeUserOperation({
safeAddress: safe.address,
safeOp,
safeAddress: safe.address,
safeOp,
signature,
initCode: safe.getInitCode()
})
Expand All @@ -101,6 +100,30 @@ describe('EIP4337Module - Newly deployed safe', async () => {
await expect(entryPoint.executeUserOp(userOp, 0)).to.be.revertedWithCustomError(entryPoint, "InvalidNonce").withArgs(0)
})

it('reverts on failure', async () => {
const { user1, safe, validator, entryPoint } = await setupTests()

await user1.sendTransaction({to: safe.address, value: ethers.parseEther("0.000001")})
expect(await ethers.provider.getBalance(safe.address)).to.be.eq(ethers.parseEther("0.000001"))
const safeOp = buildSafeUserOpTransaction(safe.address, user1.address, ethers.parseEther("0.5"), "0x", '0', await entryPoint.getAddress())
const signature = buildSignatureBytes([await signSafeOp(user1, await validator.getAddress(), safeOp, await chainId())])
const userOp = buildUserOperationFromSafeUserOperation({
safeAddress: safe.address,
safeOp,
signature,
initCode: safe.getInitCode()
})
const transaction = await logGas(
"Execute UserOp without fee payment",
entryPoint.executeUserOp(userOp, 0)
)
const receipt = await transaction.wait();
const logs = receipt!.logs.map((log: any) => entryPoint.interface.parseLog(log))
const emittedRevert = logs.some((log) => log?.name === "UserOpReverted")
expect(emittedRevert).to.be.true
expect(await safe.isDeployed()).to.be.true;
})

it('should execute contract calls with fee', async () => {
const { user1, safe, validator, entryPoint } = await setupTests()

Expand All @@ -109,8 +132,28 @@ describe('EIP4337Module - Newly deployed safe', async () => {
const safeOp = buildSafeUserOpTransaction(safe.address, user1.address, ethers.parseEther("0.5"), "0x", '0', await entryPoint.getAddress())
const signature = buildSignatureBytes([await signSafeOp(user1, await validator.getAddress(), safeOp, await chainId())])
const userOp = buildUserOperationFromSafeUserOperation({
safeAddress: safe.address,
safeOp,
safeAddress: safe.address,
safeOp,
signature,
initCode: safe.getInitCode()
})
await logGas(
"Execute UserOp with fee payment",
entryPoint.executeUserOp(userOp, ethers.parseEther("0.000001"))
)
expect(await ethers.provider.getBalance(safe.address)).to.be.eq(ethers.parseEther("0.499999"))
})

it('executeUserOpWithErrorString should execute contract calls', async () => {
const { user1, safe, validator, entryPoint } = await setupTests()

await user1.sendTransaction({to: safe.address, value: ethers.parseEther("1.0")})
expect(await ethers.provider.getBalance(safe.address)).to.be.eq(ethers.parseEther("1.0"))
const safeOp = buildSafeUserOpTransaction(safe.address, user1.address, ethers.parseEther("0.5"), "0x", '0', await entryPoint.getAddress(), false, true)
const signature = buildSignatureBytes([await signSafeOp(user1, await validator.getAddress(), safeOp, await chainId())])
const userOp = buildUserOperationFromSafeUserOperation({
safeAddress: safe.address,
safeOp,
signature,
initCode: safe.getInitCode()
})
Expand All @@ -120,5 +163,33 @@ describe('EIP4337Module - Newly deployed safe', async () => {
)
expect(await ethers.provider.getBalance(safe.address)).to.be.eq(ethers.parseEther("0.499999"))
})

it('executeUserOpWithErrorString reverts on failure and bubbles up the revert reason', async () => {
const { user1, safe, validator, entryPoint } = await setupTests()

const reverterContract = await ethers.getContractFactory("TestReverter").then(factory => factory.deploy())
const callData = reverterContract.interface.encodeFunctionData("alwaysReverting")

await user1.sendTransaction({to: safe.address, value: ethers.parseEther("1.0")})
expect(await ethers.provider.getBalance(safe.address)).to.be.eq(ethers.parseEther("1.0"))
const safeOp = buildSafeUserOpTransaction(safe.address, await reverterContract.getAddress(), 0, callData, '0', await entryPoint.getAddress(), false, true)
const signature = buildSignatureBytes([await signSafeOp(user1, await validator.getAddress(), safeOp, await chainId())])
const userOp = buildUserOperationFromSafeUserOperation({
safeAddress: safe.address,
safeOp,
signature,
initCode: safe.getInitCode()
})
const transaction = await logGas(
"Execute UserOp without fee payment",
entryPoint.executeUserOp(userOp, 0)
)
const receipt = await transaction.wait();
const logs = receipt!.logs.map((log: any) => entryPoint.interface.parseLog(log))
const emittedRevert = logs.find((log) => log?.name === "UserOpReverted")
const [decodedError] = ethers.AbiCoder.defaultAbiCoder().decode(["string"], `0x${emittedRevert!.args.reason.slice(10)}`)
expect(decodedError).to.equal("You called a function that always reverts")
expect(await safe.isDeployed()).to.be.true;
})
})
})
2 changes: 1 addition & 1 deletion 4337/test/eip4337/EIP4337Safe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '../../src/utils/userOp'
import { chainId } from '../utils/encoding'

describe('EIP4337Safe', async () => {
describe('EIP4337Safe', () => {
const setupTests = deployments.createFixture(async ({ deployments }) => {
await deployments.fixture()

Expand Down