2

I'm developing a scanner/checker for honeypot tokens and I was using it for a few days now to scan the BSC network. While using it I keep stumbling on one particular design many times a day, which is obviously a honeypot. I checked it using a small amount, you can buy but not sell. These tokens are all using the same source code.

Here is an example on bscscan.com: 0x7339601174cC7d7b8b5d87D026a3e41Ee0D3bbfE, but they are hundreds, even thousands.

The core of my detector is a contract function, which makes 2 swaps: one to buy and one to sell the token, also calculating the taxes and gas used along the way. I'm calling this function using eth_call in order to only simulate the swaps. Pretty classic.

I've tried every design that I know of to make my detector detect it, but it keeps telling that the 2 swaps are successful! I tried these designs:

  • a payable function receiving the initial funds via {value: amount}
  • a non-payable function receiving the initial funds via state and code override (the 3rd parameter of eth_call, see info here)
  • a payable constructor containing the checking code and using assembly to return the results

How do you think this token design does its evil magic blocking the sells only in mined transactions and not in simulated ones, regardless of the design I'm using? I know that the real implementation is in a separate "Accounting" contract, which doesn't show the source code.

In a more general context, is there any way for a solidity function to figure out how it was called (eth_call or eth_sendTransaction) and change the behavior accordingly?

Here is my constructor only checker:

pragma solidity >=0.8.12;

import "@uniswap/lib/contracts/libraries/TransferHelper.sol";
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol";
import "@uniswap/v2-periphery/contracts/interfaces/IWETH.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract ConstructorChecker {
    constructor (address _token, address _router) payable { 
        require(_token != address(0), "Checker: MISSING_TOKEN_PARAMETER");
        require(_router != address(0), "Checker: MISSING_ROUTER_PARAMETER");
        
        IUniswapV2Router02 router = IUniswapV2Router02(_router);
        IERC20 token = IERC20(_token);
        
        uint256 buyAmountOut;
        uint256 buyAmountOutExpected;

        uint256 sellAmountOut;
        uint256 sellAmountOutExpected;

        uint256 buyGas;
        uint256 sellGas;

        {
            // check if can buy token, ETH -> token
            uint256 buyAmountIn = address(this).balance;
            require(buyAmountIn > 0, "Checker: BUY_INPUT_ZERO");
            address[] memory pathBuy  = new address[](2);
            pathBuy[0] = router.WETH();
            pathBuy[1] = _token;
            buyAmountOutExpected = router.getAmountsOut(buyAmountIn, pathBuy)[1];
            //uint256 tokensBefore = token.balanceOf(address(this));
            uint256 startBuyGas = gasleft();
            try router.swapExactETHForTokensSupportingFeeOnTransferTokens {value: buyAmountIn} (1, pathBuy, address(this), block.timestamp){
                buyGas = startBuyGas - gasleft();
                buyAmountOut = token.balanceOf(address(this)); // - tokensBefore;
                require(buyAmountOut > 0, "Checker: BUY_OUTPUT_ZERO");
            }
            catch Error(string memory){
                revert("Checker: BUY_FAILED");
            }
        }

        {
            // check if can sell token, token -> ETH
            uint256 sellAmountIn = token.balanceOf(address(this));
            require(sellAmountIn > 0, "Checker: SELL_INPUT_ZERO");
            address[] memory pathSell  = new address[](2);
            pathSell[0] = _token;
            pathSell[1] = router.WETH();
            sellAmountOutExpected = router.getAmountsOut(sellAmountIn, pathSell)[1];
            TransferHelper.safeApprove( address(token), address(router), sellAmountIn);
            //uint256 ethBefore = address(this).balance;
            uint256 startSellGas = gasleft();
            try router.swapExactTokensForETHSupportingFeeOnTransferTokens(sellAmountIn, 1, pathSell, address(this), block.timestamp){
                sellGas = startSellGas - gasleft();
                sellAmountOut = address(this).balance; // - ethBefore;
                require(sellAmountOut > 0, "Checker: SELL_OUTPUT_ZERO");
            }
            catch Error(string memory){
                revert("Checker: SELL_FAILED");
            }
        }

        /*
        return [
            buyAmountOut,
            buyAmountOutExpected,
            sellAmountOut,
            sellAmountOutExpected,
            buyGas,
            sellGas
        ] ;
        */
        
        // returns(uint256[6] memory)
        assembly{
            let ret := mload(0x40)
            mstore(ret,            buyAmountOut)
            mstore(add(ret, 0x20), buyAmountOutExpected)
            mstore(add(ret, 0x40), sellAmountOut)
            mstore(add(ret, 0x60), sellAmountOutExpected)
            mstore(add(ret, 0x80), buyGas)
            mstore(add(ret, 0xa0), sellGas)
            
            return(ret, mul(0x20, 6))
        }
    }
}
2
  • I read about anti-bot protection on pinksale and it striked me, what if this honeypot type that's botherning me is using some sort of check to fool the checkers? It may allow to sell if the buy and sell are part of the same transaction, but block the sell if in different transaction.
    – Vladimir
    Commented Nov 7, 2022 at 10:49
  • I would like to know if you simulated anyway next block sell situation or not? Commented Jan 17, 2023 at 0:00

2 Answers 2

0

Like I mentioned in the comment, I might found an explanation. I read about anti-bot protection on pinksale and it hit me, what if this honeypot type that's bothering me is using some sort of check to fool the checkers?

Usually the checkers are doing a buy and sell swaps in the same transaction, as explained in the question. The rogue token may allow to sell if the buy and sell are part of the same transaction, which normally doesn't make much sense, and block the sell if in different transaction.

I wrote this answer to my own question because I couldn't find such information anywhere else, and perhaps someone else may be interested.

2
  • I was thinking to build a contract that checks if transfer and permir functions bytes are oppenzeppelin ones, just rounded the idea in my mind, what you think of this approach? Commented May 20 at 23:19
  • It is possilbe the contract is bypassing you cuz transfer has a deadline and b4 that deadline contrat works well but after it gets locked? Commented May 20 at 23:24
0

I've also read places where transaction gas or other opcodes can be used to distinguish an on-chain transaction from a simulated one.

https://github.com/Defi-Cartel/salmonella

Here's an example of working counter to contract simulators. Hope it helps.

Not the answer you're looking for? Browse other questions tagged or ask your own question.