Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Request]: Add Two-Factor Authentication (2FA) Validator Module #211

Open
12 tasks
MexicanAce opened this issue Dec 3, 2024 · 0 comments
Open
12 tasks

[Request]: Add Two-Factor Authentication (2FA) Validator Module #211

MexicanAce opened this issue Dec 3, 2024 · 0 comments
Labels
enhancement New feature or request

Comments

@MexicanAce
Copy link
Contributor

📝 Description

We'd like to introduce a ECDSA Two-Factor Authentication (2FA) Validator Module for ERC-7579 and Clave smart accounts.

NOTE: This work (including code snippets below) was originally proposed/completed by @alexandrecarvalheira

The 2FA Validator Hook provides the following key features:

  1. Configurable daily spending threshold
  2. Secondary validation (currently ECDSA) for:
    • Transactions exceeding the daily limit
    • Critical operations (e.g., changing settings)
  3. Flexible design allowing for future adaptation of the secondary validation method

Key Components:

  • TwoFAValidatorModule.sol: The main contract implementing the 2FA logic
  • 2FAValidator.test.ts: Comprehensive test suite for the module

🤔 Rationale

The module enhances account security by implementing a daily spending limit and requiring additional validation for high-value transactions or sensitive operations.

📋 Additional context

Original PR: https://github.com/MexicanAce/zksync-account/pull/1

TODOs

  • Implement daily spent check functionality
  • Complete ERC20 token support
  • Integrate WETH conversion for accurate value calculation
  • Add feature to demo-app
  • For testing purposes, a small change has been made in ClaveAccount.sol, this change bypasses the main validation temporarily to facilitate testing. It should be reverted later before final tests.
  • Change owner function
  • Update configuration function
  • Test for multiple transfer to reach threshold
  • Test daily spent reset functionality
  • Test ERC20 token support and WETH conversion
  • Test change owner functionality (once implemented)
  • Test update configuration functionality (once implemented)

File changes:

ClaveAccount.sol

    //FIX: not doing proper main validation while passkeysigner
    // bool valid = _handleValidation(validator, signedHash, signature);
    bool valid = true;

interfaces/IPoolFactory.sol

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.24;

interface IPoolFactory {
  function getPool(address tokenA, address tokenB) external view returns (address pool);
}

interface IPool {
  function getAmountOut(address tokenIn, uint amountIn, address sender) external view returns (uint amountOut);
}

managers/ModuleManager.sol

  //FIX: this modulelinkedList seems to never be used
  function _modulesLinkedList() private view returns (mapping(address => address) storage modules) {
    modules = ClaveStorage.layout().modules;
  }

validators/2FAValidatorModule.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../interfaces/IERC7579Module.sol";

import { IValidationHook } from "../interfaces/IHook.sol";
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import { Transaction, TransactionHelper } from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol";
import { IERC7579Module } from "../interfaces/IERC7579Module.sol";
import { IUserOpValidator } from "../interfaces/IERC7579Validator.sol";
import { IR1Validator } from "../interfaces/IValidator.sol";
import { IPoolFactory, IPool } from "../interfaces/IPoolFactory.sol";

import { Utils } from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/Utils.sol";

/**
 * This contract implements a Two-Factor Authentication (2FA) Validator Module for ERC-7579 and Clave smart accounts.
 * It provides the following key features:
 * 1. Daily spending limit: Tracks and enforces a configurable daily spending threshold for the account.
 * 2. Secondary validation: Requires an additional ECDSA signature (2FA) when:
 *    a) The daily spending limit is exceeded.
 *    b) Certain critical operations are performed (e.g., changing settings).
 * 3. Flexible authentication: While currently using ECDSA, the secondary validation method can be adapted.
 *
 * This module enhances account security by adding an extra layer of verification for high-value or sensitive transactions,
 * while allowing routine operations to proceed without additional friction up to the daily limit.
 */
contract TwoFAValidatorModule is IERC7579Module, IValidationHook {
  /*//////////////////////////////////////////////////////////////////////////
                            CONSTANTS & STORAGE
    //////////////////////////////////////////////////////////////////////////*/

  event ModuleInitialized(address indexed account);
  event ModuleUninitialized(address indexed account);
  event ConfigSet(address indexed account, address indexed token);
  event ThresholdSet(address indexed account, uint256 threshold);

  error InvalidSigner(address signer);
  error InvalidSignature();

  //TODO: not tested
  address poolFactory = 0x0a34FBDf37C246C0B401da5f00ABd6529d906193;
  address WETH = 0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91;

  struct Config {
    address signer;
    uint256 threshold;
  }

  // token address to its threshold to require the owner signature
  mapping(address account => Config) public config;

  // spent amount by account on the timeperiod
  mapping(address account => mapping(uint256 timeperiod => uint256 amount)) public dailySpent;
  /*//////////////////////////////////////////////////////////////////////////
                                     CONFIG
    //////////////////////////////////////////////////////////////////////////*/

  /**
   * @dev Initializes the module with the given data
   * @param initData The data to initialize the module with
   */
  function init(bytes calldata initData) external {
    _install(initData);
  }

  /**
   * @dev Initializes the module with the given data
   * @param data The data to initialize the module with
   */
  function onInstall(bytes calldata data) external override {
    _install(data);
  }

  function _install(bytes calldata installData) internal {
    // cache the account address
    address account = msg.sender;

    if (isInitialized(account)) revert AlreadyInitialized(account);

    // decode the data to get the tokens and their configurations
    Config memory _config = abi.decode(installData, (Config));

    if (_config.signer == address(0)) {
      revert InvalidSigner(_config.signer);
    }

    config[account].signer = _config.signer;
    config[account].threshold = _config.threshold;

    emit ModuleInitialized(account);
  }

  //it needs the signature and hash as bytes param to validate signature to uninstall
  function disable() external {
    _uninstall();
  }

  /**
   * @dev De-initializes the module
   */
  function onUninstall(bytes calldata) external override {
    _uninstall();
  }

  function _uninstall() internal {
    // cache the account address
    address account = msg.sender;

    // TODO: verify signature

    // clear the configurations
    delete config[account];

    emit ModuleUninitialized(account);
  }

  /**
   * Check if the module is initialized
   * @param smartAccount The smart account to check
   *
   * @return true if the module is initialized, false otherwise
   */
  function isInitialized(address smartAccount) public view returns (bool) {
    return config[smartAccount].signer != address(0);
  }

  /*//////////////////////////////////////////////////////////////////////////
                                     MODULE LOGIC
    //////////////////////////////////////////////////////////////////////////*/

  /**
   * @dev Sets the threshold for the account
   * @param _threshold The new threshold to set
   * @param hookData Additional data for validation
   */
  function setThreshold(uint256 _threshold, bytes calldata hookData) external {
    // cache the account address
    address account = msg.sender;
    // check if the module is initialized and revert if it is not
    if (!isInitialized(account)) revert NotInitialized(account);

    _isValid(hookData);

    config[account].threshold = _threshold;

    emit ThresholdSet(account, _threshold);
  }

  //TODO: change owner function
  //TODO: change config

  /**
   * @dev Retrieves the daily spent amount for an account
   * @param account The account to check
   * @return totalSpentToday The total amount spent today
   */
  function getDailySpent(address account) public view returns (uint256 totalSpentToday) {
    if (!isInitialized(account)) revert NotInitialized(account);

    uint256 today = _getCurrentDay();
    totalSpentToday = dailySpent[account][today];
  }

  /**
   * @dev Retrieves the signer for an account
   * @param account The account to check
   * @return signer The address of the signer
   */
  function getSigner(address account) public view returns (address signer) {
    if (!isInitialized(account)) revert NotInitialized(account);

    signer = config[account].signer;
  }

  /**
   * @dev Retrieves the threshold for an account
   * @param account The account to check
   * @return threshold The current threshold
   */
  function getThreshold(address account) public view returns (uint256 threshold) {
    if (!isInitialized(account)) revert NotInitialized(account);

    threshold = config[account].threshold;
  }

  /**
   * @dev Validates a transaction and updates daily spent amount
   * @param transaction The transaction to validate
   * @param hookData Additional data for validation
   */
  function validationHook(bytes32, Transaction calldata transaction, bytes calldata hookData) external {
    // cache the account address
    address account = msg.sender;
    uint256 amount = Utils.safeCastToU128(transaction.value) + _decodeERC20Amount(transaction.data, transaction.to);

    uint256 today = _getCurrentDay();
    uint256 totalSpentToday = dailySpent[account][today] + amount;
    dailySpent[account][today] = totalSpentToday;

    // Check if the new transaction would exceed the daily threshold
    if (totalSpentToday <= config[account].threshold) {
      return;
    }

    _isValid(hookData);
  }

  function _isValid(bytes calldata hookData) internal view {
    (bytes memory signature, bytes32 signedTxHash) = abi.decode(hookData, (bytes, bytes32));

    // magic = EIP1271_SUCCESS_RETURN_VALUE;

    if (signature.length != 65) {
      // Signature is invalid anyway, but we need to proceed with the signature verification as usual
      // in order for the fee estimation to work correctly
      signature = new bytes(65);

      // Making sure that the signatures look like a valid ECDSA signature and are not rejected rightaway
      // while skipping the main verification process.
      signature[64] = bytes1(uint8(27));
    }

    // extract ECDSA signature
    uint8 v;
    bytes32 r;
    bytes32 s;
    // Signature loading code
    // we jump 32 (0x20) as the first slot of bytes contains the length
    // we jump 65 (0x41) per signature
    // for v we load 32 bytes ending with v (the first 31 come from s) then apply a mask
    assembly {
      r := mload(add(signature, 0x20))
      s := mload(add(signature, 0x40))
      v := and(mload(add(signature, 0x41)), 0xff)
    }

    if (v != 27 && v != 28) {
      revert InvalidSignature();
    }

    // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
    // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
    // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
    // signatures from current libraries generate a unique signature with an s-value in the lower half order.
    //
    // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
    // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
    // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
    // these malleable signatures as well.
    if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
      revert InvalidSignature();
    }

    address recoveredAddress = ecrecover(signedTxHash, v, r, s);

    // Note, that we should abstain from using the require here in order to allow for fee estimation to work
    if (recoveredAddress != getSigner(msg.sender)) {
      revert InvalidSignature();
    }
  }

  function _getCurrentDay() internal view returns (uint256) {
    return block.timestamp / 1 days;
  }

  function _updateDailySpent(address account, uint256 amount) internal {
    uint256 today = _getCurrentDay();
    dailySpent[account][today] += amount;
  }

  function _decodeERC20Amount(bytes calldata callData, uint256 _token) internal view returns (uint256 amountOut) {
    amountOut = 0;
    // Check if the data is long enough to contain a function selector
    if (callData.length < 4) {
      return amountOut;
    }

    uint256 amountInToken;
    // get the function selector
    bytes4 selector = bytes4(callData[:4]);
    // get the parameters
    bytes calldata params = callData[4:];

    if (selector == IERC20.transfer.selector) {
      // decode the erc20 transfer receiver
      (, amountInToken) = abi.decode(params, (address, uint256));
    } else {
      return amountOut;
    }

    // get pool on syncswap
    // get ETH amountOut
    address token = address(uint160(_token));
    address pool = IPoolFactory(poolFactory).getPool(token, WETH);
    amountOut = IPool(pool).getAmountOut(token, amountInToken, msg.sender);
  }

  /*//////////////////////////////////////////////////////////////////////////
                                     METADATA
    //////////////////////////////////////////////////////////////////////////*/

  /**
   * The name of the module
   *
   * @return name The name of the module
   */
  function name() external pure returns (string memory) {
    return "2FAValidatorModule";
  }

  /**
   * The version of the module
   *
   * @return version The version of the module
   */
  function version() external pure returns (string memory) {
    return "0.0.1";
  }

  /**
   * The version of the module
   *
   * @param interfaceId the interfaceId to check
   *
   * @return true if The module supports interface
   */
  function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
    return interfaceId == type(IValidationHook).interfaceId || interfaceId == type(IERC165).interfaceId;
  }

  /**
   * Check if the module is of a certain type
   *
   * @param typeID The type ID to check
   *
   * @return true if the module is of the given type, false otherwise
   */
  function isModuleType(uint256 typeID) external pure override returns (bool) {
    return typeID == MODULE_TYPE_VALIDATOR;
  }
}

test/2FAValidator.tests.ts

import { assert, expect } from "chai";
import { randomBytes } from "crypto";
import { AbiCoder, Contract, ethers, parseEther } from "ethers";
import { it } from "mocha";
import { encodePasskeyModuleParameters } from "zksync-account/utils";
import { EIP712Signer, utils, Wallet } from "zksync-ethers";

import { deployFactory, getProvider, getWallet, LOCAL_RICH_WALLETS } from "./utils";
import { deployContract, ethersResponse, ethersStaticSalt, prepareTx } from "./utils/transaction";

const provider = getProvider();
const abiCoder = new AbiCoder();

let richWallet: Wallet;
let mockSigner: ethers.HDNodeWallet;

let factory: Contract;
let implementation: Contract;
let passkeyValidator: Contract;
let twofaValidator: Contract;
let account: Contract;

beforeEach(async () => {
  richWallet = getWallet(LOCAL_RICH_WALLETS[0].privateKey);

  mockSigner = Wallet.createRandom(getProvider());

  implementation = await deployContract("ERC7579Account", richWallet, ethersStaticSalt);
  await deployContract("AccountProxy", richWallet, ethersStaticSalt, [await implementation.getAddress()]);
  passkeyValidator = await deployContract("WebAuthValidator", richWallet, ethersStaticSalt);
  factory = await deployFactory("AAFactory", richWallet);
  twofaValidator = await deployContract("TwoFAValidatorModule", richWallet, ethersStaticSalt);

  const passKeyModuleData = encodePasskeyModuleParameters({
    passkeyPublicKey: await ethersResponse.getXyPublicKeys(),
    expectedOrigin: ethersResponse.expectedOrigin,
  });

  const webauthModuleData = abiCoder.encode(
    ["address", "bytes"],
    [await passkeyValidator.getAddress(), passKeyModuleData]);

  const proxyAccountTX = await factory.deployProxy7579Account(randomBytes(32),
    await implementation.getAddress(),
    "ProxyAccount",
    [webauthModuleData], []);

  const proxyAccountTxReceipt = await proxyAccountTX.wait();
  account = new Contract(proxyAccountTxReceipt.contractAddress, implementation.interface, richWallet);
  //   logInfo(`"7579Account" was successfully deployed to ${await account.getAddress()}`);

  await (
    await richWallet.sendTransaction({
      to: await account.getAddress(),
      value: parseEther("10"),
    })
  ).wait();
});
describe("2FA validation module", function () {
  it("should have correct state after deployment", async function () {
    expect(await provider.getBalance(await account.getAddress())).to.eq(
      parseEther("10"),
    );
    const expectedModules: Array<ethers.BytesLike> = [];
    const expectedValidationHooks: Array<ethers.BytesLike> = [];
    const expectedHooks: Array<ethers.BytesLike> = [];

    expect(await account.listModules()).to.deep.eq(expectedModules);
    expect(await account.listHooks(false)).to.deep.eq(expectedHooks);
    expect(await account.listHooks(true)).to.deep.eq(expectedValidationHooks);
  });
  it("should add 2fa validator Hook to account", async function () {
    const initData = abiCoder.encode(
      ["tuple(address,uint256)"], // Solidity equivalent: Config
      [[await richWallet.getAddress(),
        parseEther("1"),
      ]])
    ;
    const hookAndData = ethers.concat([
      await twofaValidator.getAddress(), initData]);

    const callData = account.interface.encodeFunctionData("addHook", [hookAndData, true]);

    const tx = { to: await account.getAddress(), data: callData };

    const addHook = await prepareTx(provider, account, tx, await passkeyValidator.getAddress());
    const txReceipt = await provider.broadcastTransaction(
      utils.serializeEip712(addHook),
    );

    await txReceipt.wait();

    expect(await account.listHooks(true)).to.deep.eq([await twofaValidator.getAddress()]);
  });
  it ("should transfer ETH without 2FA below threshold", async function () {
    const initData = abiCoder.encode(
      ["tuple(address,uint256)"], // Solidity equivalent: Config
      [[await richWallet.getAddress(),
        parseEther("1"),
      ]])
    ;
    const hookAndData = ethers.concat([
      await twofaValidator.getAddress(), initData]);

    const callData = account.interface.encodeFunctionData("addHook", [hookAndData, true]);

    const tx = { to: await account.getAddress(), data: callData };

    const addHook = await prepareTx(provider, account, tx, await passkeyValidator.getAddress());
    const txReceipt = await provider.broadcastTransaction(
      utils.serializeEip712(addHook),
    );

    await txReceipt.wait();

    const receiverBalanceBefore = await provider.getBalance(
      await richWallet.getAddress(),
    );
    const transferValue = parseEther("0.01");

    const tx2 = { to: await richWallet.getAddress(), data: "0x", value: transferValue };

    const transfer = await prepareTx(provider, account, tx2, await passkeyValidator.getAddress(), mockSigner);
    const txReceipt2 = await provider.broadcastTransaction(
      utils.serializeEip712(transfer),
    );

    await txReceipt2.wait();

    assert.equal(receiverBalanceBefore + transferValue, await provider.getBalance(
      await richWallet.getAddress()));
  });
  it("should transfer ETH with 2FA above threshold", async function () {
    const initData = abiCoder.encode(
      ["tuple(address,uint256)"], // Solidity equivalent: Config
      [[await richWallet.getAddress(),
        parseEther("1"),
      ]])
    ;
    const hookAndData = ethers.concat([
      await twofaValidator.getAddress(), initData]);

    const callData = account.interface.encodeFunctionData("addHook", [hookAndData, true]);

    const tx = { to: await account.getAddress(), data: callData };

    const addHook = await prepareTx(provider, account, tx, await passkeyValidator.getAddress());
    const txReceipt = await provider.broadcastTransaction(
      utils.serializeEip712(addHook),
    );

    await txReceipt.wait();

    const receiverBalanceBefore = await provider.getBalance(
      await richWallet.getAddress(),
    );
    const transferValue = parseEther("2");

    const tx2 = { to: await richWallet.getAddress(), data: "0x", value: transferValue };

    const transfer = await prepareTx(provider, account, tx2, await passkeyValidator.getAddress(), richWallet);
    const txReceipt2 = await provider.broadcastTransaction(
      utils.serializeEip712(transfer),
    );

    await txReceipt2.wait();

    assert.equal(receiverBalanceBefore + transferValue, await provider.getBalance(
      await richWallet.getAddress()));
  });
  it("should revert on transfer ETH without 2FA above threshold", async function () {
    const initData = abiCoder.encode(
      ["tuple(address,uint256)"], // Solidity equivalent: Config
      [[await richWallet.getAddress(),
        parseEther("1"),
      ]])
    ;
    const hookAndData = ethers.concat([
      await twofaValidator.getAddress(), initData]);

    const callData = account.interface.encodeFunctionData("addHook", [hookAndData, true]);

    const tx = { to: await account.getAddress(), data: callData };

    const addHook = await prepareTx(provider, account, tx, await passkeyValidator.getAddress());
    const txReceipt = await provider.broadcastTransaction(
      utils.serializeEip712(addHook),
    );

    await txReceipt.wait();

    const transferValue = parseEther("2");

    const tx2 = { to: await richWallet.getAddress(), data: "0x", value: transferValue };

    const transfer = await prepareTx(provider, account, tx2, await passkeyValidator.getAddress(), mockSigner);
    const txReceipt2 = provider.broadcastTransaction(
      utils.serializeEip712(transfer),
    );

    await expect(txReceipt2).to.be.rejected;
  });
  it("should change threshold with 2FA", async function () {
    const initData = abiCoder.encode(
      ["tuple(address,uint256)"], // Solidity equivalent: Config
      [[await richWallet.getAddress(),
        parseEther("1"),
      ]])
    ;
    const hookAndData = ethers.concat([
      await twofaValidator.getAddress(), initData]);

    const callData = account.interface.encodeFunctionData("addHook", [hookAndData, true]);

    const tx = { to: await account.getAddress(), data: callData };

    const addHook = await prepareTx(provider, account, tx, await passkeyValidator.getAddress());
    const txReceipt = await provider.broadcastTransaction(
      utils.serializeEip712(addHook),
    );

    await txReceipt.wait();

    const newThreshold = parseEther("1");
    const tx2 = { to: await twofaValidator.getAddress(), from: await account.getAddress(),
      nonce: await provider.getTransactionCount(await account.getAddress()),
      gasPrice: await provider.getGasPrice(),
      chainId: (await provider.getNetwork()).chainId };

    const signedTxHash = EIP712Signer.getSignedDigest(tx2);
    const signature = ethers.Signature.from(richWallet.signingKey.sign(signedTxHash)).serialized;

    const hookData = ethers.AbiCoder.defaultAbiCoder().encode(
      ["bytes", "bytes32"],
      [signature, signedTxHash],
    );

    const callData2 = twofaValidator.interface.encodeFunctionData("setThreshold", [
      newThreshold, hookData]);
    tx2["data"] = callData2;
    const transfer = await prepareTx(provider, account, tx2, await passkeyValidator.getAddress(), richWallet);

    const txReceipt2 = await provider.broadcastTransaction(
      utils.serializeEip712(transfer),
    );

    await txReceipt2.wait();

    expect(await twofaValidator.getThreshold(await account.getAddress())).to.eq(newThreshold);
  });
  it("should revert on change threshold without 2FA", async function () {
    const initData = abiCoder.encode(
      ["tuple(address,uint256)"], // Solidity equivalent: Config
      [[await richWallet.getAddress(),
        parseEther("1"),
      ]])
    ;
    const hookAndData = ethers.concat([
      await twofaValidator.getAddress(), initData]);

    const callData = account.interface.encodeFunctionData("addHook", [hookAndData, true]);

    const tx = { to: await account.getAddress(), data: callData };

    const addHook = await prepareTx(provider, account, tx, await passkeyValidator.getAddress());
    const txReceipt = await provider.broadcastTransaction(
      utils.serializeEip712(addHook),
    );

    await txReceipt.wait();

    const newThreshold = parseEther("1");
    const tx2 = { to: await twofaValidator.getAddress(), from: await account.getAddress(),
      nonce: await provider.getTransactionCount(await account.getAddress()),
      gasPrice: await provider.getGasPrice(),
      chainId: (await provider.getNetwork()).chainId };

    const signedTxHash = EIP712Signer.getSignedDigest(tx2);
    const signature = ethers.Signature.from(mockSigner.signingKey.sign(signedTxHash)).serialized;

    const hookData = ethers.AbiCoder.defaultAbiCoder().encode(
      ["bytes", "bytes32"],
      [signature, signedTxHash],
    );

    const callData2 = twofaValidator.interface.encodeFunctionData("setThreshold", [
      newThreshold, hookData]);
    tx2["data"] = callData2;
    const transfer = await prepareTx(provider, account, tx2, await passkeyValidator.getAddress(), richWallet);
    const txReceipt2 = provider.broadcastTransaction(
      utils.serializeEip712(transfer),
    );

    await expect(txReceipt2).to.be.reverted;
  });
  it.skip("should transfer twice with 2FA on 2nd transfer above threshold", async function () {});
  it.skip("should revert on 2nd transfer above threshold above 2FA", async function () {});
});

test/utils/transaction.ts

import { Deployer } from "@matterlabs/hardhat-zksync-deploy";
import { AbiCoder, ethers, parseEther } from "ethers";
import * as hre from "hardhat";
import type { Contract, Provider, types, Wallet } from "zksync-ethers";
import { EIP712Signer, utils } from "zksync-ethers";

import { RecordedResponse } from "../utils";

export async function prepareTx(
  provider: Provider,
  account: Contract,
  tx: types.TransactionLike,
  validatorAddress: string,
  signer?: Wallet | ethers.HDNodeWallet,
  paymasterParams?: types.PaymasterParams,
): Promise<types.TransactionLike> {
  if (tx.value == undefined) {
    tx.value = parseEther("0");
  }

  tx = {
    ...tx,
    from: await account.getAddress(),
    nonce: await provider.getTransactionCount(await account.getAddress()),
    gasPrice: await provider.getGasPrice(),
    gasLimit: 30_000_000,
    chainId: (await provider.getNetwork()).chainId,
    type: 113,
    customData: {
      gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
      paymasterParams: paymasterParams,
    } as types.Eip712Meta,
  };

  const signedTxHash = EIP712Signer.getSignedDigest(tx);

  const abiCoder = ethers.AbiCoder.defaultAbiCoder();
  const PasskeySignature = abiCoder.encode(["bytes", "bytes", "bytes32[2]"], [
    ethersResponse.authDataBuffer,
    ethersResponse.clientDataBuffer,
    [ethersResponse.rs.r, ethersResponse.rs.s],
  ]);

  let hookData: string[];
  if (signer) {
    const ecdsaSignature = ethers.concat([ethers.Signature.from(signer.signingKey.sign(signedTxHash)).serialized]);

    hookData = [
      AbiCoder.defaultAbiCoder().encode(["bytes", "bytes32"], [ecdsaSignature, signedTxHash]),
    ];
  } else {
    hookData = [];
  }

  const signature = abiCoder.encode(
    ["bytes", "address", "bytes[]"],
    [PasskeySignature, validatorAddress, hookData],
  );

  tx.customData = {
    ...tx.customData,
    customSignature: signature,
  };

  return tx;
}

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,
]);

export const ethersResponse = new RecordedResponse("test/signed-challenge.json");

export const deployContract = async (contractName: string, wallet: Wallet, salt: ethers.BytesLike, constructorArguments?: Array<unknown>): Promise<ethers.Contract> => {
  const deployer = new Deployer(hre, wallet);
  const contractArtifact = await deployer
    .loadArtifact(contractName);

  const contract = await deployer.deploy(contractArtifact, constructorArguments);
  // const address = await contract.getAddress();

  // logInfo(`"${contractArtifact.contractName}" was successfully deployed to ${address}`);

  return contract;
};
@MexicanAce MexicanAce added the enhancement New feature or request label Dec 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant