web3eye.eth

Posted on Apr 01, 2022Read on Mirror.xyz

安全第一 | 2. 如何识别貔貅 Token

貔貅(pí xiū)——为中国传说的一种瑞兽。以金银珠宝为食,而且嘴大食四方,吃得自己浑身珠光宝气!更奇特的是,它没有屁屁,财宝只进不出。

在吉祥物的排行榜里你可能非常想拥有的就是它,而在加密货币领域,貔貅盘的意思只能买进不能卖出,有的是只有项目方的地址才能卖出,有的是只能卖一次或者只能小额才能卖出等等。

利用投资者 “不想错过新币,想尽快抢先买进获利卖出” 的心理,此时的投资者因为幻想着梭哈暴富,所以防范会降低,一不小心就会点击群里的链接或者复制不经过验证的合约地址进行交易,交易过后才知道被骗。

我们也梳理了验证貔貅 Token 的几个技巧:

一、看合约是否已通过验证开源

合约开源的一个好处就是增加合约的公信力,接受群众的监督。新 Token 的发行,稍微有点实力的项目方都会选择开源。打开 https://etherscan.io/,输入 Token 合约地址,如果 Contract 旁边有绿色的 ✅,说明项目方已验证的合约来源,可以直接查看合约的 Source Code,相反则没有。

当然,即使开源了合约代码,也可能存在猫腻,例如通过代理合约调用隐藏合约的方式。

二、Token 持有人分析

持有人的数量可以侧面反应 Token 的分散程度,若 Token 集中化地归合约管理员或者少数几个人持有,当他们出售 Token 时,可能会对代币价格产生很大波动影响。

同样,etherscan 提供了 Holders 分析功能,可以看到当前 Token 由多少用户持有,以及持有比例等。持有人越多、单个用户持有比例越低,则 Token 被大户操盘的影响概率就越小。

然而,有的时候,我们也很容易被 etherscan 展示出来的内容所“蒙骗”,除了直接查看持有人分析结果外,还应该深入了解下这些 Token 是如何分发出去的。上图可见,Token 的最大持有人是 Vitalik(etherscan 已标记 Vb),有足够的社区公信力,其他的持有人份额都不大。切换到Transfers 标签,查看 Token 关联的交易:

合约创建时,即铸造了所有的 Token 并转移给 Vb,然后又由 Vb 账户分发到其他的地址,随后在 Uniswap 上创建了流动性。我们选择 0xdb2a0c...ce2c90 交易 hash 进行查看,不难发现问题,这笔交易的发起人不是 Vb,但 Vb 账户的 Token 却被转移走了,表明别人可以通过 Token 合约随意操控他人的账户。

三、Swap 流动性分析

Swap 流动性不足,会导致你的 Token 成为没有意义的一串数字。

a. 打开 https://www.dextools.io/,找到对应的 Token 交易币对,查看 Token 交易历史,可以观察以下特征进行判断: 1. 只有买单,没有卖单; 2. 有大量买单,仅有一两个卖单; 3. 卖单地址是否是同一个地址;4. 卖单金额是否有明显特征。

b. 打开 https://honeypot.is/,输入 Token 地址,验证其是否存在 Swap 买卖限制。

如果以上验证步骤均存在疑点,强烈建议您保持清醒,远离它,举报它!

若你对 honeypot.is 技术验证原理感兴趣,我们也提供了一个 demo,供君查阅。

------------------------华丽的分割线-------------------------

为了能模拟交易,而无须每次在链上发起真实交易,可以利用 RPC 接口支持自定义代码的特性实现,geth 1.9.2 版本后开始支持(https://github.com/ethereum/go-ethereum/issues/19836)。例如:

$ curl localhost:8545 -d '{
    "jsonrpc": "2.0",
    "id": 13,
    "method": "eth_call",
    "params": [
       {
            "to": "0x0000000000000000000000000000000000000111"
       },
        "latest",
       {"0x0000000000000000000000000000000000000111": {
        "code": "0x6080604052600760005260206000f3fe"
        }
       }
   ]
}' -H 'content-type: application/json'
{"jsonrpc":"2.0","id":13,"result":"0x0000000000000000000000000000000000000000000000000000000000000007"}

上面的调用会将 0x0000000000000000000000000000000000000111 地址覆盖为合约,合约代码为 0x6080604052600760005260206000f3fe,然而真实链上并不存在对应的合约地址。

web3 nodejs sdk 可以通过如下方式进行调用:

web3.extend({
  methods: [
   {
      name: "customCall",
      call: "eth_call",
      params: 3,
   },
 ],
});

web3.customCall({
 to: "0x0000000000000000000000000000000000000111",
 from: "0x0000000000000000000000000000000000000112"
},
"latest",
{
 "0x0000000000000000000000000000000000000111": {
  "code": "0x6080604052600760005260206000f3fe"
 }
})

好了,知道了检测原理和方法,接下来进行实操。首先,编写一个用户链上检测的合约 token_checker.sol

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.0;


import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract TokenChecker {
    using SafeMath for uint256;
    uint256 private constant DEADLINE = 30 minutes;
    address private constant UNISWAP_V2_ROUTER_ADDRESS = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
    IUniswapV2Router02 private uniswapRouter;

    function check(address tokenAddress) external payable returns (uint256, uint256, uint256, uint256) {
        uniswapRouter = IUniswapV2Router02(UNISWAP_V2_ROUTER_ADDRESS);
       (uint256 buyExpectedOut, uint256 buyActualOut) = buy(tokenAddress, msg.value);
       (uint256 sellExpectedOut, uint256 sellActualOut) = sell(tokenAddress, buyActualOut);
        return (buyExpectedOut, buyActualOut, sellExpectedOut, sellActualOut);
   }

    //eth -> tokens 检测通过ETH买入Token
    function buy(address tokenAddress, uint256 amountEthIn) internal returns (uint256, uint256) {
        IERC20 token = IERC20(tokenAddress);
        address senderAddress = address(this);
        //eth to tokens
        require(amountEthIn > 0, " INSUFFICIENT_INPUT_AMOUNT");
        address[] memory eth2TokenPath = getPathForEthToToken(tokenAddress);
        uint256 buyExpectedOut = uniswapRouter.getAmountsOut(amountEthIn, eth2TokenPath)[1];

        uint256 initBalance = token.balanceOf(senderAddress);
        uniswapRouter.swapExactETHForTokensSupportingFeeOnTransferTokens{
            value: amountEthIn
       }(
            buyExpectedOut,
            eth2TokenPath,
            senderAddress,
            block.timestamp + DEADLINE
       );
        uint256 buyActualOut = token.balanceOf(senderAddress).sub(initBalance);
        require(buyActualOut > 0, "buyActualOut > 0");
        return (buyExpectedOut, buyActualOut);
   }

    //token -> eth 检测通过卖出Token,获得ETH
    function sell(address tokenAddress, uint256 amountTokenIn) private returns (uint256, uint256) {
        IERC20 token = IERC20(tokenAddress);
        address senderAddress = address(this);

        //eth to tokens
        require(amountTokenIn > 0, " INSUFFICIENT_INPUT_AMOUNT");
        address[] memory token2EthPath = getPathForTokenToEth(tokenAddress);

        //tokens to eth
        uint256 sellExpectedOut = uniswapRouter.getAmountsOut(amountTokenIn, token2EthPath)[1];
        require(
            token.approve(UNISWAP_V2_ROUTER_ADDRESS, amountTokenIn),
            "Approve Failed."
       );
        uint256 initEthBalance = senderAddress.balance;
        uniswapRouter.swapExactTokensForETHSupportingFeeOnTransferTokens(
            amountTokenIn,
            sellExpectedOut,
            token2EthPath,
            senderAddress,
            block.timestamp + DEADLINE
       );
        uint256 sellActualOut = senderAddress.balance.sub(initEthBalance);
        return (sellExpectedOut, sellActualOut);
   }

    function getPathForEthToToken(address tokenAddress) private view returns (address[] memory) {
        address[] memory path = new address[](2);
        path[0] = uniswapRouter.WETH();
        path[1] = tokenAddress;
        return path;
   }

    function getPathForTokenToEth(address tokenAddress) private view returns (address[] memory) {
        address[] memory path = new address[](2);
        path[0] = tokenAddress;
        path[1] = uniswapRouter.WETH();
        return path;
   }

    fallback() external payable {}
}

在 Remix 中进行编译,点击 Compilation Details,拷贝 FUNCTIONHASHES 里面 check(address) 函数对应的 Hash —— c23697a8,以及 RUNTIME BYTECODE 中对应的 object 代码,以备后用。

然后,我们来编写用于检测的执行脚本 tokenChecker.js

(async () => {
  const Web3 = require("web3");

  var web3 = new Web3("https://main-light.eth.linkpool.io/");

  let address = "0xc770EEfAd204B5180dF6a14Ee197D99d808ee52d";  //待检测Token地址
  check(address);

  function check(tokenAddress) {
    // 扩展SDK接口
    web3.extend({
      methods: [
       {
          name: "customCall",
          call: "eth_call",
          params: 3,
       },
     ],
   });

    let customCode = "0x......."; // '0x' + RUNTIME_BYTECODE;

    let encodedAddress = web3.eth.abi.encodeParameter("address", tokenAddress);
    let contractFuncData = "0xc23697a8";  // '0x' + FUNCTION HASH
    let callData = contractFuncData + encodedAddress.substring(2);
    let val = 50000000000000000;    // Uniswap交易需要输入ETH,这里模拟交易0.05ETH,实际场景还应根据Token交易池最大供应量调整输入

    web3
     .customCall(
       {
          to: "0x0000000000000000000000000000000000000111",
          from: "0x0000000000000000000000000000000000000112",
          value: "0x" + val.toString(16),
          gas: "0x" + (45000000).toString(16),
          data: callData,
       },
        "latest",
       {
          "0x0000000000000000000000000000000000000111": {
            code: customCode, // 将该地址合约内容替换为前面编译的内容
         },
          "0x0000000000000000000000000000000000000112": {
            balance: "0x" + (100000000000000000000).toString(16),  // 给当前地址Mock足够的ETH余额
         },
       }
     )
     .then((val) => {
        let decoded = web3.eth.abi.decodeParameters(
         ["uint256", "uint256", "uint256", "uint256"],
          val
       );
        let buyExpectedOut = web3.utils.toBN(decoded[0]);
        let buyActualOut = web3.utils.toBN(decoded[1]);
        let sellExpectedOut = web3.utils.toBN(decoded[2]);
        let sellActualOut = web3.utils.toBN(decoded[3]);
        // 判断交易买入和卖出Token时收取了多少手续费,如果手续费过大,则判定为貔貅Token
        const buyFee = ((buyExpectedOut - buyActualOut) / buyExpectedOut) * 100;
        const sellFee =
         ((sellExpectedOut - sellActualOut) / sellExpectedOut) * 100;  
        console.log("Buy Fees: " + buyFee + "%, Sell Fees: " + sellFee + "%");
     })
     .catch((e) => {
      // 貔貅Token在检测时通常会抛出转出金额不足的异常
        // 若输入EOA地址,无法检测识别时,也会抛出execution reverted的异常
        console.error(e);
     });
 }
})();

接下来,就是见证奇迹的时刻了,替换 address 为待检测的 Token 地址,根据检测结果,即可判定是否为貔貅 Token 了。

关于我们

Web3Eye 是一个专注于技术研究和分享的 Web3 加密技术社区,聚集了一批拥有多年区块链研发经验和安全技术能力的加密 OG,以帮助更多人安全地进入 Web3 世界,欢迎关注我们的 Twitter 帐号,了解最新动态。

Recommended Reading