Skip to content

Cannot get ETH balance with BalanceChecker.sol #64

@kopax

Description

@kopax

Hello, first of all thanks for sharing.

I have updated the code for Solidity 0.8:

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

// ERC20 contract interface
interface Token {
    function balanceOf(address account) external view returns (uint256);
}

contract BalanceChecker {
    /* Fallback function, don't accept any ETH */
    receive() external payable {
        revert("BalanceChecker does not accept payments");
    }

    /*
      Check the token balance of a wallet in a token contract

      Returns the balance of the token for user. Avoids possible errors:
        - return 0 on non-contract address
        - returns 0 if the contract doesn't implement balanceOf
    */
    function tokenBalance(address user, address token) public view returns (uint256) {
        // check if token is actually a contract
        uint256 tokenCode;
        assembly { tokenCode := extcodesize(token) } // contract code size

        // is it a contract and does it implement balanceOf
        if (tokenCode > 0) {
            (bool success, bytes memory data) = token.staticcall(abi.encodeWithSignature("balanceOf(address)", user));
            if (success) {
                return abi.decode(data, (uint256));
            }
        }
        return 0;
    }

    /*
      Check the token balances of a wallet for multiple tokens.
      Pass address(0) as a "token" address to get ETH balance.

      Possible error throws:
        - extremely large arrays for user and/or tokens (gas cost too high)

      Returns a one-dimensional array that's user.length * tokens.length long. The
      array is ordered by all of the 0th users token balances, then the 1st
      user, and so on.
    */
    function balances(address[] calldata users, address[] calldata tokens) external view returns (uint256[] memory) {
        uint256[] memory addrBalances = new uint256[](tokens.length * users.length);

        for (uint256 i = 0; i < users.length; i++) {
            for (uint256 j = 0; j < tokens.length; j++) {
                uint256 addrIdx = j + tokens.length * i;
                if (tokens[j] != address(0)) {
                    addrBalances[addrIdx] = tokenBalance(users[i], tokens[j]);
                } else {
                    addrBalances[addrIdx] = users[i].balance; // ETH balance
                }
            }
        }

        return addrBalances;
    }
}

Here is also a javascript/typescript function that can be used to optimize the amount of call to the contract so the page never execed 100

import { BalanceChecker } from 'balance-checker' // this is typechain generated by hardhat with the previous contract

type Address = string
type Token = string

interface Result {
  address: Address
  token: Token
  balance: bigint
}

export async function balanceCheckerProcessInBatches(
  addresses: Address[],
  tokens: Token[],
  balanceChecker: BalanceChecker,
): Promise<Result[]> {
  const results: Result[] = []
  const maxBatchSize = 100

  for (let i = 0; i < addresses.length; i++) {
    for (let j = 0; j < tokens.length; j += maxBatchSize) {
      // Create subsets of addresses and tokens while respecting the limit of 100
      const batchTokens = tokens.slice(
        j,
        Math.min(j + maxBatchSize, tokens.length),
      )

      const currentBatchSize = batchTokens.length
      const remainingCapacity = maxBatchSize - currentBatchSize

      let batchAddresses: Address[] = [addresses[i]]

      if (remainingCapacity > 0 && i + 1 < addresses.length) {
        const extraAddresses = Math.min(
          Math.floor(remainingCapacity / tokens.length),
          addresses.length - i - 1,
        )
        batchAddresses = batchAddresses.concat(
          addresses.slice(i + 1, i + 1 + extraAddresses),
        )
        i += extraAddresses // Move forward the address index
      }

      const balances = await balanceChecker.balances(
        batchAddresses,
        batchTokens,
      )

      if (balances.length !== batchAddresses.length * batchTokens.length) {
        throw new Error(
          `Batch mismatch: Expected ${batchAddresses.length * batchTokens.length} balances, but got ${balances.length}`,
        )
      }

      for (let a = 0; a < batchAddresses.length; a++) {
        for (let t = 0; t < batchTokens.length; t++) {
          results.push({
            address: batchAddresses[a],
            token: batchTokens[t],
            balance: balances[a * batchTokens.length + t],
          })
        }
      }
    }
  }

  return results
}

/**
 * Test code below
 */
// const addresses = Array.from({ length: 50 }, (_, i) => `0xAddress${i + 1}`)
// const tokens = Array.from({ length: 2 }, (_, i) => `Token${i + 1}`)
//
// const balanceChecker = {
//   balances: async (
//     addresses: Address[],
//     tokens: Token[],
//   ): Promise<bigint[]> => {
//     console.log(
//       `Page size: ${addresses.length * tokens.length} (address ${addresses.length})  (tokens ${tokens.length})`,
//     )
//
//     const arrayOfBigInt = [];
//     for (let i = 0; i < addresses.length; i++) {
//       for (let j = 0; j < tokens.length; j++) {
//         arrayOfBigInt.push(BigInt(addresses[i].length + tokens[j].length));
//       }
//     }
//
//     console.log(`Confirmed page size: ${arrayOfBigInt.length}`);
//     return arrayOfBigInt;
//   },
// }
//
// ;(async () => {
//   const results = await balanceCheckerProcessInBatches(addresses, tokens, balanceChecker as BalanceChecker)
//
//   results.forEach((result) => {
//     console.log(`Address: ${result.address}, Token: ${result.token}, Balance: ${result.balance}`);
//   })
//
//   console.log(`Total number of results: ${results.length}`)
// })()

The commented code is for testing yourself, in case you don't trust.

This is awesome however, I am struggling trying to pass 0x0 to get ETH balance, with your deployed version on mainnet, or with my hardhat fork.

I keep having such kind of errors: : RangeError: cannot slice beyond data bounds (buffer=0x, length=0, offset=4, code=BUFFER_OVERRUN, version=6.13.1)

When I deploy my own BalanceChecker.sol, I don´t get much more information:

eth_call
  Contract call:             <UnrecognizedContract>
  From:                      0xbaa4dbc880eaf033fd6a01128116a060ca2ac661
  To:                        0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e

  Error: Transaction reverted without a reason

Any clue how to get ETH balance ?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions