Daily spending limit account

Build a native smart contract account that has a daily spend limit.

This tutorial will guide you through the process of building a smart contract account that has a daily spend limit. We'll create a factory contract that deploys new smart accounts and configure the account limit and test it works as expected

What you'll learn:

  • How to build a smart contract factory
  • How to build a smart contract account
  • How to send transactions through a smart contract account
GitHub

Tools:

  • - zksync-cli
  • - zksync-ethers
  • - Hardhat
account abstractionsmart contracts
Last Updated: May 09, 2024

This tutorial shows you how to create a smart contract account with a daily spending limit using the ZKsync Era native account abstraction.

The daily limit feature prevents an account from spending more ETH than the limit set by the account's owner.

Prerequisites

Skip the hassle for test ETH by using zksync-cli for local testing. Simply execute npx zksync-cli dev start to initialize a local ZKsync development environment, which includes local Ethereum and ZKsync nodes. This method allows you to test contracts without requesting external testnet funds. Explore more in the zksync-cli documentation.

Complete Project

Download the complete project on GitHub. Additionally, the repository contains a test folder with more detailed tests for running on a ZKsync Era local network.

This entire tutorial can be run in under a minute using Atlas. Atlas is a smart contract IDE that lets you write, deploy, and interact with contracts from your browser. Open this project in Atlas.

Project Setup

We will use the ZKsync Era Hardhat plugins to build, deploy, and interact with the smart contracts in this project.

  1. Initiate a new project by running the command:
    npx zksync-cli create custom-spendlimit-tutorial --template hardhat_solidity
    

    This creates a new ZKsync Era project called custom-spendlimit-tutorial with a few example contracts.
  2. Navigate into the project directory:
    cd custom-spendlimit-tutorial
    
  3. For the purposes of this tutorial, we don't need the example contracts related files. So, proceed by removing all the files inside the /contracts and /deploy folders manually or by running the following commands:
    rm -rf ./contracts/*
    rm -rf ./deploy/*
    
  4. Add the ZKsync and OpenZeppelin contract libraries:
    npm install -D @matterlabs/zksync-contracts @openzeppelin/contracts@4.9.5 @matterlabs/hardhat-zksync-deploy@1.3.0
    
    This project does not use the latest version available of @openzeppelin/contracts. Make sure you install the specific version mentioned above.
  5. Include the isSystem: true setting in the zksolc section of the hardhat.config.ts configuration file to allow interaction with system contracts:
    hardhat.config.ts
    import { HardhatUserConfig } from "hardhat/config";
    
    import "@matterlabs/hardhat-zksync-node";
    import "@matterlabs/hardhat-zksync-deploy";
    import "@matterlabs/hardhat-zksync-solc";
    import "@matterlabs/hardhat-zksync-verify";
    
    const config: HardhatUserConfig = {
      defaultNetwork: "zkSyncSepoliaTestnet",
      networks: {
        zkSyncSepoliaTestnet: {
          url: "https://sepolia.era.zksync.dev",
          ethNetwork: "sepolia",
          zksync: true,
          verifyURL: "https://explorer.sepolia.era.zksync.dev/contract_verification",
        },
        inMemoryNode: {
          url: "http://127.0.0.1:8011",
          ethNetwork: "localhost", // in-memory node doesn't support eth node; removing this line will cause an error
          zksync: true,
        },
        hardhat: {
          zksync: true,
        },
        // Additional networks
      },
      zksolc: {
        version: "latest",
        settings: {
          isSystem: true, // ⚠️ Make sure to include this line
        },
      },
      solidity: {
        version: "0.8.17",
      },
    };
    
    export default config;
    

Design

Now let’s dive into the design and implementation of the daily spending limit feature.

The SpendLimit contract inherits from the Account contract as a module that does the following:

  • Allows the account to enable/disable the daily spending limit in a token (ETH in this example).
  • Allows the account to change (increase/decrease or remove) the daily spending limit.
  • Rejects token transfer if the daily spending limit has been exceeded.
  • Restores the available amount for spending after 24 hours.

Basic Structure

Below you'll find the SpendLimit skeleton contract.

SpendLimit.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract SpendLimit {

    uint public ONE_DAY = 24 hours;

    modifier onlyAccount() {
        require(
            msg.sender == address(this),
            "Only the account that inherits this contract can call this method."
        );
        _;
    }

    function setSpendingLimit(address _token, uint _amount) public onlyAccount {
    }

    function removeSpendingLimit(address _token) public onlyAccount {
    }

    function _isValidUpdate(address _token) internal view returns(bool) {
    }

    function _updateLimit(address _token, uint _limit, uint _available, uint _resetTime, bool _isEnabled) private {
    }

    function _checkSpendingLimit(address _token, uint _amount) internal {
    }

}

The mapping limits and struct Limit below serve as data storage for the state of daily limits accounts enable.

The roles of each variable in the struct are detailed in the comments.

SpendLimit.sol
    /// This struct serves as data storage of daily spending limits users enable
    /// limit: the amount of a daily spending limit
    /// available: the available amount that can be spent
    /// resetTime: block.timestamp at the available amount is restored
    /// isEnabled: true when a daily spending limit is enabled
    struct Limit {
        uint limit;
        uint available;
        uint resetTime;
        bool isEnabled;
    }

    mapping(address => Limit) public limits; // token => Limit

Note that the limits mapping uses the token address as its key. This means that users can set limits for ETH and any other ERC20 token.

Setting and Removing the Daily Spending Limit

The code below sets and removes the limit.

SpendLimit.sol
    /// this function enables a daily spending limit for specific tokens.
    /// @param _token ETH or ERC20 token address that a given spending limit is applied.
    /// @param _amount non-zero limit.
    function setSpendingLimit(address _token, uint _amount) public onlyAccount {
        require(_amount != 0, "Invalid amount");

        uint resetTime;
        uint timestamp = block.timestamp; // L2 block timestamp

        if (isValidUpdate(_token)) {
            resetTime = timestamp + ONE_DAY;
        } else {
            resetTime = timestamp;
        }

        _updateLimit(_token, _amount, _amount, resetTime, true);
    }

    // this function disables an active daily spending limit,
    // decreasing each uint number in the Limit struct to zero and setting isEnabled false.
    function removeSpendingLimit(address _token) public onlyAccount {
        require(isValidUpdate(_token), "Invalid Update");
        _updateLimit(_token, 0, 0, 0, false);
    }

   // verify if the update to a Limit struct is valid
    // Ensure that users can't freely modify(increase or remove) the daily limit to spend more.
    function isValidUpdate(address _token) internal view returns (bool) {
        // Reverts unless it is first spending after enabling
        // or called after 24 hours have passed since the last update.
        if (limits[_token].isEnabled) {
            require(
                limits[_token].limit == limits[_token].available ||
                    block.timestamp > limits[_token].resetTime,
                "Invalid Update"
            );

            return true;
        } else {
            return false;
        }
    }

    // storage-modifying private function called by either setSpendingLimit or removeSpendingLimit
    function _updateLimit(
        address _token,
        uint _limit,
        uint _available,
        uint _resetTime,
        bool _isEnabled
    ) private {
        Limit storage limit = limits[_token];
        limit.limit = _limit;
        limit.available = _available;
        limit.resetTime = _resetTime;
        limit.isEnabled = _isEnabled;
    }

Both setSpendingLimit and removeSpendingLimit can only be called by account contracts that inherit the contract SpendLimit. This is ensured by the onlyAccount modifier. They call _updateLimit and pass the arguments to modify the storage data of the limit after the verification in _isValidUpdate succeeds.

Specifically, setSpendingLimit sets a non-zero daily spending limit for a given token, and removeSpendingLimit disables the active daily spending limit by decreasing limit and available to 0 and setting isEnabled to false.

_isValidUpdate returns false if the spending limit is not enabled and also throws an Invalid Update error if the user has spent some amount in the day (the available amount is different from the limit) or the function is called before 24 hours have passed since the last update. This ensures that users can't freely modify (increase or remove) the daily limit to spend more.

Checking Daily Spending Limit

The _checkSpendingLimit function is internally called by the account contract before executing the transaction.

SpendLimit.sol
    // this function is called by the account before execution.
    // Verify the account is able to spend a given amount of tokens. And it records a new available amount.
    function _checkSpendingLimit(address _token, uint _amount) internal {
        Limit memory limit = limits[_token];

        // return if spending limit hasn't been enabled yet
        if (!limit.isEnabled) return;

        uint timestamp = block.timestamp; // L2 block timestamp

        // Renew resetTime and available amount, which is only performed
        // if a day has already passed since the last update: timestamp > resetTime
        if (limit.limit != limit.available && timestamp > limit.resetTime) {
            limit.resetTime = timestamp + ONE_DAY;
            limit.available = limit.limit;

            // Or only resetTime is updated if it's the first spending after enabling limit
        } else if (limit.limit == limit.available) {
            limit.resetTime = timestamp + ONE_DAY;
        }

        // reverts if the amount exceeds the remaining available amount.
        require(limit.available >= _amount, "Exceed daily limit");

        // decrement `available`
        limit.available -= _amount;
        limits[_token] = limit;
    }

If the daily spending limit is disabled, the checking process immediately stops.

SpendLimit.sol
if(!limit.isEnabled) return;

Before checking the spending amount, this method renews the resetTime and available amount if a day has already passed since the last update: timestamp > resetTime. It only updates the resetTime if the transaction is the first spending after enabling the limit. This way the daily limit actually starts with the first transaction.

SpendLimit.sol
if (limit.limit != limit.available && timestamp > limit.resetTime) {
    limit.resetTime = timestamp + ONE_DAY;
    limit.available = limit.limit;

    // Or only resetTime is updated if it's the first spending after enabling limit
} else if (limit.limit == limit.available) {
    limit.resetTime = timestamp + ONE_DAY;
}

Finally, the method checks if the account can spend a specified amount of the token. If the amount doesn't exceed the available amount, it decrements the available in the limit:

SpendLimit.sol
require(limit.available >= _amount, 'Exceed daily limit');

limit.available -= _amount;

Full Code for the SpendLimit Contract

  1. In the folder contracts, add a file called SpendLimit.sol
    touch contracts/SpendLimit.sol
    
  2. Copy/paste the complete code below.
    The value of the ONE_DAY variable is initially set to 1 minutes instead of 24 hours. This is just for testing purposes as we don't want to wait a full day to see if it works! Don't forget to change the value before deploying the contract.

Account and AAFactory Contracts

Let's create the account contract Account.sol, and the factory contract that deploys account contracts, in AAFactory.sol. As noted earlier, those two contracts are based on the implementations of the multisig account abstraction tutorial. The main difference is that our account has a single signer.

Account.sol

  1. Create a file Account.sol in the contracts folder.
    touch contracts/Account.sol
    
  2. Copy/paste the code below.

The account implements the IAccount interface and inherits the SpendLimit contract we just created. Since we are building an account with signers, we should also implement EIP1271.

The isValidSignature method will take care of verifying the signature and making sure the extracted address matches with the owner of the account.

The formal ETH address on ZKsync Era is 0x000000000000000000000000000000000000800a. Neither the well-known 0xEee...EEeE used by protocols as a placeholder on Ethereum, nor the zero address 0x000...000, that (zksync-ethers provides) has a more user-friendly alias.
The SpendLimit account is token-agnostic. This means an extension is also possible: add a check for whether or not the execution is an ERC20 transfer by extracting the function selector in bytes from transaction calldata.

AAFactory.sol

The AAFactory.sol contract is responsible for deploying instances of the Account.sol contract.

  1. Create the AAFactory.sol file in the contracts folder.
    touch contracts/AAFactory.sol
    
  2. Copy/paste the code below.
AAFactory.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol";
import "@matterlabs/zksync-contracts/l2/system-contracts/libraries/SystemContractsCaller.sol";

contract AAFactory {
    bytes32 public aaBytecodeHash;

    constructor(bytes32 _aaBytecodeHash) {
        aaBytecodeHash = _aaBytecodeHash;
    }

    function deployAccount(
        bytes32 salt,
        address owner
    ) external returns (address accountAddress) {
        (bool success, bytes memory returnData) = SystemContractsCaller
            .systemCallWithReturndata(
                uint32(gasleft()),
                address(DEPLOYER_SYSTEM_CONTRACT),
                uint128(0),
                abi.encodeCall(
                    DEPLOYER_SYSTEM_CONTRACT.create2Account,
                    (
                        salt,
                        aaBytecodeHash,
                        abi.encode(owner),
                        IContractDeployer.AccountAbstractionVersion.Version1
                    )
                )
            );
        require(success, "Deployment failed");

        (accountAddress) = abi.decode(returnData, (address));
    }
}

Compile and Deploy the Smart Contracts

  1. Compile the contracts from the project root.
    npm run compile
    
  2. Create a file named deploy/deploy.ts. Then, copy and paste the following code into it. Remember to add your DEPLOYER_PRIVATE_KEY to the .env file.
    touch deploy/deploy.ts
    

    The script deploys the factory, creates a new smart contract account, and funds it with some ETH.
    deploy/deploy.ts
    import { utils, Wallet, Provider } from "zksync-ethers";
    import * as ethers from "ethers";
    import { HardhatRuntimeEnvironment } from "hardhat/types";
    import { Deployer } from "@matterlabs/hardhat-zksync-deploy";
    
    // load env file
    import dotenv from "dotenv";
    dotenv.config();
    
    const DEPLOYER_PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY || "";
    
    export default async function (hre: HardhatRuntimeEnvironment) {
      // @ts-ignore target zkSyncSepoliaTestnet in config file which can be testnet or local
      const provider = new Provider(hre.network.config.url);
      const wallet = new Wallet(DEPLOYER_PRIVATE_KEY, provider);
      const deployer = new Deployer(hre, wallet);
      const factoryArtifact = await deployer.loadArtifact("AAFactory");
      const aaArtifact = await deployer.loadArtifact("Account");
    
      // Bridge funds if the wallet on ZKsync doesn't have enough funds.
      // const depositAmount = ethers.parseEther('0.1');
      // const depositHandle = await deployer.zkWallet.deposit({
      //   to: deployer.zkWallet.address,
      //   token: utils.ETH_ADDRESS,
      //   amount: depositAmount,
      // });
      // await depositHandle.wait();
    
      const factory = await deployer.deploy(factoryArtifact, [utils.hashBytecode(aaArtifact.bytecode)], undefined, [aaArtifact.bytecode]);
      const factoryAddress = await factory.getAddress();
      console.log(`AA factory address: ${factoryAddress}`);
    
      const aaFactory = new ethers.Contract(factoryAddress, factoryArtifact.abi, wallet);
    
      const owner = Wallet.createRandom();
      console.log("SC Account owner pk: ", owner.privateKey);
    
      const salt = ethers.ZeroHash;
      const tx = await aaFactory.deployAccount(salt, owner.address);
      await tx.wait();
    
      const abiCoder = new ethers.AbiCoder();
      const accountAddress = utils.create2Address(factoryAddress, await aaFactory.aaBytecodeHash(), salt, abiCoder.encode(["address"], [owner.address]));
    
      console.log(`SC Account deployed on address ${accountAddress}`);
    
      console.log("Funding smart contract account with some ETH");
      await (
        await wallet.sendTransaction({
          to: accountAddress,
          value: ethers.parseEther("0.02"),
        })
      ).wait();
      console.log(`Done!`);
    }
    
  3. Run the script.
    npm run deploy
    

    You should see the following:
    AA factory address: 0x0d3205bc8134A11f9402fBA01947Bf377FaE4C39
    SC Account owner pk:  0x4ze1f87c5e575dsb6b35afe70ftu93fs53bca24592f6ng25f3v1a4f6fsd3vz
    SC Account deployed on address 0x29d0D17857bdA971F40FF11145859eED7bD15c00
    Funding smart contract account with some ETH
    Done!
    ✨  Done in 10.85s.
    

Open up the ZKsync Sepolia Testnet Block Explorer and search for the deployed Account contract address in order to track transactions and changes in the balance.

Set the daily spending limit

  1. Create the file setLimit.ts in the deploy folder and copy/paste the example code below.
    touch deploy/setLimit.ts
    
  2. Replace <DEPLOYED_ACCOUNT_ADDRESS> and <DEPLOYED_ACCOUNT_OWNER_PRIVATE_KEY> with the output from the previous section in your .env file.
    To enable the daily spending limit, we execute the setSpendingLimit function with two parameters: token address and limit amount. The token address is ETH_ADDRESS and the limit parameter is 0.0005 in the example below (and can be any amount).
    setLimit.ts
    import { utils, Wallet, Provider, Contract, EIP712Signer, types } from "zksync-ethers";
    import * as ethers from "ethers";
    import { HardhatRuntimeEnvironment } from "hardhat/types";
    
    // load env file
    import dotenv from "dotenv";
    dotenv.config();
    
    // load the values into .env file after deploying the FactoryAccount
    const DEPLOYED_ACCOUNT_OWNER_PRIVATE_KEY = process.env.DEPLOYED_ACCOUNT_OWNER_PRIVATE_KEY || "";
    const ETH_ADDRESS = process.env.ETH_ADDRESS || "0x000000000000000000000000000000000000800A";
    const ACCOUNT_ADDRESS = process.env.DEPLOYED_ACCOUNT_ADDRESS || "";
    
    export default async function (hre: HardhatRuntimeEnvironment) {
      // @ts-ignore target zkSyncSepoliaTestnet in config file which can be testnet or local
      const provider = new Provider(hre.network.config.url);
    
      const owner = new Wallet(DEPLOYED_ACCOUNT_OWNER_PRIVATE_KEY, provider);
    
      const accountArtifact = await hre.artifacts.readArtifact("Account");
      const account = new Contract(ACCOUNT_ADDRESS, accountArtifact.abi, owner);
    
      let setLimitTx = await account.setSpendingLimit.populateTransaction(ETH_ADDRESS, ethers.parseEther("0.0005"));
    
      setLimitTx = {
        ...setLimitTx,
        from: ACCOUNT_ADDRESS,
        chainId: (await provider.getNetwork()).chainId,
        nonce: await provider.getTransactionCount(ACCOUNT_ADDRESS),
        type: 113,
        customData: {
          gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
        } as types.Eip712Meta,
        value: BigInt(0),
      };
    
      setLimitTx.gasPrice = await provider.getGasPrice();
      setLimitTx.gasLimit = await provider.estimateGas(setLimitTx);
    
      const signedTxHash = EIP712Signer.getSignedDigest(setLimitTx);
    
      const signature = ethers.concat([ethers.Signature.from(owner.signingKey.sign(signedTxHash)).serialized]);
    
      setLimitTx.customData = {
        ...setLimitTx.customData,
        customSignature: signature,
      };
    
      console.log("Setting limit for account...");
      const sentTx = await provider.broadcastTransaction(types.Transaction.from(setLimitTx).serialized);
    
      await sentTx.wait();
    
      const limit = await account.limits(ETH_ADDRESS);
      console.log("Account limit enabled?: ", limit.isEnabled);
      console.log("Account limit: ", limit.limit.toString());
      console.log("Available limit today: ", limit.available.toString());
      console.log("Time to reset limit: ", limit.resetTime.toString());
    }
    
  3. Run the script.
    npx hardhat deploy-zksync --script setLimit.ts
    

    You should see the following output:
    Setting limit for account...
    Account limit enabled?:  true
    Account limit:  500000000000000
    Available limit today:  500000000000000
    Time to reset limit:  1708688165
    

Perform ETH Transfer

Let's test the SpendLimit contract works to make it refuse ETH transfers that exceed the daily limit.

  1. Create transferETH.ts and copy/paste the example code below, replacing the placeholder constants as before and adding an account address for RECEIVER_ACCOUNT.
    touch deploy/transferETH.ts
    
    deploy/transferETH.ts
    import { utils, Wallet, Provider, Contract, EIP712Signer, types } from "zksync-ethers";
    import * as ethers from "ethers";
    import type { HardhatRuntimeEnvironment } from "hardhat/types";
    
    // load env file
    import dotenv from "dotenv";
    dotenv.config();
    
    // load the values into .env file after deploying the FactoryAccount
    const DEPLOYED_ACCOUNT_OWNER_PRIVATE_KEY = process.env.DEPLOYED_ACCOUNT_OWNER_PRIVATE_KEY || "";
    const ETH_ADDRESS = process.env.ETH_ADDRESS || "0x000000000000000000000000000000000000800A";
    const ACCOUNT_ADDRESS = process.env.DEPLOYED_ACCOUNT_ADDRESS || "";
    const RECEIVER_ACCOUNT = process.env.RECEIVER_ACCOUNT || "";
    
    export default async function (hre: HardhatRuntimeEnvironment) {
      // @ts-ignore target zkSyncSepoliaTestnet in config file which can be testnet or local
      const provider = new Provider(hre.network.config.url);
    
      const owner = new Wallet(DEPLOYED_ACCOUNT_OWNER_PRIVATE_KEY, provider);
    
      // ⚠️ update this amount to test if the limit works; 0.00051 fails but 0.00049 succeeds
      const transferAmount = "0.00051";
    
      let ethTransferTx = {
        from: ACCOUNT_ADDRESS,
        to: RECEIVER_ACCOUNT, // account that will receive the ETH transfer
        chainId: (await provider.getNetwork()).chainId,
        nonce: await provider.getTransactionCount(ACCOUNT_ADDRESS),
        type: 113,
        customData: {
          gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
        } as types.Eip712Meta,
    
        value: ethers.parseEther(transferAmount),
        gasPrice: await provider.getGasPrice(),
        gasLimit: BigInt(20000000), // constant 20M since estimateGas() causes an error and this tx consumes more than 15M at most
        data: "0x",
      };
      const signedTxHash = EIP712Signer.getSignedDigest(ethTransferTx);
      const signature = ethers.concat([ethers.Signature.from(owner.signingKey.sign(signedTxHash)).serialized]);
    
      ethTransferTx.customData = {
        ...ethTransferTx.customData,
        customSignature: signature,
      };
    
      const accountArtifact = await hre.artifacts.readArtifact("Account");
    
      // read account limits
      const account = new Contract(ACCOUNT_ADDRESS, accountArtifact.abi, owner);
      const limitData = await account.limits(ETH_ADDRESS);
    
      console.log("Account ETH limit is: ", limitData.limit.toString());
      console.log("Available today: ", limitData.available.toString());
    
      // L1 timestamp tends to be undefined in latest blocks. So it should find the latest L1 Batch first.
      if (hre.network.config.ethNetwork !== 'localhost') {
        let l1BatchRange = await provider.getL1BatchBlockRange(await provider.getL1BatchNumber());
        let l1TimeStamp = (await provider.getBlock(l1BatchRange[1])).l1BatchTimestamp;
    
        console.log('L1 timestamp: ', l1TimeStamp);
        console.log('Limit will reset on timestamp: ', limitData.resetTime.toString());
      }
    
      // actually do the ETH transfer
      console.log("Sending ETH transfer from smart contract account");
      const sentTx = await provider.broadcastTransaction(types.Transaction.from(ethTransferTx).serialized);
      await sentTx.wait();
      console.log(`ETH transfer tx hash is ${sentTx.hash}`);
    
      console.log("Transfer completed and limits updated!");
    
      const newLimitData = await account.limits(ETH_ADDRESS);
      console.log("Account limit: ", newLimitData.limit.toString());
      console.log("Available today: ", newLimitData.available.toString());
      console.log("Limit will reset on timestamp:", newLimitData.resetTime.toString());
    
      if (newLimitData.resetTime.toString() == limitData.resetTime.toString()) {
        console.log("Reset time was not updated as not enough time has passed");
      } else {
        console.log("Limit timestamp was reset");
      }
      return;
    }
    
  2. Run the script to attempt to make a transfer.
    npx hardhat deploy-zksync --script transferETH.ts
    

    You should see an error message with the following content so we know it failed because the amount exceeded the limit.
    An unexpected error occurred:
    Error: transaction failed...
    
    shortMessage: 'execution reverted: "Exceed daily limit"'
    

    You can also search the transaction in the explorer to see the error reason.
    After the error, we can rerun the code with a different ETH amount that doesn't exceed the limit, say "0.00049", to see if the SpendLimit contract doesn't refuse the amount lower than the limit.
    If the transaction succeeds, the output should look something like this:
    Account ETH limit is:  500000000000000
    Available today:  494900000000000
    Limit will reset on timestamp:  1708689912
    Sending ETH transfer from smart contract account
    ETH transfer tx hash is 0x6e7742e6555a88ca1489a06992711d413a12358f77c611cca96aba112ced812b
    Transfer completed and limits updated!
    Account limit:  500000000000000
    Available today:  449000000000000
    Limit will reset on timestamp: 1708690236
    Current timestamp:  1708690185
    Reset time was not updated as not enough time has passed
    

    The available value in the Limit struct updates to the initial limit minus the amount we transferred.
    Since ONE_DAY is set to 1 minute for this test in the SpendLimit.sol contract, you should expect it to reset after 60 seconds.

Common Errors

  • Insufficient gasLimit: Transactions often fail due to insufficient gasLimit. Please increase the value manually when transactions fail without clear reasons.
  • Insufficient balance in account contract: transactions may fail due to the lack of balance in the deployed account contract. Please transfer funds to the account using MetaMask or wallet.sendTransaction() method used in deploy/deployFactoryAccount.ts.
  • Transactions submitted in a close range of time will have the same block.timestamp as they can be added to the same L1 batch and might cause the spend limit to not work as expected.

Learn More

  • To learn more about L1->L2 interaction on ZKsync, check out the documentation.
  • To learn more about the zksync-ethers SDK, check out its documentation.
  • To learn more about the ZKsync hardhat plugins, check out their documentation.

Credits

Written by porco-rosso for the GitCoin bounty.


Made with ❤️ by the ZKsync Community