pk910 / PoWFaucet

Modularized faucet for EVM chains with different protection methods (Captcha, Mining, IP, Mainnet Balance, Gitcoin Passport and more)
GNU Affero General Public License v3.0
3.8k stars 1.41k forks source link

Mainnet wallet module to check other evm tokens. #246

Closed Leooehh closed 1 week ago

Leooehh commented 2 weeks ago

Hey !

Would it be possible to change the Mainnet token balance check's functionality to check a specific evm token instead of the native token, or to check multiple tokens?

Thank you very much in advance!

pk910 commented 2 weeks ago

Heya,

I've added the functionality to the mainnet-wallet module. It can be configured via:

modules:
  ## Mainnet Wallet module
  mainnet-wallet:
    # ...
    # require minimal erc20 token balances on mainnet wallet
    minErc20Balances:
      - name: "WETH"
        address: "0x94373a4919B3240D86eA41593D5eBa789FEF3848" # WETH9 on holesky
        decimals: 18
        minBalance: 10000000000000000 # 0.01 WETH
Leooehh commented 2 weeks ago

You're the goat. 🫶

I'll test it out and will close this inc asap after.
Thank you very much !

Leooehh commented 1 week ago

Hey!

I tried the new version but ran into a few issues, which are most likely user errors on my side T-T.

The Advanced WebServer configuration was causing the app to crash after it was accessed on port 8080. This issue stopped after I added the VPS IP or commented out corsAllowOrigin.

### Advanced WebServer config

# allow external sites embedding this faucet
corsAllowOrigin:
#- "*"  # allow all - for development only!
#- "https://faucets.pk910.de"

The error after crash :

2024-06-22 21:31:32  ERROR    ### Caught unhandled exception: TypeError: Cannot read properties of null (reading 'length')
  Exception origin: uncaughtException
  Stack Trace: TypeError: Cannot read properties of null (reading 'length')
    at FaucetHttpServer.getCorsHeaders (file:///home/user/PoWFaucet/dist/webserv/FaucetHttpServer.js:130:42)
    at IncomingMessage.<anonymous> (file:///home/user/PoWFaucet/dist/webserv/FaucetHttpServer.js:115:77)
    at IncomingMessage.emit (node:events:519:28)
    at endReadableNT (node:internal/streams/readable:1696:12)
    at process.processTicksAndRejections (node:internal/process/task_queues:82:21)

After addressing the CORS configuration by adding the server IP or commenting out the function, the faucet page does not load properly.

Here is a screenshot of the issue:

image

Please let me know if any further info is required, thank you very much again for your continuous help!

pk910 commented 1 week ago

Oh yea, thanks for reporting :) Yea, the explorer requires that the corsAllowOrigin setting is either set or completely left out from the config. Setting it without any values in the way you did makes that field "null", which causes the issue you see. Change it to corsAllowOrigin: [] (the [] behind set the value to an empty array), or comment out the setting completely.

I'll make that more clear in the readme and check for null values :)

pk910 commented 1 week ago

lol, I see I've introduced that issue myself by putting that malformed empty setting in the example config 🤦😂

0b174a8ed835bcfbfc46af7b5f91ca305f29808c should fix this on code side, so that syntax is basically allowed now

Leooehh commented 1 week ago

Thanks for the quick update.

I just re-tested it, and:

The CORS function is no longer causing any issues, even with the default configuration. The faucet launches without any errors shown in PoWFaucet-out.log. ✅

However, I'm still encountering the attached error in the dev view with the latest version. I tried setting up the version before the Mainnet module change, and that version works fine. This leaves me a bit confused about what might be causing the issue:

Please let me know if there is something that I can try/change on my side. Once more thanks a lot for the help < 3 I really do appreciate it.

image

image

Leooehh commented 1 week ago

Here's the text content of the error given in console (in-case the image doesnt load)

**Content Security Policy of your site blocks the use of 'eval' in JavaScript**

The Content Security Policy (CSP) prevents the evaluation of arbitrary strings as JavaScript to make it more difficult for an attacker to inject unauthorized code on your site.

To solve this issue, avoid using `eval()`, `new Function()`, `setTimeout([string], ...)`, and `setInterval([string], ...)` for evaluating strings.

If you absolutely must: you can enable string evaluation by adding `unsafe-eval` as an allowed source in a `script-src` directive.

⚠️ Allowing string evaluation comes at the risk of inline script injection.

| Source location | Directive   | Status  |
|-----------------|-------------|---------|
| script-src      | blocked     |
pk910 commented 1 week ago

Thanks for your detailed report! Yea, I've recently introduced that bug, so thanks for catching it before the next release 😅 Should be fixed by 27c2da81e43d7c2a88bef3b7ba69040cef1a7e16, can you re-test?

The CSP warning is a bit weird, which browser are you using?

Leooehh commented 1 week ago

Thank you very much !

Its working all fine now, everything seems to functioning as expected.

I tried both with a chromium browser and firefox which was giving the CSP warning ( Its fully good now )

I think we can mark this as resolved for now. I'll try the ERC-20 token check function and will re-open in-case of any issues.

Once more I really appreciate your help with this, 🫶

Leooehh commented 1 week ago

Good morning @pk910 !

I tested the ERC20 balance check and encountered a small issue. It correctly blocks access if the user doesn't own any of the designated tokens. However, it still allows access even if the user has just "1" token, despite the minBalance being set to a much higher limit.

I used a random coin in this example, but other tokens are experiencing the same issue. I also tried changing the minBalance to different values, but that didn't help either.

Here's the config where I noticed the problem:

modules:
  ## Mainnet Wallet module
  mainnet-wallet:
    # ...
    # require minimal erc20 token balances on mainnet wallet
    minErc20Balances:
      - name: "PEPE"
        address: "0x6982508145454Ce325dDbE47a25d4ec3d2311933" # Pepe
        decimals: 18
        minBalance: 100000000000 # 10b

here's the message when access is successfully blocked showing 0 as requirement:

image

pk910 commented 1 week ago

Yea, the numbers don't look right. The faucet handles balances and amounts as raw integer values (without decimals).

I guess you wanted to check for >= 10 PEPE, so given the token has 18 decimals, 10PEPE is actually a value of 10000000000000000000 (10 * 10^18)

Leooehh commented 1 week ago

Silly me. That makes total sense. Everything seems to be good. Thank you very much!

Leooehh commented 1 week ago

Back at it againnn,

After a lot of confusion and testing on my side, I think I've figured out where the problem is.

The "[MAINNET_BALANCE_LIMIT]" error message is showing the balance of the user's wallet instead of the limit set in the config. For example, if the user owns 20 PEPE, the error message says: "You need to hold at least 20 PEPE," even if the limit is set to 50.

It's only the message that's incorrect though. The function itself is recognizing the limits just fine.

Sorry for the confusion earlier, I hope this helps.

I'll keep the ticket closed since I found a workaround by changing the alarm text manually but thought I'd let you know :)

Thanks a lot!

pk910 commented 1 week ago

fixed 👍

Leooehh commented 2 days ago

Hey @pk910 ! I recently added a ERC-721 in addition to ERC-20 token check to the module. I tested it out and it should be functional (please lmk if you see anything wrong so I can fix it thank you very much !).

Here's the updated code in-case you wanted to add it :)

MainnetWalletModule.ts :

import Web3 from 'web3';
import { ServiceManager } from "../../common/ServiceManager.js";
import { EthWalletManager } from "../../eth/EthWalletManager.js";
import { FaucetSession } from "../../session/FaucetSession.js";
import { BaseModule } from "../BaseModule.js";
import { ModuleHookAction } from "../ModuleManager.js";
import { defaultConfig, IMainnetWalletConfig } from './MainnetWalletConfig.js';
import { FaucetError } from '../../common/FaucetError.js';
import { Erc20Abi } from '../../abi/ERC20.js';

export const Erc721Abi = [
  {
    "constant": true,
    "inputs": [{ "name": "_owner", "type": "address" }],
    "name": "balanceOf",
    "outputs": [{ "name": "balance", "type": "uint256" }],
    "type": "function"
  }
];

export class MainnetWalletModule extends BaseModule<IMainnetWalletConfig> {
  protected readonly moduleDefaultConfig = defaultConfig;
  private web3: Web3;

  protected override startModule(): Promise<void> {
    this.startWeb3();
    this.moduleManager.addActionHook(this, ModuleHookAction.SessionStart, 7, "Mainnet Wallet check", (session: FaucetSession, userInput: any) => this.processSessionStart(session, userInput));
    return Promise.resolve();
  }

  protected override stopModule(): Promise<void> {
    return Promise.resolve();
  }

  private startWeb3() {
    let provider = EthWalletManager.getWeb3Provider(this.moduleConfig.rpcHost);
    this.web3 = new Web3(provider);
  }

  private async processSessionStart(session: FaucetSession, userInput: any): Promise<void> {
    if (session.getSessionData<Array<string>>("skip.modules", []).indexOf(this.moduleName) !== -1)
      return;
    let targetAddr = session.getTargetAddr();

    if (this.moduleConfig.minBalance > 0) {
      let minBalance = BigInt(this.moduleConfig.minBalance);
      let walletBalance: bigint;
      try {
        walletBalance = BigInt(await this.web3.eth.getBalance(targetAddr));
      } catch (ex) {
        throw new FaucetError("MAINNET_BALANCE_CHECK", "Could not get balance of mainnet wallet " + targetAddr + ": " + ex.toString());
      }
      if (walletBalance < minBalance)
        throw new FaucetError("MAINNET_BALANCE_LIMIT", "You need to hold at least " + ServiceManager.GetService(EthWalletManager).readableAmount(minBalance, true) + " in your wallet on mainnet to use this faucet.");
    }

    if (this.moduleConfig.minTxCount > 0) {
      let walletTxCount: bigint;
      try {
        walletTxCount = await this.web3.eth.getTransactionCount(targetAddr);
      } catch (ex) {
        throw new FaucetError("MAINNET_TXCOUNT_CHECK", "Could not get tx-count of mainnet wallet " + targetAddr + ": " + ex.toString());
      }
      if (walletTxCount < this.moduleConfig.minTxCount)
        throw new FaucetError("MAINNET_TXCOUNT_LIMIT", "You need to submit at least " + this.moduleConfig.minTxCount + " transactions from your wallet on mainnet to use this faucet.");
    }

    if (this.moduleConfig.minErc20Balances.length > 0) {
      let faucetAddress = ServiceManager.GetService(EthWalletManager).getFaucetAddress();
      for (let i = 0; i < this.moduleConfig.minErc20Balances.length; i++) {
        let erc20Token = this.moduleConfig.minErc20Balances[i];
        let minBalance = BigInt(erc20Token.minBalance);

        let walletBalance: bigint;
        try {
          let tokenContract = new this.web3.eth.Contract(Erc20Abi, erc20Token.address, {
            from: faucetAddress,
          });

          walletBalance = BigInt(await tokenContract.methods.balanceOf(targetAddr).call());
        } catch (ex) {
          throw new FaucetError("MAINNET_BALANCE_CHECK", "Could not get token balance of mainnet wallet " + targetAddr + ": " + ex.toString());
        }
        if (walletBalance < minBalance) {
          let factor = Math.pow(10, erc20Token.decimals || 18);
          let amountStr = (Math.floor(parseInt(minBalance.toString()) / factor * 1000) / 1000).toString();
          throw new FaucetError("MAINNET_BALANCE_LIMIT", "You need to hold at least " + amountStr + " " + erc20Token.name + " in your wallet on mainnet to use this faucet.");
        }
      }
    }

    if (this.moduleConfig.nftContract) {
      let nftContractAddress = this.moduleConfig.nftContract;
      let walletBalance: bigint;
      try {
        let nftContract = new this.web3.eth.Contract(Erc721Abi, nftContractAddress);
        walletBalance = BigInt(await nftContract.methods.balanceOf(targetAddr).call());
      } catch (ex) {
        throw new FaucetError("MAINNET_NFT_BALANCE_CHECK", "Could not get NFT balance of mainnet wallet " + targetAddr + ": " + ex.toString());
      }
      if (walletBalance <= BigInt(0)) {
        // Use the collection name and URL from the config
        const collectionName = this.moduleConfig.nftCollectionName || "the specified collection";
        const collectionUrl = this.moduleConfig.nftCollectionUrl || "#";
        throw new FaucetError("MAINNET_NFT_BALANCE_LIMIT", `You need to own at least one NFT from the collection "${collectionName}". Visit ${collectionUrl} for more details.`);
      }
    }
  }
}

MainnetWalletConfig.ts:

import { IBaseModuleConfig } from "../BaseModule.js";

export interface IMainnetWalletConfig extends IBaseModuleConfig {
  rpcHost: string;
  minTxCount: number;
  minBalance: number;
  minErc20Balances: {
    name: string;
    address: string;
    decimals?: number;
    minBalance: number;
  }[];
  nftContract?: string;
  nftCollectionName?: string;
  nftCollectionUrl?: string;
}

export const defaultConfig: IMainnetWalletConfig = {
  enabled: false,
  rpcHost: null,
  minTxCount: 0,
  minBalance: 0,
  minErc20Balances: [],
  nftContract: null,
  nftCollectionName: null,
  nftCollectionUrl: null
};

faucet-config.yaml

  ## Mainnet Wallet module
  mainnet-wallet:
    # enable / disable mainnet wallet protection
    enabled: false

    # RPC host for mainnet
    rpcHost: "https://mainnet.infura.io/v3/YOUR-PROJECT-ID"

    # require minimum balance on mainnet wallet
    #minBalance: 10000000000000000  # 0.01 ETH

    # require minimum number of transactions from mainnet wallet (nonce count)
    minTxCount: 5

    # require minimal erc20 token balances on mainnet wallet
    #minErc20Balances: []
      #- name: "TestToken"
      #  address: "0x94373a4919B3240D86eA41593D5eBa789FEF3848" # WETH9 on holesky
      #  decimals: 18
      #  minBalance: 10000000000000000 # 0.01 WETH

    # require at least one NFT from the specified contract
    nftContract: "0x0000000000000000000000000000000000000000"
    nftCollectionName: "Name of the project"
    nftCollectionUrl: "Link to the Collection"