文件预览

auditor.js

查看 Web3 Ops 技能包中的文件内容。

文件内容

actions/auditor.js

import axios from "axios";
import { ethers } from "ethers";
import {
  getWallet,
  getChainConfig,
  logInfo,
  logSuccess,
  logWarning,
  logError,
  printColor,
  resolveTokenAddress,
  SUPPORTED_CHAINS
} from "./common.js";

// Check security of a token contract address using GoPlus Security API
export async function analyzeTokenSecurity(options = {}) {
  const chainInput = options.chain;
  const tokenInput = options.token;

  try {
    const { chainConfig } = await getWallet(chainInput, options);
    
    if (!tokenInput) {
      throw new Error("Please specify a target --token <symbolOrAddress> to analyze.");
    }

    let tokenAddress = await resolveTokenAddress(tokenInput, chainConfig.id, options);
    
    if (tokenAddress === "0x0000000000000000000000000000000000000000") {
      // Native token is safe by default
      console.log(JSON.stringify({
        success: true,
        chain: chainConfig.name,
        tokenAddress: tokenAddress,
        token: chainConfig.symbol,
        securityScore: 100,
        isHoneypot: false,
        buyTaxPercent: 0,
        sellTaxPercent: 0,
        risks: [],
        summary: "Native token of the network is secure by architecture."
      }, null, 2));
      return;
    }

    // Check if tokenAddress is actually a liquidity pair address on DexScreener
    const chainKey = Object.keys(SUPPORTED_CHAINS).find(
      key => SUPPORTED_CHAINS[key].id === chainConfig.id
    );
    
    let isPairAddress = false;
    let originalQueryAddress = tokenAddress;
    let pairDetails = null;

    if (chainKey) {
      try {
        const dexPairUrl = `https://api.dexscreener.com/latest/dex/pairs/${chainKey}/${tokenAddress}`;
        const dexResponse = await axios.get(dexPairUrl, { timeout: 5000 });
        if (
          dexResponse.data &&
          (dexResponse.data.pair || (dexResponse.data.pairs && dexResponse.data.pairs.length > 0))
        ) {
          const pair = dexResponse.data.pair || dexResponse.data.pairs[0];
          if (pair.pairAddress.toLowerCase() === tokenAddress.toLowerCase()) {
            isPairAddress = true;
            pairDetails = {
              baseSymbol: pair.baseToken.symbol,
              quoteSymbol: pair.quoteToken.symbol,
              dexId: pair.dexId
            };
            
            logWarning(
              `[Warning] Alamat ${tokenAddress} adalah Liquidity Pool Pair (${pair.baseToken.symbol}/${pair.quoteToken.symbol} di ${pair.dexId}).`,
              options
            );
            logInfo(
              `Hinata secara otomatis mengalihkan audit ke token utama: ${pair.baseToken.name} (${pair.baseToken.symbol}) - ${pair.baseToken.address}`,
              options
            );
            
            // Redirect target audit address to the actual base token address
            tokenAddress = ethers.getAddress(pair.baseToken.address.toLowerCase());
          }
        }
      } catch (err) {
        // Suppress errors to not break execution if DexScreener is down
      }
    }

    logInfo(`Contacting GoPlus Security API for token audit (${tokenAddress})...`, options);
    
    const url = `https://api.gopluslabs.io/api/v1/token_security/${chainConfig.id}?contract_addresses=${tokenAddress}`;
    const response = await axios.get(url, { timeout: 8000 });
    
    if (!response.data || response.data.code !== 1 || !response.data.result || !response.data.result[tokenAddress.toLowerCase()]) {
      throw new Error("GoPlus Security API failed to return data for this token.");
    }
    
    const auditData = response.data.result[tokenAddress.toLowerCase()];
    
    // Parse individual risk metrics
    const isHoneypot = auditData.is_honeypot === "1";
    const buyTax = parseFloat(auditData.buy_tax || 0) * 100;
    const sellTax = parseFloat(auditData.sell_tax || 0) * 100;
    const ownerAddress = auditData.owner_address;
    const isMintable = auditData.is_mintable === "1";
    const isProxy = auditData.is_proxy === "1";
    const ownerChangeBalance = auditData.owner_change_balance === "1";
    const slippageModifiable = auditData.slippage_modifiable === "1";
    const transferPausable = auditData.transfer_pausable === "1";
    const cannotSellAll = auditData.cannot_sell_all === "1";
    
    const isOwnerRenounced = !ownerAddress || 
      ownerAddress === "0x0000000000000000000000000000000000000000" ||
      ownerAddress.toLowerCase() === "0x0000000000000000000000000000000000000001" ||
      ownerAddress.toLowerCase() === "0x000000000000000000000000000000000000dead";
      
    // Calculate custom security score
    let score = 100;
    const risks = [];
    
    if (isHoneypot) {
      score -= 100;
      risks.push({ severity: "CRITICAL", type: "HONEYPOT", desc: "Token cannot be sold. This is a scam honeypot contract." });
    }
    if (ownerChangeBalance) {
      score -= 50;
      risks.push({ severity: "CRITICAL", type: "OWNER_MODIFY_BALANCE", desc: "Contract owner has the ability to directly change or drain user balances." });
    }
    if (buyTax > 15 || sellTax > 15) {
      score -= 25;
      risks.push({ severity: "HIGH", type: "HIGH_TAX", desc: `Extremely high trading tax (Buy: ${buyTax}%, Sell: ${sellTax}%).` });
    }
    if (cannotSellAll) {
      score -= 25;
      risks.push({ severity: "HIGH", type: "CANNOT_SELL_ALL", desc: "Restrictions apply to selling the maximum holding balance." });
    }
    if (transferPausable) {
      score -= 15;
      risks.push({ severity: "MEDIUM", type: "TRANSFER_PAUSABLE", desc: "Contract owner can pause/freeze all trading at any time." });
    }
    if (slippageModifiable) {
      score -= 15;
      risks.push({ severity: "MEDIUM", type: "SLIPPAGE_MODIFIABLE", desc: "Contract owner can dynamically modify trading slippage parameters." });
    }
    if (isProxy) {
      score -= 10;
      risks.push({ severity: "MEDIUM", type: "PROXY_CONTRACT", desc: "This is a proxy contract; implementation code can be changed in the future." });
    }
    if (isMintable) {
      score -= 10;
      risks.push({ severity: "LOW", type: "MINTABLE", desc: "Owner can mint additional tokens, causing potential dilution." });
    }
    if (!isOwnerRenounced) {
      score -= 15;
      risks.push({ severity: "LOW", type: "OWNERSHIP_NOT_RENOUNCED", desc: `Contract owner exists (${ownerAddress}) and can invoke admin controls.` });
    }
    
    score = Math.max(0, score);
    
    logSuccess(`Token security audit complete`, options);
    
    if (!options.json) {
      console.log(`\n==================================================`);
      console.log(printColor(`SMART CONTRACT SECURITY AUDIT`, "bold"));
      console.log(`Token     : ${auditData.token_symbol || tokenInput} (${auditData.token_name || "Unknown"})`);
      console.log(`Address   : ${tokenAddress}`);
      console.log(`Network   : ${chainConfig.name}`);
      
      let scoreColor = "green";
      if (score < 50) scoreColor = "red";
      else if (score < 80) scoreColor = "yellow";
      
      console.log(`Security Score: ${printColor(`${score}/100`, scoreColor)}`);
      console.log(`--------------------------------------------------`);
      console.log(`Honeypot Check    : ${isHoneypot ? printColor("HONEYPOT DETECTED (DANGER)", "red") : printColor("Safe (Sellable)", "green")}`);
      console.log(`Trading Tax       : Buy: ${buyTax}%, Sell: ${sellTax}%`);
      console.log(`Proxy Contract    : ${isProxy ? "Yes (Owner modifiable)" : "No"}`);
      console.log(`Mintable Supply   : ${isMintable ? "Yes (Owner can mint)" : "No"}`);
      console.log(`Ownership Status  : ${isOwnerRenounced ? printColor("Renounced (Safe)", "green") : printColor("Active (Owner Exists)", "yellow")}`);
      
      if (risks.length > 0) {
        console.log(`--------------------------------------------------`);
        console.log(printColor(`RISK FINDINGS (${risks.length}):`, "bold"));
        for (const risk of risks) {
          let riskColor = "gray";
          if (risk.severity === "CRITICAL") riskColor = "red";
          else if (risk.severity === "HIGH") riskColor = "red";
          else if (risk.severity === "MEDIUM") riskColor = "yellow";
          
          console.log(` [${printColor(risk.severity, riskColor)}] ${risk.desc}`);
        }
      } else {
        console.log(`--------------------------------------------------`);
        console.log(printColor("✔ No dangerous risks found on the smart contract.", "green"));
      }
      console.log(`==================================================\n`);
    }
    
    console.log(JSON.stringify({
      success: true,
      chain: chainConfig.name,
      tokenAddress: tokenAddress,
      tokenSymbol: auditData.token_symbol || tokenInput,
      tokenName: auditData.token_name || "Unknown",
      securityScore: score,
      isHoneypot: isHoneypot,
      buyTaxPercent: buyTax,
      sellTaxPercent: sellTax,
      isProxy: isProxy,
      isMintable: isMintable,
      isOwnerRenounced: isOwnerRenounced,
      ownerAddress: ownerAddress,
      risks: risks,
      isPairAddress: isPairAddress,
      originalQueryAddress: originalQueryAddress,
      pairDetails: pairDetails
    }, null, 2));

  } catch (error) {
    console.error(JSON.stringify({
      success: false,
      error: error.message
    }, null, 2));
  }
}