diff --git a/src/helpers/Logger.sol b/src/helpers/Logger.sol index 59839c0f..78e458f3 100644 --- a/src/helpers/Logger.sol +++ b/src/helpers/Logger.sol @@ -18,6 +18,12 @@ library Logger { } } + function logBytes4(bytes4 bytesToLog) internal view { + if (block.chainid == 260) { + console.logBytes4(bytesToLog); + } + } + function logBytes32(bytes32 bytesToLog) internal view { if (block.chainid == 260) { console.logBytes32(bytesToLog); diff --git a/src/interfaces/IHook.sol b/src/interfaces/IHook.sol index ae0d15cc..40045456 100644 --- a/src/interfaces/IHook.sol +++ b/src/interfaces/IHook.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 + pragma solidity ^0.8.24; import { Transaction } from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol"; diff --git a/src/interfaces/IHookManager.sol b/src/interfaces/IHookManager.sol index 5a868479..0e478dee 100644 --- a/src/interfaces/IHookManager.sol +++ b/src/interfaces/IHookManager.sol @@ -22,28 +22,25 @@ interface IHookManager { * @notice Add a hook to the list of hooks and call it's init function * @dev Can only be called by self * @param hook - Address of the hook - * @param isValidation bool - True if the hook is a validation hook, false otherwise * @param initData bytes calldata - Data to pass to the hook's `onInstall` function */ - function addHook(address hook, bool isValidation, bytes calldata initData) external; + function addHook(address hook, bytes calldata initData) external; /** * @notice Remove a hook from the list of hooks * @dev Can only be called by self * @param hook address - Address of the hook to remove - * @param isValidation bool - True if the hook is a validation hook, false otherwise * @param deinitData bytes calldata - Data to pass to the hook's `onUninstall` function */ - function removeHook(address hook, bool isValidation, bytes calldata deinitData) external; + function removeHook(address hook, bytes calldata deinitData) external; /** * @notice Remove a hook from the list of hooks while ignoring reverts from its `onUninstall` teardown function * @dev Can only be called by self * @param hook address - Address of the hook to remove - * @param isValidation bool - True if the hook is a validation hook, false otherwise * @param deinitData bytes calldata - Data to pass to the hook's `onUninstall` function */ - function unlinkHook(address hook, bool isValidation, bytes calldata deinitData) external; + function unlinkHook(address hook, bytes calldata deinitData) external; /** * @notice Check if an address is in the list of hooks diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 8a85aa50..8f8ac0b6 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -36,7 +36,7 @@ library Errors { //////////////////////////////////////////////////////////////*/ error EMPTY_HOOK_ADDRESS(uint256 hookAndDataLength); - error HOOK_ERC165_FAIL(address hookAddress, bool isValidation); + error HOOK_ERC165_FAIL(address hookAddress); error INVALID_KEY(bytes32 key); /*////////////////////////////////////////////////////////////// diff --git a/src/managers/HookManager.sol b/src/managers/HookManager.sol index 70dccef8..5e4cab1a 100644 --- a/src/managers/HookManager.sol +++ b/src/managers/HookManager.sol @@ -16,7 +16,6 @@ import { IModule } from "../interfaces/IModule.sol"; /** * @title Manager contract for hooks * @notice Abstract contract for managing the enabled hooks of the account - * @dev Hook addresses are stored in a linked list * @author https://getclave.io */ abstract contract HookManager is IHookManager, Auth { @@ -27,19 +26,20 @@ abstract contract HookManager is IHookManager, Auth { using ExcessivelySafeCall for address; /// @inheritdoc IHookManager - function addHook(address hook, bool isValidation, bytes calldata initData) external override onlySelf { - _addHook(hook, isValidation, initData); + function addHook(address hook, bytes calldata initData) external override onlySelf { + _addHook(hook); + IModule(hook).onInstall(initData); } /// @inheritdoc IHookManager - function removeHook(address hook, bool isValidation, bytes calldata deinitData) external override onlySelf { - _removeHook(hook, isValidation); + function removeHook(address hook, bytes calldata deinitData) external override onlySelf { + _removeHook(hook); IModule(hook).onUninstall(deinitData); } /// @inheritdoc IHookManager - function unlinkHook(address hook, bool isValidation, bytes calldata deinitData) external onlySelf { - _removeHook(hook, isValidation); + function unlinkHook(address hook, bytes calldata deinitData) external onlySelf { + _removeHook(hook); hook.excessivelySafeCall(gasleft(), 0, abi.encodeWithSelector(IModule.onUninstall.selector, deinitData)); } @@ -89,27 +89,29 @@ abstract contract HookManager is IHookManager, Auth { } } - function _addHook(address hook, bool isValidation, bytes calldata initData) internal { - if (!_supportsHook(hook, isValidation)) { - revert Errors.HOOK_ERC165_FAIL(hook, isValidation); + function _addHook(address hook) internal { + bool isExecutionHook = hook.supportsInterface(type(IExecutionHook).interfaceId); + bool isValidationHook = hook.supportsInterface(type(IValidationHook).interfaceId); + if (!isExecutionHook && !isValidationHook) { + revert Errors.HOOK_ERC165_FAIL(hook); } - - if (isValidation) { - _validationHooks().add(hook); - } else { - _executionHooks().add(hook); + if (isValidationHook) { + require(_validationHooks().add(hook), "Validation hook already exists"); + } + if (isExecutionHook) { + require(_executionHooks().add(hook), "Execution hook already exists"); } - - IModule(hook).onInstall(initData); emit HookAdded(hook); } - function _removeHook(address hook, bool isValidation) internal { - if (isValidation) { - _validationHooks().remove(hook); - } else { - _executionHooks().remove(hook); + function _removeHook(address hook) internal { + if (_validationHooks().contains(hook)) { + require(_validationHooks().remove(hook), "Validation hook not found"); + } + + if (_executionHooks().contains(hook)) { + require(_executionHooks().remove(hook), "Execution hook not found"); } emit HookRemoved(hook); @@ -132,14 +134,4 @@ abstract contract HookManager is IHookManager, Auth { function _executionHooks() private view returns (EnumerableSet.AddressSet storage executionHooks) { executionHooks = SsoStorage.layout().executionHooks; } - - function _supportsHook(address hook, bool isValidation) private view returns (bool) { - return - hook.supportsInterface(type(IModule).interfaceId) && - ( - isValidation - ? hook.supportsInterface(type(IValidationHook).interfaceId) - : hook.supportsInterface(type(IExecutionHook).interfaceId) - ); - } } diff --git a/src/test/SampleHooks.sol b/src/test/SampleHooks.sol new file mode 100644 index 00000000..d8e87e40 --- /dev/null +++ b/src/test/SampleHooks.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { Transaction } from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol"; + +import { IModule } from "../interfaces/IModule.sol"; +import { IExecutionHook, IValidationHook } from "../interfaces/IHook.sol"; + +abstract contract BaseHookValidator is IValidationHook { + /// @notice Emitted during validation + event Validating(address indexed accountAddress); + + function onInstall(bytes calldata data) external override {} + function onUninstall(bytes calldata data) external override {} + function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { + return + interfaceId == type(IValidationHook).interfaceId || + interfaceId == type(IModule).interfaceId || + interfaceId == type(IERC165).interfaceId; + } +} + +abstract contract BaseHookExecution is IExecutionHook { + /// @notice Emitted during execution + event Preexecute(address indexed accountAddress); + event Postexecute(address indexed accountAddress); + + function onInstall(bytes calldata data) external override {} + function onUninstall(bytes calldata data) external override {} + function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { + return + interfaceId == type(IExecutionHook).interfaceId || + interfaceId == type(IModule).interfaceId || + interfaceId == type(IERC165).interfaceId; + } +} + +/// @title FailHookValidator +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @dev Sample hook validator that always fails (BURNS THE ACCOUNT) +contract FailHookValidator is BaseHookValidator { + function validationHook(bytes32, Transaction calldata) external override { + emit Validating(msg.sender); + require(false, "SampleHookValidator: validationHook failed"); + } +} + +/// @title SuccessHookValidator +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @dev Sample hook validator that always passes +contract SuccessHookValidator is BaseHookValidator { + function validationHook(bytes32, Transaction calldata) external override { + emit Validating(msg.sender); + } +} + +/// @title PreFailHookExecutor +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @dev Sample hook executor that always fails on pre-execute (BURNS THE ACCOUNT) +contract PreFailHookExecutor is BaseHookExecution { + function preExecutionHook(Transaction calldata) external override returns (bytes memory _context) { + emit Preexecute(msg.sender); + require(false, "SampleHookExecutor: execution hook failed"); + } + + function postExecutionHook() external override { + emit Postexecute(msg.sender); + } +} + +/// @title PostFailHookExecutor +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @dev Sample hook executor that always fails on post-execute (BURNS THE ACCOUNT) +contract PostFailHookExecutor is BaseHookExecution { + function preExecutionHook(Transaction calldata) external override returns (bytes memory _context) { + emit Preexecute(msg.sender); + } + + function postExecutionHook() external override { + emit Postexecute(msg.sender); + require(false, "SampleHookExecutor: executionHook failed"); + } +} + +/// @title SuccessHookExecutor +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @dev Sample hook validator that always passes +contract SuccessHookExecutor is BaseHookExecution { + function preExecutionHook(Transaction calldata) external override returns (bytes memory _context) { + emit Preexecute(msg.sender); + } + + function postExecutionHook() external override { + emit Postexecute(msg.sender); + } +} + +/// @title SuccessBothHookExecutor +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @dev Sample hook execution and validator that always passes +contract SuccessBothHook is IExecutionHook, IValidationHook { + /// @notice Emitted during execution + event Preexecute(address indexed accountAddress); + event Postexecute(address indexed accountAddress); + /// @notice Emitted during validation + event Validating(address indexed accountAddress); + + function onInstall(bytes calldata data) external {} + function onUninstall(bytes calldata data) external {} + function supportsInterface(bytes4 interfaceId) external pure returns (bool) { + return + interfaceId == type(IValidationHook).interfaceId || + interfaceId == type(IExecutionHook).interfaceId || + interfaceId == type(IModule).interfaceId || + interfaceId == type(IERC165).interfaceId; + } + + function preExecutionHook(Transaction calldata) external override returns (bytes memory _context) { + emit Preexecute(msg.sender); + } + + function postExecutionHook() external override { + emit Postexecute(msg.sender); + } + + function validationHook(bytes32, Transaction calldata) external override { + emit Validating(msg.sender); + } +} diff --git a/test/HookCoverage.ts b/test/HookCoverage.ts new file mode 100644 index 00000000..ea650e00 --- /dev/null +++ b/test/HookCoverage.ts @@ -0,0 +1,209 @@ +// Copyright 2025 cbe +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { assert, expect } from "chai"; +import { randomBytes } from "crypto"; +import { parseEther, ZeroAddress } from "ethers"; +import { SmartAccount, utils } from "zksync-ethers"; + +import { BaseHookExecution__factory, BaseHookValidator__factory, FailHookValidator, SsoAccount__factory, SuccessHookExecutor } from "../typechain-types"; +import { ContractFixtures, create2, ethersStaticSalt, getProvider, getWallet, LOCAL_RICH_WALLETS, logInfo } from "./utils"; + +describe.only("Hook coverage", function () { + const ownerWallet = getWallet(LOCAL_RICH_WALLETS[0].privateKey); + const provider = getProvider(); + const ssoAbi = SsoAccount__factory.createInterface(); + const fixtures = new ContractFixtures(); + async function aaTxTemplate(accountAddress: string) { + return { + type: 113, + from: accountAddress, + data: "0x", + value: 0, + chainId: (await provider.getNetwork()).chainId, + nonce: await provider.getTransactionCount(accountAddress), + gasPrice: await provider.getGasPrice(), + customData: { + gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, + }, + gasLimit: 0n, + }; + } + async function testAaTx(from: string, data: string) { + const smartAccount = new SmartAccount({ + address: from, + secret: ownerWallet.privateKey, + }, provider); + + const aaTx = { + ...await aaTxTemplate(from), + to: from, + data, + }; + aaTx.gasLimit = await provider.estimateGas(aaTx); + + const signedTransaction = await smartAccount.signTransaction(aaTx); + const tx = await provider.broadcastTransaction(signedTransaction); + const receipt = await tx.wait(); + logInfo(`transaction gas used: ${receipt.gasUsed.toString()}`); + } + + async function getValidationHookContract(type: string): Promise { + const contract = await create2(`${type}HookValidator`, ownerWallet, ethersStaticSalt, []); + return BaseHookValidator__factory.connect(await contract.getAddress(), ownerWallet); + } + + async function getExecutionHookContract(type: string): Promise { + const contract = await create2(`${type}HookExecutor`, ownerWallet, ethersStaticSalt, []); + return BaseHookExecution__factory.connect(await contract.getAddress(), ownerWallet); + } + + async function deployFundedAccount() { + const factoryContract = await fixtures.getAaFactory(); + + const randomSalt = randomBytes(32); + const deployTx = await factoryContract.deployProxySsoAccount( + randomSalt, + "hook-test-id" + randomBytes(32).toString(), + [], + [ownerWallet.address], + ); + + const deployTxReceipt = await deployTx.wait(); + logInfo(`\`deployProxySsoAccount\` gas used: ${deployTxReceipt?.gasUsed.toString()}`); + + const proxyAccountAddress = deployTxReceipt!.contractAddress!; + expect(proxyAccountAddress, "the proxy account location via logs").to.not.equal(ZeroAddress, "be a valid address"); + + const fundTx = await ownerWallet.sendTransaction({ value: parseEther("1"), to: proxyAccountAddress }); + const receipt = await fundTx.wait(); + expect(receipt.status).to.eq(1, "send funds to proxy account"); + + return { proxyAccountAddress }; + } + + describe("validation hook", function () { + it("should support modules", async function () { + const validationHookContract = await getValidationHookContract("Fail"); + const moduleInterface = await validationHookContract.supportsInterface("0xe7f04e93"); + const validationInterface = await validationHookContract.supportsInterface("0x37d5f03a"); + assert(moduleInterface, "supports module interface"); + assert(validationInterface, "supports hook interface"); + }); + + it("should install into existing account", async function () { + const { proxyAccountAddress } = await deployFundedAccount(); + const validationHookContract = await getValidationHookContract("Fail"); + const hookModuleAddress = await validationHookContract.getAddress(); + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("addHook", [hookModuleAddress, "0x"])); + }); + + it("fail on duplicate install", async function () { + const { proxyAccountAddress } = await deployFundedAccount(); + const validationHookContract = await getValidationHookContract("Success"); + const hookModuleAddress = await validationHookContract.getAddress(); + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("addHook", [hookModuleAddress, "0x"])); + + await expect(testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("addHook", [hookModuleAddress, "0x"]))).to.be.reverted; + }); + + it("should uninstall from account", async function () { + const { proxyAccountAddress } = await deployFundedAccount(); + const validationHookContract = await getValidationHookContract("Success"); + const hookModuleAddress = await validationHookContract.getAddress(); + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("addHook", [hookModuleAddress, "0x"])); + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("removeHook", [hookModuleAddress, "0x"])); + }); + + it("not fail on duplicate remove", async function () { + const { proxyAccountAddress } = await deployFundedAccount(); + const validationHookContract = await getValidationHookContract("Success"); + const hookModuleAddress = await validationHookContract.getAddress(); + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("addHook", [hookModuleAddress, "0x"])); + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("removeHook", [hookModuleAddress, "0x"])); + + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("removeHook", [hookModuleAddress, "0x"])); + }); + + it("block transactions on failure", async function () { + const { proxyAccountAddress } = await deployFundedAccount(); + const validationHookContract = await getValidationHookContract("Fail"); + const hookModuleAddress = await validationHookContract.getAddress(); + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("addHook", [hookModuleAddress, "0x"])); + await expect(testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("removeHook", [hookModuleAddress, "0x"]))).to.be.reverted; + }); + }); + + describe("execution hook", function () { + it("should install into existing account", async function () { + const { proxyAccountAddress } = await deployFundedAccount(); + const hookContract = await getExecutionHookContract("Success"); + const hookModuleAddress = await hookContract.getAddress(); + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("addHook", [hookModuleAddress, "0x"])); + }); + + it("fail on duplicate install", async function () { + const { proxyAccountAddress } = await deployFundedAccount(); + const hookContract = await getExecutionHookContract("Success"); + const hookModuleAddress = await hookContract.getAddress(); + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("addHook", [hookModuleAddress, "0x"])); + + await expect(testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("addHook", [hookModuleAddress, "0x"]))).to.be.reverted; + }); + + it.only("should uninstall executor from account", async function () { + const { proxyAccountAddress } = await deployFundedAccount(); + const hookContract = await getExecutionHookContract("Success"); + const hookModuleAddress = await hookContract.getAddress(); + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("addHook", [hookModuleAddress, "0x"])); + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("removeHook", [hookModuleAddress, "0x"])); + }); + + it("not fail on duplicate remove", async function () { + const { proxyAccountAddress } = await deployFundedAccount(); + const hookContract = await getExecutionHookContract("Success"); + const hookModuleAddress = await hookContract.getAddress(); + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("addHook", [hookModuleAddress, "0x"])); + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("removeHook", [hookModuleAddress, "0x"])); + + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("removeHook", [hookModuleAddress, "0x"])); + }); + + it("block transactions on pre-execution failure", async function () { + const { proxyAccountAddress } = await deployFundedAccount(); + const hookContract = await getExecutionHookContract("PreFail"); + const hookModuleAddress = await hookContract.getAddress(); + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("addHook", [hookModuleAddress, "0x"])); + await expect(testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("removeHook", [hookModuleAddress, "0x"]))).to.be.reverted; + }); + + it("block transactions on post-execution failure", async function () { + const { proxyAccountAddress } = await deployFundedAccount(); + const hookContract = await getExecutionHookContract("PostFail"); + const hookModuleAddress = await hookContract.getAddress(); + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("addHook", [hookModuleAddress, "0x"])); + await expect(testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("removeHook", [hookModuleAddress, "0x"]))).to.be.reverted; + }); + }); + + describe("combined hook", function () { + it("success on install & remove", async function () { + const { proxyAccountAddress } = await deployFundedAccount(); + const contract = await create2(`SuccessBothHook`, ownerWallet, ethersStaticSalt, []); + const hookModuleAddress = await contract.getAddress(); + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("addHook", [hookModuleAddress, "0x"])); + await testAaTx(proxyAccountAddress, ssoAbi.encodeFunctionData("removeHook", [hookModuleAddress, "0x"])); + }); + }); +}); diff --git a/test/utils.ts b/test/utils.ts index 479d2b85..7c8f24b4 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -6,20 +6,19 @@ import { ethers, parseEther, randomBytes } from "ethers"; import { readFileSync } from "fs"; import { promises } from "fs"; import * as hre from "hardhat"; +import { Address, isHex, toHex } from "viem"; import { ContractFactory, Provider, utils, Wallet } from "zksync-ethers"; import { base64UrlToUint8Array, getPublicKeyBytesFromPasskeySignature, unwrapEC2Signature } from "zksync-sso/utils"; -import { Address, isHex, toHex } from "viem"; import type { AAFactory, + AccountProxy, ERC20, ExampleAuthServerPaymaster, SessionKeyValidator, SsoAccount, - WebAuthValidator, SsoBeacon, - AccountProxy -} from "../typechain-types"; + WebAuthValidator } from "../typechain-types"; import { AAFactory__factory, AccountProxy__factory, @@ -27,10 +26,9 @@ import { ExampleAuthServerPaymaster__factory, SessionKeyValidator__factory, SsoAccount__factory, - WebAuthValidator__factory, SsoBeacon__factory, - TestPaymaster__factory -} from "../typechain-types"; + TestPaymaster__factory, + WebAuthValidator__factory } from "../typechain-types"; export const ethersStaticSalt = new Uint8Array([ 205, 241, 161, 186, 101, 105, 79, @@ -95,7 +93,7 @@ export class ContractFixtures { async getPasskeyModuleAddress(): Promise
{ const webAuthnVerifierContract = await this.getWebAuthnVerifierContract(); - const contractAddress = await webAuthnVerifierContract.getAddress() + const contractAddress = await webAuthnVerifierContract.getAddress(); return isHex(contractAddress) ? contractAddress : toHex(contractAddress); } @@ -263,7 +261,7 @@ export const create2 = async (contractName: string, wallet: Wallet, salt: ethers 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!`); + logInfo(`Contract ${contractName} already exists at ${standardCreate2Address}!`); return new ethers.Contract(standardCreate2Address, contractArtifact.abi, wallet); } @@ -309,13 +307,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",