wbobeirne / eth-balance-checker

Solidity contract to batch balance checks in one call
MIT License
253 stars 82 forks source link

Cannot get ETH balance with BalanceChecker.sol #64

Open kopax opened 2 weeks ago

kopax commented 2 weeks ago

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 ?

kopax commented 2 weeks ago

@kopax You're experiencing a python error. It sounds like your query would be best dealt with by the support team. I'll have to refer you to the official support live chat with the ticket ID TRT07638 Please see the link below to our dedicated support line: Trustwallet live support Click on the live chat icon at the bottom corner of the page to initiate chat with a live support agent.

@Clarevero1 , there is not a single line of python involved. Maybe you should get a real job and learn programming languages instead of scamming people.