Getting Started

Project Setup

We recommend scaffolding your project using the zksync-cli.

Configuration

In order to obtain a Sophscan API Key, you need to create an account and get an API Key here.

Set up your project with the following configuration:

import type { HardhatUserConfig } from "hardhat/config";

import "@matterlabs/hardhat-zksync";
import * as dotenv from 'dotenv';

// Load environment variables from .env file
dotenv.config();

const config: HardhatUserConfig = {
  defaultNetwork: "sophonTestnet",
  networks: {
    hardhat: {
      zksync: true,
    },
    sophonMainnet: {
      url: "https://rpc.sophon.xyz",
      ethNetwork: "mainnet",
      verifyURL: "https://verification-explorer.sophon.xyz/contract_verification",
      browserVerifyURL: "https://explorer.sophon.xyz/",
      enableVerifyURL: true,
      zksync: true,
      accounts: [process.env.WALLET_PRIVATE_KEY as string]
    },
    sophonTestnet: {
      url: "https://rpc.testnet.sophon.xyz",
      ethNetwork: "sepolia",
      verifyURL: "https://api-explorer-verify.testnet.sophon.xyz/contract_verification",
      browserVerifyURL: "https://explorer.testnet.sophon.xyz/",
      enableVerifyURL: true,
      zksync: true,
      accounts: [process.env.WALLET_PRIVATE_KEY as string]
    },
  },
  zksolc: {
    version: "latest",
    settings: {
    },
  },
  solidity: {
    version: "0.8.27",
  },
  etherscan: {
    enabled: true,
    apiKey: {
      sophonTestnet: process.env.ETHERSCAN_SOPHON_API_KEY as string,
      sophonMainnet: process.env.ETHERSCAN_SOPHON_API_KEY as string,
    },
    customChains: [
      {
        network: "sophonTestnet",
        chainId: 531050104,
        urls: {
          apiURL: "https://api-testnet.sophscan.xyz/api",
          browserURL: "https://testnet.sophscan.xyz",
        },
      },
      {
        network: "sophonMainnet",
        chainId: 50104,
        urls: {
          apiURL: "https://api.sophscan.xyz/api",
          browserURL: "https://sophscan.xyz",
        },
      },
    ],
  }
};

export default config;

Contract Deployment

Basic Deployment

Here’s how to deploy a contract without using the paymaster:

import { utils } from "zksync-ethers";

const deployer = new Deployer(hre, wallet);
const artifact = await deployer.loadArtifact("Your_Contract");
const contract = await deployer.deploy(
    artifact,
    constructorArguments || []
);

Deployment with Paymaster

During the alpha stage, you’ll need to use our Paymaster to sponsor transactions. Here’s how to deploy using the paymaster:

import { utils } from "zksync-ethers";

const deployer = new Deployer(hre, wallet);
const artifact = await deployer.loadArtifact("Your_Contract");

const params = utils.getPaymasterParams(
  "0x98546B226dbbA8230cf620635a1e4ab01F6A99B2", // Paymaster address
  {
    type: "General",
    innerInput: new Uint8Array(),
  }
);

const contract = await deployer.deploy(
    artifact,
    constructorArguments || [], 
    {
      customData: {
        paymasterParams: params,
        gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
    },
});

Proxy Deployment

To deploy proxy contracts with paymaster support:

import { utils } from "zksync-ethers";

const deployer = new Deployer(hre, wallet);
const artifact = await deployer.loadArtifact("Your_Contract");

await hre.zkUpgrades.deployProxy(deployer.zkWallet, artifact, [initializerArgs], 
 {
   initializer: "initialize",
   paymasterProxyParams: params,
   paymasterImplParams: params,
 }
);

Contract Interaction

To interact with deployed contracts using the paymaster:

import { utils } from "zksync-ethers";

const paymasterParams = utils.getPaymasterParams(
  "0x98546B226dbbA8230cf620635a1e4ab01F6A99B2",
  {
    type: "General",
    innerInput: new Uint8Array(),
  }
);

const tx = await contract.yourFunction(params, {
  customData: {
    gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
    paymasterParams: paymasterParams,
  },
});

Contract Verification

You can verify contracts on both Sophscan and Sophon’s explorer:

npx hardhat verify --network sophonTestnet DEPLOYED_CONTRACT_ADDRESS constructor_arguments

Check if a contract is whitelisted by the paymaster

Before making calls to a newly deployed contract using a paymaster, it’s important to verify that the contract has been added to the paymaster’s whitelist. Our automated contract whitelisting service typically takes 2-3 seconds to whitelist new contracts under normal network conditions. While optional, checking the whitelist status before proceeding makes deployment scripts more robust by ensuring contracts are ready to interact with the paymaster.

The code below provides a reference implementation for checking the whitelist status:

 const restrictionContractAddress = (hre.network.config as unknown as CustomNetworkConfig)?.restrictionContract;
    if (restrictionContractAddress && usePaymaster) {
      const contractAddress = await contract.getAddress();
      logger.info(`Checking if ${contractName} (${contractAddress}) is whitelisted on restriction contract...`);
      
      // Create interface for the restriction contract
      const restrictionContractABI = [
        {
          "inputs": [
            {
              "internalType": "address",
              "name": "",
              "type": "address"
            }
          ],
          "name": "contractWhitelist",
          "outputs": [
            {
              "internalType": "bool",
              "name": "",
              "type": "bool"
            }
          ],
          "stateMutability": "view",
          "type": "function"
        }
      ];
      
      const provider = hre.ethers.provider;
      const restrictionContract = new hre.ethers.Contract(restrictionContractAddress, restrictionContractABI, provider);
      
      let isWhitelisted = false;
      let attempts = 0;
      const maxAttempts = 5;
      
      while (!isWhitelisted && attempts < maxAttempts) {
        try {
          isWhitelisted = await restrictionContract.contractWhitelist(contractAddress);
          
          if (isWhitelisted) {
            logger.info(`${contractName} is now whitelisted on restriction contract!`);
          } else {
            attempts++;
            if (attempts < maxAttempts) {
              logger.info(`${contractName} is not yet whitelisted. Waiting ${waitTime / 1000} seconds before retry (attempt ${attempts}/${maxAttempts})...`);
              await new Promise(resolve => setTimeout(resolve, waitTime));
            } else {
              logger.warn(`${contractName} is still not whitelisted after ${maxAttempts} attempts. Proceeding anyway, but paymaster transactions might fail.`);
            }
          }
        } catch (error) {
          logger.error(`Error checking whitelist status: ${error}`);
          attempts++;
          if (attempts < maxAttempts) {
            await new Promise(resolve => setTimeout(resolve, waitTime));
          }
        }
      }
    }