// AereModularAccount stack tests. // // Deploys a LOCAL copy of AERE's own AereEntryPointV2 (the same source that is // live at 0x8D6f40598d552fF0Cb358b6012cF4227B86aF770 on chain 2800) and drives // every userOp through ep.handleOps, exactly like the live bundler path. // // Routing convention under test (first signature byte): // 0x00 || 65-byte ECDSA -> root-owner fallback (EIP-191 over userOpHash) // 0x01 || validator(20) || payload -> installed type-1 validator module // // Run: npx hardhat test test/modular-account.test.js const { expect } = require("chai"); const { ethers } = require("hardhat"); const { loadFixture, time } = require("@nomicfoundation/hardhat-toolbox/network-helpers"); const coder = ethers.AbiCoder.defaultAbiCoder(); const VERIF_GAS = 500_000n; const CALL_GAS = 1_000_000n; const PRE_VERIF_GAS = 21_000n; const MAX_FEE = 10n ** 9n; // 1 gwei const NATIVE_SENTINEL = "0x00000000"; const MAGIC_1271 = "0x1626ba7e"; const FAIL_1271 = "0xffffffff"; const DAY = 24 * 3600; function packUints(hi, lo) { return ethers.toBeHex((hi << 128n) | lo, 32); } async function buildUserOp(ep, accountAddr, callData) { return { sender: accountAddr, nonce: await ep.getNonce(accountAddr), initCode: "0x", callData, accountGasLimits: packUints(VERIF_GAS, CALL_GAS), preVerificationGas: PRE_VERIF_GAS, gasFees: packUints(MAX_FEE, MAX_FEE), paymasterAndData: "0x", signature: "0x", }; } async function signRoot(ep, op, rootSigner) { const hash = await ep.getUserOpHash(op); const sig = await rootSigner.signMessage(ethers.getBytes(hash)); op.signature = ethers.concat(["0x00", sig]); return op; } async function signSession(ep, op, validatorAddr, sessionWallet) { const hash = await ep.getUserOpHash(op); const sig = await sessionWallet.signMessage(ethers.getBytes(hash)); op.signature = ethers.concat(["0x01", validatorAddr, sig]); return op; } function sessionInitData(key, validAfter, validUntil, maxValuePerOp, targets, selectors) { return coder.encode( ["address", "uint48", "uint48", "uint256", "address[]", "bytes4[]"], [key, validAfter, validUntil, maxValuePerOp, targets, selectors] ); } describe("AereModularAccount + session keys + social recovery", function () { async function fixture() { const [deployer, root, guardian1, guardian2, guardian3, rando, newOwner, bundler] = await ethers.getSigners(); const EP = await ethers.getContractFactory("AereEntryPointV2"); const ep = await EP.deploy(); await ep.waitForDeployment(); const epAddr = await ep.getAddress(); const Factory = await ethers.getContractFactory("AereModularAccountFactory"); const factory = await Factory.deploy(epAddr); await factory.waitForDeployment(); const Validator = await ethers.getContractFactory("AereSessionKeyValidator"); const validator = await Validator.deploy(); await validator.waitForDeployment(); const validatorAddr = await validator.getAddress(); const Recovery = await ethers.getContractFactory("AereSocialRecoveryModule"); const recovery = await Recovery.deploy(); await recovery.waitForDeployment(); const recoveryAddr = await recovery.getAddress(); const Ping = await ethers.getContractFactory("AerePingCounter"); const ping = await Ping.deploy(); await ping.waitForDeployment(); const pingAddr = await ping.getAddress(); const pingSel = ping.interface.getFunction("ping").selector; // Create the account (root owner = root signer). const salt = 1n; const predicted = await factory["getAddress(address,uint256)"](root.address, salt); await (await factory.createAccount(root.address, salt)).wait(); const account = await ethers.getContractAt("AereModularAccount", predicted); const accountAddr = predicted; // Fund the account so it can prefund its EntryPoint deposit. await (await deployer.sendTransaction({ to: accountAddr, value: ethers.parseEther("1") })).wait(); // Session key wallet (never funded, never a tx sender - signature only). const sessionKey = ethers.Wallet.createRandom(); return { deployer, root, guardian1, guardian2, guardian3, rando, newOwner, bundler, ep, epAddr, factory, validator, validatorAddr, recovery, recoveryAddr, ping, pingAddr, pingSel, account, accountAddr, sessionKey, salt, }; } // Fixture with the session validator installed and a session enabled: // scope = { (ping, ping()), (ping, native transfer) }, maxValue 0.01 ETH, // window = [now, now + 1h]. async function sessionFixture() { const ctx = await loadFixture(fixture); const now = await time.latest(); const validUntil = now + 3600; const initData = sessionInitData( ctx.sessionKey.address, 0, validUntil, ethers.parseEther("0.01"), [ctx.pingAddr, ctx.pingAddr], [ctx.pingSel, NATIVE_SENTINEL] ); await (await ctx.account.connect(ctx.root).installModule(1, ctx.validatorAddr, initData)).wait(); return { ...ctx, validUntil }; } // ---- 1. Account creation --------------------------------------------------- describe("account creation (CREATE2 factory)", function () { it("deploys at the deterministic getAddress and sets root owner + entryPoint", async function () { const { factory, account, accountAddr, root, epAddr, salt } = await loadFixture(fixture); expect(await factory["getAddress(address,uint256)"](root.address, salt)).to.equal(accountAddr); expect(await account.rootOwner()).to.equal(root.address); expect(await account.entryPoint()).to.equal(epAddr); expect(await account.accountId()).to.equal("aere.modular-account.1.0.0"); }); it("createAccount is idempotent for the same (owner, salt)", async function () { const { factory, root, salt, accountAddr } = await loadFixture(fixture); await (await factory.createAccount(root.address, salt)).wait(); // second call, no revert expect(await factory["getAddress(address,uint256)"](root.address, salt)).to.equal(accountAddr); }); it("different owner produces a different address (no front-running)", async function () { const { factory, root, rando, salt } = await loadFixture(fixture); const a = await factory["getAddress(address,uint256)"](root.address, salt); const b = await factory["getAddress(address,uint256)"](rando.address, salt); expect(a).to.not.equal(b); }); it("initialize cannot be called twice", async function () { const { account, epAddr, rando } = await loadFixture(fixture); await expect(account.connect(rando).initialize(epAddr, rando.address)) .to.be.revertedWithCustomError(account, "Unauthorized"); }); }); // ---- 2. Root-owner fallback path via LIVE-shape EntryPoint ------------------- describe("root-owner fallback validation (0x00 prefix)", function () { it("root-signed userOp executes through ep.handleOps", async function () { const { ep, account, accountAddr, root, bundler, ping, pingAddr } = await loadFixture(fixture); const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]); const op = await signRoot(ep, await buildUserOp(ep, accountAddr, callData), root); await (await ep.connect(bundler).handleOps([op], bundler.address)).wait(); expect(await ping.pings()).to.equal(1n); expect(await ep.getNonce(accountAddr)).to.equal(1n); }); it("non-root signature on 0x00 path REVERTS handleOps", async function () { const { ep, account, accountAddr, rando, bundler, ping, pingAddr } = await loadFixture(fixture); const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]); const op = await signRoot(ep, await buildUserOp(ep, accountAddr, callData), rando); await expect(ep.connect(bundler).handleOps([op], bundler.address)) .to.be.revertedWithCustomError(ep, "AccountValidationFailed"); }); it("root can rotate its own key via execute self-call, direct setRootOwner is blocked", async function () { const { account, accountAddr, root, newOwner, rando } = await loadFixture(fixture); await expect(account.connect(root).setRootOwner(newOwner.address)) .to.be.revertedWithCustomError(account, "Unauthorized"); await expect(account.connect(rando).execute(accountAddr, 0, "0x")) .to.be.revertedWithCustomError(account, "Unauthorized"); const data = account.interface.encodeFunctionData("setRootOwner", [newOwner.address]); await (await account.connect(root).execute(accountAddr, 0, data)).wait(); expect(await account.rootOwner()).to.equal(newOwner.address); }); }); // ---- 3. Module management ----------------------------------------------------- describe("ERC-7579-style module management", function () { it("install validator module + isModuleInstalled + selector constants match", async function () { const { account, validator, validatorAddr, root } = await loadFixture(fixture); await (await account.connect(root).installModule(1, validatorAddr, "0x")).wait(); expect(await account.isModuleInstalled(1, validatorAddr, "0x")).to.equal(true); expect(await account.isModuleInstalled(2, validatorAddr, "0x")).to.equal(false); expect(await validator.EXECUTE_SELECTOR()).to.equal(account.interface.getFunction("execute").selector); expect(await validator.EXECUTE_BATCH_SELECTOR()).to.equal(account.interface.getFunction("executeBatch").selector); }); it("stranger cannot install or uninstall modules", async function () { const { account, validatorAddr, rando } = await loadFixture(fixture); await expect(account.connect(rando).installModule(1, validatorAddr, "0x")) .to.be.revertedWithCustomError(account, "Unauthorized"); await expect(account.connect(rando).uninstallModule(1, validatorAddr, "0x")) .to.be.revertedWithCustomError(account, "Unauthorized"); }); it("wrong module type / non-contract module are rejected", async function () { const { account, validatorAddr, recoveryAddr, root, rando } = await loadFixture(fixture); await expect(account.connect(root).installModule(3, validatorAddr, "0x")) .to.be.revertedWithCustomError(account, "UnsupportedModuleType"); await expect(account.connect(root).installModule(1, rando.address, "0x")) .to.be.revertedWithCustomError(account, "InvalidModule"); // recovery module declares type 2, not type 1 await expect(account.connect(root).installModule(1, recoveryAddr, "0x")) .to.be.revertedWithCustomError(account, "InvalidModule"); }); it("executeFromExecutor rejected for non-installed executor", async function () { const { account, rando } = await loadFixture(fixture); await expect(account.connect(rando).executeFromExecutor(rando.address, 0, "0x")) .to.be.revertedWithCustomError(account, "Unauthorized"); }); }); // ---- 4. Session key happy path ------------------------------------------------- describe("session key userOps (0x01 prefix, signed ONLY by the session key)", function () { it("in-scope call (ping) executes through ep.handleOps", async function () { const { ep, account, accountAddr, validatorAddr, sessionKey, bundler, ping, pingAddr } = await sessionFixture(); const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]); const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey); const rc = await (await ep.connect(bundler).handleOps([op], bundler.address)).wait(); expect(await ping.pings()).to.equal(1n); // UserOperationEvent(success=true) const evt = rc.logs.map(l => { try { return ep.interface.parseLog(l); } catch { return null; } }) .find(p => p && p.name === "UserOperationEvent"); expect(evt.args.success).to.equal(true); expect(evt.args.sender).to.equal(accountAddr); }); it("in-scope native transfer within maxValuePerOp executes", async function () { const { ep, account, accountAddr, validatorAddr, sessionKey, bundler, pingAddr } = await sessionFixture(); const before = await ethers.provider.getBalance(pingAddr); const callData = account.interface.encodeFunctionData("execute", [pingAddr, ethers.parseEther("0.005"), "0x"]); const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey); await (await ep.connect(bundler).handleOps([op], bundler.address)).wait(); expect(await ethers.provider.getBalance(pingAddr)).to.equal(before + ethers.parseEther("0.005")); }); it("in-scope executeBatch (two pings) executes", async function () { const { ep, account, accountAddr, validatorAddr, sessionKey, bundler, ping, pingAddr } = await sessionFixture(); const pingData = ping.interface.encodeFunctionData("ping"); const callData = account.interface.encodeFunctionData("executeBatch", [[ { target: pingAddr, value: 0, data: pingData }, { target: pingAddr, value: 0, data: pingData }, ]]); const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey); await (await ep.connect(bundler).handleOps([op], bundler.address)).wait(); expect(await ping.pings()).to.equal(2n); }); }); // ---- 5. Session key negative paths --------------------------------------------- describe("session key scope enforcement (all REVERT handleOps)", function () { it("out-of-scope target reverts", async function () { const { ep, account, accountAddr, validatorAddr, sessionKey, bundler, ping, rando } = await sessionFixture(); const callData = account.interface.encodeFunctionData("execute", [rando.address, 0, ping.interface.encodeFunctionData("ping")]); const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey); await expect(ep.connect(bundler).handleOps([op], bundler.address)) .to.be.revertedWithCustomError(ep, "AccountValidationFailed"); }); it("wrong selector on allowed target reverts", async function () { const { ep, account, accountAddr, validatorAddr, sessionKey, bundler, ping, pingAddr } = await sessionFixture(); const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("forbidden")]); const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey); await expect(ep.connect(bundler).handleOps([op], bundler.address)) .to.be.revertedWithCustomError(ep, "AccountValidationFailed"); expect(await ping.forbiddenCalls()).to.equal(0n); }); it("value above maxValuePerOp reverts", async function () { const { ep, account, accountAddr, validatorAddr, sessionKey, bundler, pingAddr } = await sessionFixture(); const callData = account.interface.encodeFunctionData("execute", [pingAddr, ethers.parseEther("0.02"), "0x"]); const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey); await expect(ep.connect(bundler).handleOps([op], bundler.address)) .to.be.revertedWithCustomError(ep, "AccountValidationFailed"); }); it("batch with one out-of-scope call reverts entirely", async function () { const { ep, account, accountAddr, validatorAddr, sessionKey, bundler, ping, pingAddr, rando } = await sessionFixture(); const pingData = ping.interface.encodeFunctionData("ping"); const callData = account.interface.encodeFunctionData("executeBatch", [[ { target: pingAddr, value: 0, data: pingData }, { target: rando.address, value: 0, data: "0x" }, ]]); const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey); await expect(ep.connect(bundler).handleOps([op], bundler.address)) .to.be.revertedWithCustomError(ep, "AccountValidationFailed"); expect(await ping.pings()).to.equal(0n); }); it("session key cannot call installModule (only execute/executeBatch enterable)", async function () { const { ep, account, accountAddr, validatorAddr, recoveryAddr, sessionKey, bundler } = await sessionFixture(); const callData = account.interface.encodeFunctionData("installModule", [2, recoveryAddr, "0x"]); const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey); await expect(ep.connect(bundler).handleOps([op], bundler.address)) .to.be.revertedWithCustomError(ep, "AccountValidationFailed"); }); it("expired session (past validUntil) reverts", async function () { const { ep, account, accountAddr, validatorAddr, sessionKey, bundler, ping, pingAddr } = await sessionFixture(); await time.increase(2 * 3600); // window was 1h const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]); const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey); await expect(ep.connect(bundler).handleOps([op], bundler.address)) .to.be.revertedWithCustomError(ep, "AccountValidationFailed"); }); it("not-yet-valid session (validAfter in the future) reverts, then works after time passes", async function () { const { ep, account, accountAddr, validator, validatorAddr, root, bundler, ping, pingAddr, pingSel } = await loadFixture(fixture); const futureKey = ethers.Wallet.createRandom(); const now = await time.latest(); const initData = sessionInitData(futureKey.address, now + 3600, now + 7200, 0, [pingAddr], [pingSel]); await (await account.connect(root).installModule(1, validatorAddr, initData)).wait(); const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]); let op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, futureKey); await expect(ep.connect(bundler).handleOps([op], bundler.address)) .to.be.revertedWithCustomError(ep, "AccountValidationFailed"); await time.increase(3700); op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, futureKey); await (await ep.connect(bundler).handleOps([op], bundler.address)).wait(); expect(await ping.pings()).to.equal(1n); (validator); // silence unused }); it("revoked session reverts (revocation via root-driven account.execute)", async function () { const { ep, account, accountAddr, validator, validatorAddr, sessionKey, root, bundler, ping, pingAddr } = await sessionFixture(); const revokeData = validator.interface.encodeFunctionData("revokeSession", [sessionKey.address]); await (await account.connect(root).execute(validatorAddr, 0, revokeData)).wait(); const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]); const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey); await expect(ep.connect(bundler).handleOps([op], bundler.address)) .to.be.revertedWithCustomError(ep, "AccountValidationFailed"); }); it("uninstalling the validator removes routing", async function () { const { ep, account, accountAddr, validatorAddr, sessionKey, root, bundler, ping, pingAddr } = await sessionFixture(); await (await account.connect(root).uninstallModule(1, validatorAddr, "0x")).wait(); expect(await account.isModuleInstalled(1, validatorAddr, "0x")).to.equal(false); const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]); const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey); await expect(ep.connect(bundler).handleOps([op], bundler.address)) .to.be.revertedWithCustomError(ep, "AccountValidationFailed"); }); it("stranger cannot register a session for someone else's account (state is per-msg.sender)", async function () { const { validator, accountAddr, rando, pingAddr, pingSel } = await sessionFixture(); const evilKey = ethers.Wallet.createRandom(); // rando CAN call enableSession, but it writes rando's own namespace, // not the account's - the account's session set is untouched. await (await validator.connect(rando).enableSession(evilKey.address, 0, 0, 0, [pingAddr], [pingSel])).wait(); const s = await validator.sessions(accountAddr, evilKey.address); expect(s.enabled).to.equal(false); }); }); // ---- 6. Social recovery ----------------------------------------------------------- async function recoveryFixture() { const ctx = await loadFixture(fixture); const initData = coder.encode( ["address[]", "uint256"], [[ctx.guardian1.address, ctx.guardian2.address, ctx.guardian3.address], 2] ); await (await ctx.account.connect(ctx.root).installModule(2, ctx.recoveryAddr, initData)).wait(); return ctx; } describe("social recovery (2-of-3 guardians, 48h timelock)", function () { it("install as executor sets guardians and threshold", async function () { const { account, recovery, recoveryAddr, accountAddr, guardian1, guardian2, guardian3, rando } = await recoveryFixture(); expect(await account.isModuleInstalled(2, recoveryAddr, "0x")).to.equal(true); expect(await recovery.isGuardian(accountAddr, guardian1.address)).to.equal(true); expect(await recovery.isGuardian(accountAddr, guardian2.address)).to.equal(true); expect(await recovery.isGuardian(accountAddr, guardian3.address)).to.equal(true); expect(await recovery.isGuardian(accountAddr, rando.address)).to.equal(false); expect(await recovery.thresholdOf(accountAddr)).to.equal(2n); expect((await recovery.guardiansOf(accountAddr)).length).to.equal(3); }); it("non-guardian cannot initiate or support", async function () { const { recovery, accountAddr, rando, newOwner, guardian1 } = await recoveryFixture(); await expect(recovery.connect(rando).initiateRecovery(accountAddr, newOwner.address)) .to.be.revertedWithCustomError(recovery, "NotGuardian"); await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait(); await expect(recovery.connect(rando).supportRecovery(accountAddr)) .to.be.revertedWithCustomError(recovery, "NotGuardian"); }); it("premature executeRecovery reverts even with threshold met", async function () { const { recovery, accountAddr, guardian1, guardian2, newOwner } = await recoveryFixture(); await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait(); await (await recovery.connect(guardian2).supportRecovery(accountAddr)).wait(); await expect(recovery.connect(guardian1).executeRecovery(accountAddr)) .to.be.revertedWithCustomError(recovery, "TimelockNotElapsed"); }); it("below-threshold executeRecovery reverts even after 48h", async function () { const { recovery, accountAddr, guardian1, newOwner } = await recoveryFixture(); await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait(); await time.increase(2 * DAY + 60); await expect(recovery.connect(guardian1).executeRecovery(accountAddr)) .to.be.revertedWithCustomError(recovery, "BelowThreshold"); }); it("non-guardian executeRecovery reverts even when otherwise ready", async function () { const { recovery, accountAddr, guardian1, guardian2, rando, newOwner } = await recoveryFixture(); await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait(); await (await recovery.connect(guardian2).supportRecovery(accountAddr)).wait(); await time.increase(2 * DAY + 60); await expect(recovery.connect(rando).executeRecovery(accountAddr)) .to.be.revertedWithCustomError(recovery, "NotGuardian"); }); it("happy path: initiate + support + 48h -> root owner replaced, control transfers", async function () { const { recovery, account, accountAddr, guardian1, guardian2, root, newOwner, ping, pingAddr } = await recoveryFixture(); await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait(); await (await recovery.connect(guardian2).supportRecovery(accountAddr)).wait(); await time.increase(2 * DAY + 60); await (await recovery.connect(guardian1).executeRecovery(accountAddr)).wait(); expect(await account.rootOwner()).to.equal(newOwner.address); // Old root lost direct admin; new owner has it. await expect(account.connect(root).execute(pingAddr, 0, ping.interface.encodeFunctionData("ping"))) .to.be.revertedWithCustomError(account, "Unauthorized"); await (await account.connect(newOwner).execute(pingAddr, 0, ping.interface.encodeFunctionData("ping"))).wait(); expect(await ping.pings()).to.equal(1n); }); it("current owner can cancelRecovery; execution then impossible", async function () { const { recovery, accountAddr, guardian1, guardian2, root, newOwner } = await recoveryFixture(); await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait(); await (await recovery.connect(guardian2).supportRecovery(accountAddr)).wait(); await (await recovery.connect(root).cancelRecovery(accountAddr)).wait(); await time.increase(2 * DAY + 60); await expect(recovery.connect(guardian1).executeRecovery(accountAddr)) .to.be.revertedWithCustomError(recovery, "NoActiveRecovery"); }); it("stranger cannot cancelRecovery", async function () { const { recovery, accountAddr, guardian1, rando, newOwner } = await recoveryFixture(); await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait(); await expect(recovery.connect(rando).cancelRecovery(accountAddr)) .to.be.revertedWithCustomError(recovery, "NotOwner"); }); it("duplicate support reverts; guardian add/remove is account-only", async function () { const { recovery, account, accountAddr, guardian1, root, rando, newOwner } = await recoveryFixture(); await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait(); await expect(recovery.connect(guardian1).supportRecovery(accountAddr)) .to.be.revertedWithCustomError(recovery, "AlreadyApproved"); // rando calling addGuardian writes rando's own namespace, not the account's. await (await recovery.connect(rando).addGuardian(newOwner.address)).wait(); expect(await recovery.isGuardian(accountAddr, newOwner.address)).to.equal(false); // Account (driven by root) can add a guardian for itself. const addData = recovery.interface.encodeFunctionData("addGuardian", [rando.address]); await (await account.connect(root).execute(await recovery.getAddress(), 0, addData)).wait(); expect(await recovery.isGuardian(accountAddr, rando.address)).to.equal(true); }); it("uninstall cleans guardians and active recovery", async function () { const { recovery, recoveryAddr, account, accountAddr, guardian1, root, newOwner } = await recoveryFixture(); await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait(); await (await account.connect(root).uninstallModule(2, recoveryAddr, "0x")).wait(); expect(await recovery.isGuardian(accountAddr, guardian1.address)).to.equal(false); expect(await recovery.thresholdOf(accountAddr)).to.equal(0n); const r = await recovery.activeRecovery(accountAddr); expect(r.active).to.equal(false); // And the module lost executor rights: even a ready recovery could not fire. expect(await account.isModuleInstalled(2, recoveryAddr, "0x")).to.equal(false); }); }); // ---- 7. EIP-1271 ------------------------------------------------------------------- describe("EIP-1271 isValidSignature", function () { function wrap1271(accountAddr, chainId, hash) { return ethers.keccak256(coder.encode( ["bytes32", "address", "uint256", "bytes32"], [ ethers.keccak256(ethers.toUtf8Bytes("AereAccount1271(address account,uint256 chainid,bytes32 hash)")), accountAddr, chainId, hash, ] )); } it("root path (0x00) returns magic for a valid root signature", async function () { const { account, accountAddr, root } = await loadFixture(fixture); const { chainId } = await ethers.provider.getNetwork(); const hash = ethers.keccak256(ethers.toUtf8Bytes("Sign in to dapp.example")); const wrapped = wrap1271(accountAddr, chainId, hash); const sig = ethers.concat(["0x00", await root.signMessage(ethers.getBytes(wrapped))]); expect(await account.isValidSignature(hash, sig)).to.equal(MAGIC_1271); }); it("root path rejects a stranger's signature", async function () { const { account, accountAddr, rando } = await loadFixture(fixture); const { chainId } = await ethers.provider.getNetwork(); const hash = ethers.keccak256(ethers.toUtf8Bytes("Sign in to dapp.example")); const wrapped = wrap1271(accountAddr, chainId, hash); const sig = ethers.concat(["0x00", await rando.signMessage(ethers.getBytes(wrapped))]); expect(await account.isValidSignature(hash, sig)).to.equal(FAIL_1271); }); it("validator path (0x01) returns magic for an active session key, fails after expiry", async function () { const { account, accountAddr, validatorAddr, sessionKey } = await sessionFixture(); const { chainId } = await ethers.provider.getNetwork(); const hash = ethers.keccak256(ethers.toUtf8Bytes("dapp login")); const wrapped = wrap1271(accountAddr, chainId, hash); const inner = await sessionKey.signMessage(ethers.getBytes(wrapped)); const sig = ethers.concat(["0x01", validatorAddr, inner]); expect(await account.isValidSignature(hash, sig)).to.equal(MAGIC_1271); await time.increase(2 * 3600); // past validUntil expect(await account.isValidSignature(hash, sig)).to.equal(FAIL_1271); }); it("validator path fails for a non-installed validator address", async function () { const { account, accountAddr, sessionKey, recoveryAddr } = await sessionFixture(); const { chainId } = await ethers.provider.getNetwork(); const hash = ethers.keccak256(ethers.toUtf8Bytes("dapp login")); const wrapped = wrap1271(accountAddr, chainId, hash); const inner = await sessionKey.signMessage(ethers.getBytes(wrapped)); const sig = ethers.concat(["0x01", recoveryAddr, inner]); expect(await account.isValidSignature(hash, sig)).to.equal(FAIL_1271); }); }); // ---- 8. EntryPoint economics --------------------------------------------------------- describe("EntryPoint deposit handling (AereEntryPointV2 semantics)", function () { it("account self-funds its EP deposit during validation and the bundler is paid", async function () { const { ep, account, accountAddr, root, bundler, ping, pingAddr } = await loadFixture(fixture); const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]); const op = await signRoot(ep, await buildUserOp(ep, accountAddr, callData), root); const bundlerBefore = await ethers.provider.getBalance(bundler.address); await (await ep.connect(bundler).handleOps([op], bundler.address)).wait(); const dep = await ep.balanceOf(accountAddr); // Deposit was topped to maxCost and charged actualGasUsed * maxFee. const maxCost = (VERIF_GAS + CALL_GAS + PRE_VERIF_GAS) * MAX_FEE; expect(dep).to.be.lt(maxCost); expect(dep).to.be.gt(0n); (bundlerBefore); // bundler pays tx gas but receives the op fee; net not asserted }); it("nonce replay is rejected by the EntryPoint", async function () { const { ep, account, accountAddr, root, bundler, ping, pingAddr } = await loadFixture(fixture); const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]); const op = await signRoot(ep, await buildUserOp(ep, accountAddr, callData), root); await (await ep.connect(bundler).handleOps([op], bundler.address)).wait(); await expect(ep.connect(bundler).handleOps([op], bundler.address)) .to.be.revertedWithCustomError(ep, "FailedOp"); }); }); });