npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@d3or/slotseek

v1.1.2

Published

A library for finding the storage slots on an ERC20 token for balances and approvals, which can be used to mock the balances and approvals of an address when estimating gas costs of transactions that would fail if the address did not have the required bal

Downloads

26,708

Readme

slotseek

slotseek is a javascript library that assists with finding the storage slots for the balanceOf and allowance mappings in an ERC20 token contract, and the permit2 allowance mapping. It also provides a way to generate mock data that can be used to override the state of a contract in an eth_call or eth_estimateGas call.

The main use case for this library is to estimate gas costs of transactions that would fail if the address did not have the required balance or approval.

For example, estimating the gas a transaction will consume when swapping, before the user has approved the contract to spend their tokens.

Features

  • Find storage slots for balanceOf and allowance mappings in an ERC20 token contract, and permit2 allowance mapping
  • Generates mock data that can be used to override the state of a contract in an eth_call/eth_estimateGas call
  • Supports vyper storage layouts

How it works

The library uses a brute force approach to find the storage slot of the balanceOf and allowance mappings in an ERC20 token contract. It does this by using a user-provided address that we know has a balance or approval, and then iterates through the storage slots of the contract via the eth_getStorageAt JSON-RPC method until it finds the slot where the storage value matches the user's balance or approval.

This is not a perfect method, and there are more efficient ways to find the storage slot outside of just interacting directly with the contract over RPC. But it's difficult to do so without needing to setup more tools/infra, especially for multi-chain support and gas estimation at runtime. Also, there are not many tools to help with this in javascript.

Installation

npm install @d3or/slotseek
# or
yarn add @d3or/slotseek

TODO

  • [X] Add caching options to reduce the number of RPC calls and reduce the time it takes to find the same slot again

Example of overriding a users balance via eth_call

import { ethers } from "ethers";
import { generateMockBalanceData } from "@d3or/slotseek";

async function fakeUserBalance() {
  // Setup - Base RPC
  const provider = new ethers.providers.JsonRpcProvider("YOUR_RPC_URL");

  // Constants
  const tokenAddress = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"; // USDC on Base
  const holderAddress = "0x0000c3Caa36E2d9A8CD5269C976eDe05018f0000"; // USDC holder
  const mockAddress = ethers.Wallet.createRandom().address; // Address to fake balance for
  const mockBalanceAmount = "1000000000000"; // 1 million USDC (6 decimal places), optional. If not provided, defaults to the balance of the holder

  // Generate mock balance data
  const data = await generateMockBalanceData(provider, {
    tokenAddress,
    holderAddress,
    mockAddress,
    mockBalanceAmount,
  });

  // Prepare state diff object
  const stateDiff = {
    [tokenAddress]: {
      stateDiff: {
        [data.slot]: data.balance,
      },
    },
  };

  // Prepare balanceOf call
  const balanceOfSelector = "0x70a08231";
  const encodedAddress = ethers.utils.defaultAbiCoder
    .encode(["address"], [mockAddress])
    .slice(2);
  const getBalanceCalldata = balanceOfSelector + encodedAddress;

  // Make the eth_call with state overrides, or eth_estimateGas
  const balanceOfResponse = await provider.send("eth_call", [
    {
      from: mockAddress,
      to: tokenAddress,
      data: getBalanceCalldata,
    },
    "latest",
    stateDiff,
  ]);

  // Decode and log the result
  const balance = ethers.BigNumber.from(
    ethers.utils.defaultAbiCoder.decode(["uint256"], balanceOfResponse)[0]
  );

  console.log(
    `Mocked balance for ${mockAddress}: ${ethers.utils.formatUnits(
      balance,
      6
    )} USDC`
  );
}

fakeUserBalance().catch(console.error);

This can also be used to fake approvals, by using the generateMockApprovalData function instead of generateMockBalanceData.

import { ethers } from "ethers";
import { generateMockApprovalData } from "@d3or/slotseek";

async function fakeUserApproval() {
  // Setup
  const provider = new ethers.providers.JsonRpcProvider("YOUR_RPC_URL");

  // Constants
  const tokenAddress = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"; // USDC on Base
  const ownerAddress = "0x0000c3Caa36E2d9A8CD5269C976eDe05018f0000"; // USDC holder
  const spenderAddress = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; // Spender address
  const mockAddress = ethers.Wallet.createRandom().address; // Address to fake balance for
  const mockApprovalAmount = "1000000000000"; // 1 million USDC (6 decimal places)

  // Generate mock approval data
  const mockApprovalData = await generateMockApprovalData(provider, {
    tokenAddress,
    ownerAddress,
    spenderAddress,
    mockAddress,
    mockApprovalAmount,
  });

  // Prepare state diff object
  const stateDiff = {
    [tokenAddress]: {
      stateDiff: {
        [mockApprovalData.slot]: mockApprovalData.approval,
      },
    },
  };

  // Function selector for allowance(address,address)
  const allowanceSelector = "0xdd62ed3e";
  // Encode the owner and spender addresses
  const encodedAddresses = ethers.utils.defaultAbiCoder
    .encode(["address", "address"], [mockAddress, spenderAddress])
    .slice(2);
  const getAllowanceCalldata = allowanceSelector + encodedAddresses;

  // Make the eth_call with state overrides, or eth_estimateGas
  const allowanceResponse = await provider.send("eth_call", [
    {
      from: mockAddress,
      to: tokenAddress,
      data: getAllowanceCalldata,
    },
    "latest",
    stateDiff,
  ]);

  // Decode and log the result
  const allowance = ethers.BigNumber.from(
    ethers.utils.defaultAbiCoder.decode(["uint256"], allowanceResponse)[0]
  );

  console.log(
    `Mocked allowance for ${mockAddress}: ${ethers.utils.formatUnits(
      allowance,
      6
    )} USDC`
  );
}

fakeUserApproval().catch(console.error);

You can also override both the balance and the allowance at the same time by providing both the balance and approval fields in the state diff object.

Example of just finding the storage slot in a contract

import { ethers } from "ethers";
import { getErc20BalanceStorageSlot } from "@d3or/slotseek";

async function findStorageSlot() {
  // Setup - Base RPC
  const provider = new ethers.providers.JsonRpcProvider(
    "https://mainnet.base.org"
  );

  // Constants
  const tokenAddress = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"; // USDC on Base
  const holderAddress = "0x0000c3Caa36E2d9A8CD5269C976eDe05018f0000"; // USDC holder
  const maxSlots = 100; // Max slots to search

  // Find the storage slot for the balance of the holde
  // or for approvals, use getErc20AllowanceStorageSlot
  const { slot, balance, isVyper } = await getErc20BalanceStorageSlot(
    provider,
    tokenAddress,
    holderAddress,
    maxSlots
  );

  console.log(
    `User has balance of ${ethers.utils.formatUnits(
      balance,
      6
    )} USDC stored at slot #${Number(slot)}`
  );
}

findStorageSlot().catch(console.error);

Example of mocking the permit2 allowance mapping

import { ethers } from "ethers";
import { computePermit2AllowanceStorageSlot } from "@d3or/slotseek";

async function findStorageSlot() {
  // Setup - Base RPC
  const provider = new ethers.providers.JsonRpcProvider(
    "https://mainnet.base.org"
  );

  // Constants
  const tokenAddress = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"; // USDC on Base
  const mockAddress = "0x0000c3Caa36E2d9A8CD5269C976eDe05018f0000"; // USDC holder to mock approval for
  const spenderAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"

  // Compute storage slot of where the allowance would be held
  const { slot } = computePermit2AllowanceStorageSlot(mockAddress, tokenAddress, spenderAddress)

  const permit2Contract = '0x000000000022d473030f116ddee9f6b43ac78ba3'

  // Prepare state diff object
  const stateDiff = {
    [permit2Contract]: {
      stateDiff: {
        [slot]: ethers.utils.hexZeroPad(
          ethers.utils.hexlify(ethers.BigNumber.from("1461501637330902918203684832716283019655932142975")),
          32
        )
        ,
      },
    },
  };

  // Function selector for allowance(address,address,address)
  const allowanceSelector = "0x927da105";
  // Encode the owner and spender addresses
  const encodedAddresses = ethers.utils.defaultAbiCoder
    .encode(["address", "address", "address"], [mockAddress, tokenAddress, spenderAddress])
    .slice(2);
  const getAllowanceCalldata = allowanceSelector + encodedAddresses;


  const callParams = [
    {
      to: permit2Contract,
      data: getAllowanceCalldata,
    },
    "latest",
  ];

  const allowanceResponse = await baseProvider.send("eth_call", [
    ...callParams,
    stateDiff,
  ]);

  // convert the response to a BigNumber
  const approvalAmount = ethers.BigNumber.from(
    ethers.utils.defaultAbiCoder.decode(["uint256"], allowanceResponse)[0]
  );

  console.log(
    `Mocked balance for ${mockAddress}: ${ethers.utils.formatUnits(
      approvalAmount,
      6
    )} USDC`
  );

}
findStorageSlot().catch(console.error);