diff --git a/.prettierignore b/.prettierignore index 02686d63..3a9c9292 100644 --- a/.prettierignore +++ b/.prettierignore @@ -23,7 +23,6 @@ deployments-zk # Disable prettier for files if VSCode settings are ignored **/*.vue **/*.js -**/*.ts **/*.jsx **/*.tsx **/*.mjs diff --git a/hardhat.config.ts b/hardhat.config.ts index 72650e9a..1651408d 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -45,7 +45,7 @@ const config: HardhatUserConfig = { }, }, zksolc: { - version: "1.5.7", + version: "1.5.8", settings: { // https://era.zksync.io/docs/tools/hardhat/hardhat-zksync-solc.html#configuration // Native AA calls an internal system contract, so it needs extra permissions @@ -56,7 +56,7 @@ const config: HardhatUserConfig = { version: "0.8.28", settings: { evmVersion: "cancun", - } + }, }, }; diff --git a/scripts/deploy.ts b/scripts/deploy.ts index e0e2f1b1..98a43483 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -32,7 +32,6 @@ async function deploy(name: string, deployer: Wallet, proxy: boolean, args?: any return proxyAddress; } - task("deploy", "Deploys ZKsync SSO contracts") .addOptionalParam("only", "name of a specific contract to deploy") .addFlag("noProxy", "do not deploy transparent proxies for factory and modules") diff --git a/scripts/upgrade.ts b/scripts/upgrade.ts index 48b26e22..281834db 100644 --- a/scripts/upgrade.ts +++ b/scripts/upgrade.ts @@ -38,4 +38,4 @@ task("upgrade", "Upgrades ZKsync SSO contracts") const tx = await proxy.upgradeTo(await newImpl.getAddress()); await tx.wait(); console.log("Proxy upgraded successfully"); -}); + }); diff --git a/test/BasicTest.ts b/test/BasicTest.ts index b5f25ff2..d0757763 100644 --- a/test/BasicTest.ts +++ b/test/BasicTest.ts @@ -37,24 +37,30 @@ describe("Basic tests", function () { assert(aaFactoryContract != null, "No AA Factory deployed"); const factoryAddress = await aaFactoryContract.getAddress(); - expect(factoryAddress, "the factory address").to.equal(await fixtures.getAaFactoryAddress(), "factory address match"); + expect(factoryAddress, "the factory address").to.equal( + await fixtures.getAaFactoryAddress(), + "factory address match", + ); const bytecodeHash = await aaFactoryContract.beaconProxyBytecodeHash(); const deployedAccountContract = await fixtures.getAccountProxyContract(); const deployedAccountContractCode = await deployedAccountContract.getDeployedCode(); assert(deployedAccountContractCode != null, "No account code deployed"); const ssoBeaconBytecodeHash = ethers.hexlify(utils.hashBytecode(deployedAccountContractCode)); - expect(bytecodeHash, "deployed account bytecode hash").to.equal(ssoBeaconBytecodeHash, "deployed account code doesn't match"); + expect(bytecodeHash, "deployed account bytecode hash").to.equal( + ssoBeaconBytecodeHash, + "deployed account code doesn't match", + ); const args = await aaFactoryContract.getEncodedBeacon(); const deployedBeaconAddress = new ethers.AbiCoder().encode(["address"], [await fixtures.getBeaconAddress()]); expect(args, "the beacon address").to.equal(deployedBeaconAddress, "the deployment beacon"); - + const randomSalt = randomBytes(32); - const standardCreate2Address = utils.create2Address(factoryAddress, bytecodeHash, randomSalt, args) ; + const standardCreate2Address = utils.create2Address(factoryAddress, bytecodeHash, randomSalt, args); const preDeployAccountCode = await fixtures.wallet.provider.getCode(standardCreate2Address); - expect(preDeployAccountCode , "expected deploy location").to.equal("0x", "nothing deployed here (yet)"); + expect(preDeployAccountCode, "expected deploy location").to.equal("0x", "nothing deployed here (yet)"); const deployTx = await aaFactoryContract.deployProxySsoAccount( randomSalt, @@ -69,7 +75,10 @@ describe("Basic tests", function () { expect(postDeployAccountCode, "expected deploy location").to.not.equal("0x", "deployment didn't match create2!"); expect(proxyAccountAddress, "the proxy account location via logs").to.not.equal(ZeroAddress, "be a valid address"); - expect(proxyAccountAddress, "the proxy account location").to.equal(standardCreate2Address, "be what create2 returns"); + expect(proxyAccountAddress, "the proxy account location").to.equal( + standardCreate2Address, + "be what create2 returns", + ); const account = SsoAccount__factory.connect(proxyAccountAddress, provider); assert(await account.k1IsOwner(fixtures.wallet.address)); @@ -81,16 +90,19 @@ describe("Basic tests", function () { const balanceBefore = await provider.getBalance(proxyAccountAddress); - const smartAccount = new SmartAccount({ - address: proxyAccountAddress, - secret: fixtures.wallet.privateKey, - }, provider); + const smartAccount = new SmartAccount( + { + address: proxyAccountAddress, + secret: fixtures.wallet.privateKey, + }, + provider, + ); const value = parseEther("0.01"); const target = Wallet.createRandom().address; const aaTx = { - ...await aaTxTemplate(), + ...(await aaTxTemplate()), to: target, value, gasLimit: 300_000n, @@ -103,15 +115,21 @@ describe("Basic tests", function () { const tx = await provider.broadcastTransaction(signedTransaction); const receipt = await tx.wait(); const fee = receipt.gasUsed * aaTx.gasPrice; - expect(await provider.getBalance(proxyAccountAddress)).to.equal(balanceBefore - value - fee, "invalid final balance"); + expect(await provider.getBalance(proxyAccountAddress)).to.equal( + balanceBefore - value - fee, + "invalid final balance", + ); expect(await provider.getBalance(target)).to.equal(value, "invalid final balance"); }); it("should execute a multicall", async () => { - const smartAccount = new SmartAccount({ - address: proxyAccountAddress, - secret: fixtures.wallet.privateKey, - }, provider); + const smartAccount = new SmartAccount( + { + address: proxyAccountAddress, + secret: fixtures.wallet.privateKey, + }, + provider, + ); const balanceBefore = await provider.getBalance(proxyAccountAddress); const value = parseEther("0.01"); @@ -136,7 +154,7 @@ describe("Basic tests", function () { const account = SsoAccount__factory.connect(proxyAccountAddress, provider); const aaTx = { - ...await aaTxTemplate(), + ...(await aaTxTemplate()), to: proxyAccountAddress, data: account.interface.encodeFunctionData("batchCall", [calls]), value: value * 2n, @@ -152,7 +170,10 @@ describe("Basic tests", function () { const receipt = await tx.wait(); const fee = receipt.gasUsed * aaTx.gasPrice; - expect(await provider.getBalance(proxyAccountAddress)).to.equal(balanceBefore - value * 2n - fee, "invalid final own balance"); + expect(await provider.getBalance(proxyAccountAddress)).to.equal( + balanceBefore - value * 2n - fee, + "invalid final own balance", + ); expect(await provider.getBalance(target1)).to.equal(value, "invalid final target-1 balance"); expect(await provider.getBalance(target2)).to.equal(value, "invalid final target-2 balance"); }); diff --git a/test/PasskeyModule.ts b/test/PasskeyModule.ts index fc68d029..2d034dd4 100644 --- a/test/PasskeyModule.ts +++ b/test/PasskeyModule.ts @@ -19,21 +19,14 @@ import { getWallet, LOCAL_RICH_WALLETS, RecordedResponse } from "./utils"; * @param buffer Value to decode from base64 * @param to (optional) The decoding to use, in case it's desirable to decode from base64 instead */ -export function toBuffer( - base64urlString: string, - from: "base64" | "base64url" = "base64url", -): Uint8Array { +export function toBuffer(base64urlString: string, from: "base64" | "base64url" = "base64url"): Uint8Array { const _buffer = toArrayBuffer(base64urlString, from === "base64url"); return new Uint8Array(_buffer); } -async function deployValidator( - wallet: Wallet, -): Promise { +async function deployValidator(wallet: Wallet): Promise { const deployer: Deployer = new Deployer(hre, wallet); - const passkeyValidatorArtifact = await deployer.loadArtifact( - "WebAuthValidator", - ); + const passkeyValidatorArtifact = await deployer.loadArtifact("WebAuthValidator"); const validator = await deployer.deploy(passkeyValidatorArtifact, []); return WebAuthValidator__factory.connect(await validator.getAddress(), wallet); @@ -118,10 +111,7 @@ export function decodeFirst(input: Uint8Array): Type { return first; } -export function fromBuffer( - buffer: Uint8Array, - to: "base64" | "base64url" = "base64url", -): string { +export function fromBuffer(buffer: Uint8Array, to: "base64" | "base64url" = "base64url"): string { return fromArrayBuffer(buffer, to === "base64url"); } @@ -199,9 +189,7 @@ function shouldRemoveLeadingZero(bytes: Uint8Array): boolean { * Returns hash digest of the given data, using the given algorithm when provided. Defaults to using * SHA-256. */ -export async function toHash( - data: Uint8Array | string, -): Promise { +export async function toHash(data: Uint8Array | string): Promise { if (typeof data === "string") { data = new TextEncoder().encode(data); } @@ -214,7 +202,8 @@ async function rawVerify( authenticatorData: string, clientData: string, b64SignedChallange: string, - publicKeyEs256Bytes: Uint8Array) { + publicKeyEs256Bytes: Uint8Array, +) { const authDataBuffer = toBuffer(authenticatorData); const clientDataHash = await toHash(toBuffer(clientData)); const hashedData = await toHash(concat([authDataBuffer, clientDataHash])); @@ -232,22 +221,24 @@ describe("Passkey validation", function () { // 37 bytes const authenticatorData = "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABQ"; - const clientData = "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZFhPM3ctdWdycS00SkdkZUJLNDFsZFk1V2lNd0ZORDkiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUxNzMiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ"; - const b64SignedChallange = "MEUCIQCYrSUCR_QUPAhvRNUVfYiJC2JlOKuqf4gx7i129n9QxgIgaY19A9vAAObuTQNs5_V9kZFizwRpUFpiRVW_dglpR2A"; + const clientData = + "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZFhPM3ctdWdycS00SkdkZUJLNDFsZFk1V2lNd0ZORDkiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUxNzMiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ"; + const b64SignedChallange = + "MEUCIQCYrSUCR_QUPAhvRNUVfYiJC2JlOKuqf4gx7i129n9QxgIgaY19A9vAAObuTQNs5_V9kZFizwRpUFpiRVW_dglpR2A"; // this is a binary object formatted by @simplewebauthn that contains the alg type and public key const publicKeyEs256Bytes = new Uint8Array([ - 165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 167, 69, - 109, 166, 67, 163, 110, 143, 71, 60, 77, 232, 220, 7, - 121, 156, 141, 24, 71, 28, 210, 116, 124, 90, 115, 166, - 213, 190, 89, 4, 216, 128, 34, 88, 32, 193, 67, 151, - 85, 245, 24, 139, 246, 220, 204, 228, 76, 247, 65, 179, - 235, 81, 41, 196, 37, 216, 117, 201, 244, 128, 8, 73, - 37, 195, 20, 194, 9, + 165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 167, 69, 109, 166, 67, 163, 110, 143, 71, 60, 77, 232, 220, 7, 121, 156, 141, + 24, 71, 28, 210, 116, 124, 90, 115, 166, 213, 190, 89, 4, 216, 128, 34, 88, 32, 193, 67, 151, 85, 245, 24, 139, + 246, 220, 204, 228, 76, 247, 65, 179, 235, 81, 41, 196, 37, 216, 117, 201, 244, 128, 8, 73, 37, 195, 20, 194, 9, ]); const verifyMessage = await rawVerify( passkeyValidator, - authenticatorData, clientData, b64SignedChallange, publicKeyEs256Bytes); + authenticatorData, + clientData, + b64SignedChallange, + publicKeyEs256Bytes, + ); assert(verifyMessage == true, "valid sig"); }); @@ -260,7 +251,8 @@ describe("Passkey validation", function () { ethersResponse.authenticatorData, ethersResponse.clientData, ethersResponse.b64SignedChallenge, - ethersResponse.passkeyBytes); + ethersResponse.passkeyBytes, + ); assert(verifyMessage == true, "test sig is valid"); }); @@ -268,13 +260,15 @@ describe("Passkey validation", function () { it("should fail when signature is bad", async function () { const passkeyValidator = await deployValidator(wallet); - const b64SignedChallenge = "MEUCIQCYrSUCR_QUPAhvRNUVfYiJC2JlOKuqf4gx7i129n9QxgIgaY19A9vAAObuTQNs5_V9kZFizwRpUFpiRVW_dglpR2A"; + const b64SignedChallenge = + "MEUCIQCYrSUCR_QUPAhvRNUVfYiJC2JlOKuqf4gx7i129n9QxgIgaY19A9vAAObuTQNs5_V9kZFizwRpUFpiRVW_dglpR2A"; const verifyMessage = await rawVerify( passkeyValidator, ethersResponse.authenticatorData, ethersResponse.clientData, b64SignedChallenge, - ethersResponse.passkeyBytes); + ethersResponse.passkeyBytes, + ); assert(verifyMessage == false, "bad sig should be false"); }); diff --git a/test/SessionKeyTest.ts b/test/SessionKeyTest.ts index 3e57b704..9a565825 100644 --- a/test/SessionKeyTest.ts +++ b/test/SessionKeyTest.ts @@ -144,31 +144,41 @@ class SessionTester { // having this is a bit hacky, but it's so we can provide correct period ids in the signature aaTransaction: ethers.TransactionLike; - constructor(public proxyAccountAddress: string, sessionKeyModuleAddress: string) { + constructor( + public proxyAccountAddress: string, + sessionKeyModuleAddress: string, + ) { this.sessionOwner = new Wallet(Wallet.createRandom().privateKey, provider); - this.sessionAccount = new SmartAccount({ - payloadSigner: async (hash) => abiCoder.encode( - ["bytes", "address", "bytes"], - [ - this.sessionOwner.signingKey.sign(hash).serialized, - sessionKeyModuleAddress, + this.sessionAccount = new SmartAccount( + { + payloadSigner: async (hash) => abiCoder.encode( - [sessionSpecAbi, "uint64[]"], - [this.session, await this.periodIds(this.aaTransaction.to!, this.aaTransaction.data?.slice(0, 10))], + ["bytes", "address", "bytes"], + [ + this.sessionOwner.signingKey.sign(hash).serialized, + sessionKeyModuleAddress, + abiCoder.encode( + [sessionSpecAbi, "uint64[]"], + [this.session, await this.periodIds(this.aaTransaction.to!, this.aaTransaction.data?.slice(0, 10))], + ), + ], ), - ], - ), - address: this.proxyAccountAddress, - secret: this.sessionOwner.privateKey, - }, provider); + address: this.proxyAccountAddress, + secret: this.sessionOwner.privateKey, + }, + provider, + ); } async createSession(newSession: PartialSession) { const sessionKeyModuleContract = await fixtures.getSessionKeyContract(); - const smartAccount = new SmartAccount({ - address: this.proxyAccountAddress, - secret: fixtures.wallet.privateKey, - }, provider); + const smartAccount = new SmartAccount( + { + address: this.proxyAccountAddress, + secret: fixtures.wallet.privateKey, + }, + provider, + ); this.session = this.getSession(newSession); @@ -176,9 +186,9 @@ class SessionTester { expect(oldState.status).to.equal(0, "session should not exist yet"); const aaTx = { - ...await this.aaTxTemplate(), + ...(await this.aaTxTemplate()), to: await sessionKeyModuleContract.getAddress(), - data: sessionKeyModuleContract.interface.encodeFunctionData("createSession", [this.session]) + data: sessionKeyModuleContract.interface.encodeFunctionData("createSession", [this.session]), }; aaTx.gasLimit = await provider.estimateGas(aaTx); @@ -208,7 +218,9 @@ class SessionTester { const isTransfer = selector == null || ethers.getBytes(selector).length < 4; const policy: SessionLib.CallSpecStruct | SessionLib.TransferSpecStruct | undefined = isTransfer ? this.session.transferPolicies.find((policy) => policy.target == target) - : this.session.callPolicies.find((policy) => policy.target == target && ethers.hexlify(policy.selector) == selector); + : this.session.callPolicies.find( + (policy) => policy.target == target && ethers.hexlify(policy.selector) == selector, + ); if (policy == null) { throw new Error("Transaction does not fit any policy"); @@ -217,7 +229,9 @@ class SessionTester { const periodIds = [ getId(this.session.feeLimit), getId(policy.valueLimit), - ...(isTransfer ? [] : (policy).constraints.map((constraint) => getId(constraint.limit))), + ...(isTransfer + ? [] + : (policy).constraints.map((constraint) => getId(constraint.limit))), ]; return periodIds; } @@ -227,15 +241,18 @@ class SessionTester { const oldState = await sessionKeyModuleContract.sessionState(this.proxyAccountAddress, this.session); expect(oldState.status).to.equal(1, "session should be active"); - const smartAccount = new SmartAccount({ - address: this.proxyAccountAddress, - secret: fixtures.wallet.privateKey, - }, provider); + const smartAccount = new SmartAccount( + { + address: this.proxyAccountAddress, + secret: fixtures.wallet.privateKey, + }, + provider, + ); const sessionHash = ethers.keccak256(this.encodeSession()); const aaTx = { - ...await this.aaTxTemplate(), + ...(await this.aaTxTemplate()), to: await sessionKeyModuleContract.getAddress(), data: sessionKeyModuleContract.interface.encodeFunctionData("revokeKey", [sessionHash]), }; @@ -250,7 +267,7 @@ class SessionTester { async sendTxSuccess(txRequest: ethers.TransactionLike = {}) { this.aaTransaction = { - ...await this.aaTxTemplate(await this.periodIds(txRequest.to!, txRequest.data?.slice(0, 10))), + ...(await this.aaTxTemplate(await this.periodIds(txRequest.to!, txRequest.data?.slice(0, 10)))), ...txRequest, }; this.aaTransaction.gasLimit = await provider.estimateGas(this.aaTransaction); @@ -264,14 +281,14 @@ class SessionTester { async sendTxFail(tx: ethers.TransactionLike = {}) { this.aaTransaction = { - ...await this.aaTxTemplate(await this.periodIds(tx.to!, tx.data?.slice(0, 10))), + ...(await this.aaTxTemplate(await this.periodIds(tx.to!, tx.data?.slice(0, 10)))), gasLimit: 100_000_000n, ...tx, }; const signedTransaction = await this.sessionAccount.signTransaction(this.aaTransaction); await expect(provider.broadcastTransaction(signedTransaction)).to.be.reverted; - }; + } _getLimit(limit?: PartialLimit): SessionLib.UsageLimitStruct { return SessionTester.getLimit(limit); @@ -280,48 +297,49 @@ class SessionTester { static getLimit(limit?: PartialLimit): SessionLib.UsageLimitStruct { return limit == null ? { - limitType: LimitType.Unlimited, - limit: 0, - period: 0, - } - : limit.period == null - ? { - limitType: LimitType.Lifetime, - limit: limit.limit, + limitType: LimitType.Unlimited, + limit: 0, period: 0, } + : limit.period == null + ? { + limitType: LimitType.Lifetime, + limit: limit.limit, + period: 0, + } : { - limitType: LimitType.Allowance, - limit: limit.limit, - period: limit.period, - }; + limitType: LimitType.Allowance, + limit: limit.limit, + period: limit.period, + }; } - - getSession(session: PartialSession): SessionLib.SessionSpecStruct { return { signer: this.sessionOwner.address, expiresAt: session.expiresAt ?? Math.floor(Date.now() / 1000) + 60 * 60 * 24, // unlimited fees are not safe feeLimit: session.feeLimit ? this._getLimit(session.feeLimit) : this._getLimit({ limit: parseEther("0.1") }), - callPolicies: session.callPolicies?.map((policy) => ({ - target: policy.target, - selector: policy.selector ?? "0x00000000", - maxValuePerUse: policy.maxValuePerUse ?? 0, - valueLimit: this._getLimit(policy.valueLimit), - constraints: policy.constraints?.map((constraint) => ({ - condition: constraint.condition ?? 0, - index: constraint.index, - refValue: constraint.refValue ?? ethers.ZeroHash, - limit: this._getLimit(constraint.limit), + callPolicies: + session.callPolicies?.map((policy) => ({ + target: policy.target, + selector: policy.selector ?? "0x00000000", + maxValuePerUse: policy.maxValuePerUse ?? 0, + valueLimit: this._getLimit(policy.valueLimit), + constraints: + policy.constraints?.map((constraint) => ({ + condition: constraint.condition ?? 0, + index: constraint.index, + refValue: constraint.refValue ?? ethers.ZeroHash, + limit: this._getLimit(constraint.limit), + })) ?? [], + })) ?? [], + transferPolicies: + session.transferPolicies?.map((policy) => ({ + target: policy.target, + maxValuePerUse: policy.maxValuePerUse ?? 0, + valueLimit: this._getLimit(policy.valueLimit), })) ?? [], - })) ?? [], - transferPolicies: session.transferPolicies?.map((policy) => ({ - target: policy.target, - maxValuePerUse: policy.maxValuePerUse ?? 0, - valueLimit: this._getLimit(policy.valueLimit), - })) ?? [], }; } @@ -336,17 +354,16 @@ class SessionTester { gasPrice: await provider.getGasPrice(), customData: { gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, - customSignature: periodIds ? abiCoder.encode( - ["bytes", "address", "bytes"], - [ - ethers.zeroPadValue("0x1b", 65), - await fixtures.getSessionKeyModuleAddress(), - abiCoder.encode( - [sessionSpecAbi, "uint64[]"], - [this.session, periodIds], - ), - ], - ) : undefined, + customSignature: periodIds + ? abiCoder.encode( + ["bytes", "address", "bytes"], + [ + ethers.zeroPadValue("0x1b", 65), + await fixtures.getSessionKeyModuleAddress(), + abiCoder.encode([sessionSpecAbi, "uint64[]"], [this.session, periodIds]), + ], + ) + : undefined, }, gasLimit: 0n, }; @@ -400,15 +417,19 @@ describe("SessionKeyModule tests", function () { const randomSalt = randomBytes(32); const bytecodeHash = await factoryContract.beaconProxyBytecodeHash(); const factoryAddress = await factoryContract.getAddress(); - const standardCreate2Address = utils.create2Address(factoryAddress, bytecodeHash, randomSalt, args) ; + const standardCreate2Address = utils.create2Address(factoryAddress, bytecodeHash, randomSalt, args); let tester = new SessionTester(standardCreate2Address, await fixtures.getSessionKeyModuleAddress()); const initialSession = await tester.getSession({ - transferPolicies: [{ + transferPolicies: [ + { target: transferSessionTarget, maxValuePerUse: parseEther("0.01"), - }], - }); - const initSessionData = abiCoder.encode(sessionKeyModuleContract.interface.getFunction("createSession").inputs, [initialSession]); + }, + ], + }); + const initSessionData = abiCoder.encode(sessionKeyModuleContract.interface.getFunction("createSession").inputs, [ + initialSession, + ]); const sessionKeyPayload = abiCoder.encode(["address", "bytes"], [sessionKeyModuleAddress, initSessionData]); const deployTx = await factoryContract.deployProxySsoAccount( @@ -432,7 +453,7 @@ describe("SessionKeyModule tests", function () { const account = SsoAccount__factory.connect(proxyAccountAddress, provider); assert(await account.k1IsOwner(fixtures.wallet.address)); - assert(!await account.isHook(sessionKeyModuleAddress), "session key module should not be an execution hook"); + assert(!(await account.isHook(sessionKeyModuleAddress)), "session key module should not be an execution hook"); assert(await account.isModuleValidator(sessionKeyModuleAddress), "session key module should be a validator"); }); @@ -443,10 +464,12 @@ describe("SessionKeyModule tests", function () { it("should create a session", async () => { tester = new SessionTester(proxyAccountAddress, await fixtures.getSessionKeyModuleAddress()); await tester.createSession({ - transferPolicies: [{ - target: sessionTarget, - maxValuePerUse: parseEther("0.01"), - }], + transferPolicies: [ + { + target: sessionTarget, + maxValuePerUse: parseEther("0.01"), + }, + ], }); }); @@ -455,8 +478,10 @@ describe("SessionKeyModule tests", function () { to: sessionTarget, value: parseEther("0.01"), }); - expect(await provider.getBalance(sessionTarget)) - .to.equal(parseEther("0.01"), "session target should have received the funds"); + expect(await provider.getBalance(sessionTarget)).to.equal( + parseEther("0.01"), + "session target should have received the funds", + ); }); it("should reject a session key transaction that goes over limit", async () => { @@ -465,7 +490,6 @@ describe("SessionKeyModule tests", function () { value: parseEther("0.02"), }); }); - }); describe("ERC20 transfer limit tests", function () { @@ -481,26 +505,28 @@ describe("SessionKeyModule tests", function () { it("should create a session", async () => { tester = new SessionTester(proxyAccountAddress, await fixtures.getSessionKeyModuleAddress()); await tester.createSession({ - callPolicies: [{ - target: await erc20.getAddress(), - selector: erc20.interface.getFunction("transfer").selector, - constraints: [ - // can only transfer to sessionTarget - { - index: 0, - refValue: ethers.zeroPadValue(sessionTarget, 32), - condition: Condition.Equal, - }, - // can only transfer upto 1000 tokens per tx - // can only transfer upto 1500 tokens in total - { - index: 1, - refValue: ethers.toBeHex(1000, 32), - condition: Condition.LessEqual, - limit: { limit: 1500 }, - }, - ], - }], + callPolicies: [ + { + target: await erc20.getAddress(), + selector: erc20.interface.getFunction("transfer").selector, + constraints: [ + // can only transfer to sessionTarget + { + index: 0, + refValue: ethers.zeroPadValue(sessionTarget, 32), + condition: Condition.Equal, + }, + // can only transfer upto 1000 tokens per tx + // can only transfer upto 1500 tokens in total + { + index: 1, + refValue: ethers.toBeHex(1000, 32), + condition: Condition.LessEqual, + limit: { limit: 1500 }, + }, + ], + }, + ], }); }); @@ -523,8 +549,7 @@ describe("SessionKeyModule tests", function () { to: await erc20.getAddress(), data: erc20.interface.encodeFunctionData("transfer", [sessionTarget, 1000n]), }); - expect(await erc20.balanceOf(sessionTarget)) - .to.equal(1000n, "session target should have received the tokens"); + expect(await erc20.balanceOf(sessionTarget)).to.equal(1000n, "session target should have received the tokens"); }); it("should reject a session key transaction that goes over total limit", async () => { @@ -558,15 +583,17 @@ describe("SessionKeyModule tests", function () { it("should create a session", async () => { tester = new SessionTester(proxyAccountAddress, await fixtures.getSessionKeyModuleAddress()); await tester.createSession({ - expiresAt: await getTimestamp() + period * 3, - transferPolicies: [{ - target: sessionTarget, - maxValuePerUse: parseEther("0.01"), - valueLimit: { - limit: parseEther("0.015"), - period, + expiresAt: (await getTimestamp()) + period * 3, + transferPolicies: [ + { + target: sessionTarget, + maxValuePerUse: parseEther("0.01"), + valueLimit: { + limit: parseEther("0.015"), + period, + }, }, - }], + ], }); }); diff --git a/test/utils.ts b/test/utils.ts index aca4b70b..e3372682 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -9,15 +9,30 @@ import * as hre from "hardhat"; import { ContractFactory, Provider, utils, Wallet } from "zksync-ethers"; import { base64UrlToUint8Array, getPublicKeyBytesFromPasskeySignature, unwrapEC2Signature } from "zksync-sso/utils"; -import { AAFactory, ERC20, ExampleAuthServerPaymaster, SessionKeyValidator, SsoAccount, WebAuthValidator, SsoBeacon, AccountProxy__factory, AccountProxy } from "../typechain-types"; -import { AAFactory__factory, ERC20__factory, ExampleAuthServerPaymaster__factory, SessionKeyValidator__factory, SsoAccount__factory, WebAuthValidator__factory, SsoBeacon__factory } from "../typechain-types"; +import { + AAFactory, + ERC20, + ExampleAuthServerPaymaster, + SessionKeyValidator, + SsoAccount, + WebAuthValidator, + SsoBeacon, + AccountProxy__factory, + AccountProxy, +} from "../typechain-types"; +import { + AAFactory__factory, + ERC20__factory, + ExampleAuthServerPaymaster__factory, + SessionKeyValidator__factory, + SsoAccount__factory, + WebAuthValidator__factory, + SsoBeacon__factory, +} from "../typechain-types"; export const ethersStaticSalt = new Uint8Array([ - 205, 241, 161, 186, 101, 105, 79, - 248, 98, 64, 50, 124, 168, 204, - 200, 71, 214, 169, 195, 118, 199, - 62, 140, 111, 128, 47, 32, 21, - 177, 177, 174, 166, + 205, 241, 161, 186, 101, 105, 79, 248, 98, 64, 50, 124, 168, 204, 200, 71, 214, 169, 195, 118, 199, 62, 140, 111, 128, + 47, 32, 21, 177, 177, 174, 166, ]); export class ContractFixtures { @@ -108,15 +123,10 @@ export class ContractFixtures { aaFactoryAddress: string, sessionKeyValidatorAddress: string, ): Promise { - const contract = await create2( - "ExampleAuthServerPaymaster", - this.wallet, - ethersStaticSalt, - [ - aaFactoryAddress, - sessionKeyValidatorAddress, - ], - ); + const contract = await create2("ExampleAuthServerPaymaster", this.wallet, ethersStaticSalt, [ + aaFactoryAddress, + sessionKeyValidatorAddress, + ]); const paymasterAddress = ExampleAuthServerPaymaster__factory.connect(await contract.getAddress(), this.wallet); // Fund the paymaster with 1 ETH @@ -136,7 +146,8 @@ dotenv.config(); export const getProvider = () => { const rpcUrl = hre.network.config["url"]; - if (!rpcUrl) throw `⛔️ RPC URL wasn't found in "${hre.network.name}"! Please add a "url" field to the network config in hardhat.config.ts`; + if (!rpcUrl) + throw `⛔️ RPC URL wasn't found in "${hre.network.name}"! Please add a "url" field to the network config in hardhat.config.ts`; // Initialize ZKsync Provider const provider = new Provider(rpcUrl); @@ -154,27 +165,36 @@ export const getProviderL1 = () => { return provider; }; -export async function deployFactory(wallet: Wallet, beaconAddress: string, salt?: ethers.BytesLike): Promise { +export async function deployFactory( + wallet: Wallet, + beaconAddress: string, + salt?: ethers.BytesLike, +): Promise { const factoryArtifact = JSON.parse(await promises.readFile("artifacts-zk/src/AAFactory.sol/AAFactory.json", "utf8")); - const proxyAaArtifact = JSON.parse(await promises.readFile("artifacts-zk/src/AccountProxy.sol/AccountProxy.json", "utf8")); + const proxyAaArtifact = JSON.parse( + await promises.readFile("artifacts-zk/src/AccountProxy.sol/AccountProxy.json", "utf8"), + ); const deployer = new ContractFactory(factoryArtifact.abi, factoryArtifact.bytecode, wallet, "create2"); const bytecodeHash = utils.hashBytecode(proxyAaArtifact.bytecode); const factoryBytecodeHash = utils.hashBytecode(factoryArtifact.bytecode); const factorySalt = ethers.hexlify(salt ?? randomBytes(32)); const constructorArgs = deployer.interface.encodeDeploy([bytecodeHash, beaconAddress]); - const standardCreate2Address = utils.create2Address(wallet.address, factoryBytecodeHash, factorySalt, constructorArgs); + const standardCreate2Address = utils.create2Address( + wallet.address, + factoryBytecodeHash, + factorySalt, + constructorArgs, + ); const accountCode = await wallet.provider.getCode(standardCreate2Address); if (accountCode != "0x") { logInfo(`Factory already exists!`); return AAFactory__factory.connect(standardCreate2Address, wallet); } logInfo(`Factory doesn't exist at ${standardCreate2Address}!`); - const factory = await deployer.deploy( - bytecodeHash, - beaconAddress, - { customData: { salt: factorySalt, factoryDeps: [proxyAaArtifact.bytecode] } }, - ); + const factory = await deployer.deploy(bytecodeHash, beaconAddress, { + customData: { salt: factorySalt, factoryDeps: [proxyAaArtifact.bytecode] }, + }); const factoryAddress = await factory.getAddress(); if (hre.network.config.verifyURL) { @@ -209,7 +229,8 @@ export const getWallet = (privateKey?: string) => { export const verifyEnoughBalance = async (wallet: Wallet, amount: bigint) => { // Check if the wallet has enough balance const balance = await wallet.getBalance(); - if (balance < amount) throw `Wallet balance is too low! Required ${ethers.formatEther(amount)} ETH, but current ${wallet.address} balance is ${ethers.formatEther(balance)} ETH`; + if (balance < amount) + throw `Wallet balance is too low! Required ${ethers.formatEther(amount)} ETH, but current ${wallet.address} balance is ${ethers.formatEther(balance)} ETH`; }; /** @@ -228,20 +249,32 @@ export const verifyContract = async (data: { return verificationRequestId; }; -export const create2 = async (contractName: string, wallet: Wallet, salt: ethers.BytesLike, args?: ReadonlyArray) => { +export const create2 = async ( + contractName: string, + wallet: Wallet, + salt: ethers.BytesLike, + args?: ReadonlyArray, +) => { salt = ethers.hexlify(salt); const contractArtifact = await hre.artifacts.readArtifact(contractName); const deployer = new ContractFactory(contractArtifact.abi, contractArtifact.bytecode, wallet, "create2"); const bytecodeHash = utils.hashBytecode(contractArtifact.bytecode); const constructorArgs = deployer.interface.encodeDeploy(args); - const standardCreate2Address = utils.create2Address(wallet.address, bytecodeHash, salt, args ? constructorArgs : "0x"); + const standardCreate2Address = utils.create2Address( + wallet.address, + bytecodeHash, + salt, + args ? constructorArgs : "0x", + ); const accountCode = await wallet.provider.getCode(standardCreate2Address); if (accountCode != "0x") { logInfo(`Contract ${contractName} already exists!`); return new ethers.Contract(standardCreate2Address, contractArtifact.abi, wallet); } - const deployingContract = await (args ? deployer.deploy(...args, { customData: { salt } }) : deployer.deploy({ customData: { salt } })); + const deployingContract = await (args + ? deployer.deploy(...args, { customData: { salt } }) + : deployer.deploy({ customData: { salt } })); const deployedContract = await deployingContract.waitForDeployment(); const deployedContractAddress = await deployedContract.getAddress(); logInfo(`"${contractName}" was successfully deployed to ${deployedContractAddress}`); @@ -274,7 +307,9 @@ export function logWarning(message: string) { console.log("\x1b[33m%s\x1b[0m", message); } -const masterWallet = ethers.Wallet.fromPhrase("stuff slice staff easily soup parent arm payment cotton trade scatter struggle"); +const masterWallet = ethers.Wallet.fromPhrase( + "stuff slice staff easily soup parent arm payment cotton trade scatter struggle", +); /** * Rich wallets can be used for testing purposes. @@ -283,13 +318,13 @@ const masterWallet = ethers.Wallet.fromPhrase("stuff slice staff easily soup par export const LOCAL_RICH_WALLETS = [ hre.network.name == "dockerizedNode" ? { - address: masterWallet.address, - privateKey: masterWallet.privateKey, - } + address: masterWallet.address, + privateKey: masterWallet.privateKey, + } : { - address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", - }, + address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + }, { address: "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049", privateKey: "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110", @@ -332,9 +367,7 @@ export const LOCAL_RICH_WALLETS = [ }, ]; -const convertObjArrayToUint8Array = (objArray: { - [key: string]: number; -}): Uint8Array => { +const convertObjArrayToUint8Array = (objArray: { [key: string]: number }): Uint8Array => { const objEntries = Object.entries(objArray); return objEntries.reduce((existingArray, nextKv) => { const index = parseInt(nextKv[0]); @@ -364,9 +397,15 @@ export class RecordedResponse { return getPublicKeyBytesFromPasskeySignature(this.passkeyBytes); } - get authDataBuffer() { return base64UrlToUint8Array(this.authenticatorData); } - get clientDataBuffer() { return base64UrlToUint8Array(this.clientData); } - get rs() { return unwrapEC2Signature(base64UrlToUint8Array(this.b64SignedChallenge)); } + get authDataBuffer() { + return base64UrlToUint8Array(this.authenticatorData); + } + get clientDataBuffer() { + return base64UrlToUint8Array(this.clientData); + } + get rs() { + return unwrapEC2Signature(base64UrlToUint8Array(this.b64SignedChallenge)); + } // this is the encoded data explaining what authenticator was used (fido, web, etc) readonly authenticatorData: string;