background

In CertiK’s previous article “On-chain IPO Game, Large-scale RugPull Technique Revealed”, a large-scale exit scam automated harvester address 0xdf1a targeting IPO robots was revealed. This address completed more than 200 exit scams (hereinafter referred to as RugPull) in just about two months, but this gang does not only use one RugPull technique.

In the previous article, the RugPull method of the gang behind the address was described using the MUMI token as an example: the token balance of the tax address was directly modified through the code backdoor, but the total supply of the token was not modified, and no Transfer event was sent, so that users who checked etherscan could not discover the project party's secret minting of tokens.

Today's article uses the token "ZhongHua" as an example to analyze another RugPull method used by the gang: using complex tax function logic to cover up the transfer function that can be used for RugPull. Next, we will use the "ZhongHua" token case to analyze the details of another RugPull method at address 0xdf1a.

Deep into the scam

In this case, the project team exchanged a total of 999 billion ZhongHua for about 5.884 WETH, draining the liquidity of the pool. In order to gain a deeper understanding of the entire RugPull scam, we will sort out the context of the incident from the beginning.

Deployment Tokens

At 1:40 am on January 18 (UTC time, the same below), the attacker address (?0x74fc) deployed an ERC20 token called ZhongHua (?0x71d7) and pre-mined 1 billion tokens and sent them to the attacker address (?0x74fcfc).

The number of pre-mined tokens is consistent with the number defined in the contract source code.

Adding Liquidity

At 1:50 (10 minutes after token creation), the attacker address (?0x74fc) granted the Uniswap V2 Router the approval permission for ZhongHua tokens in preparation for adding liquidity.

One minute later, the attacker’s address (?0x74fc) called the addLiquidityETH function in the Router to add liquidity to create the ZhongHua-WETH liquidity pool (?0x5c8b), adding all pre-mined tokens and 1.5 ETH to the liquidity pool, and finally obtained about 1.225 LP tokens.

From the above token transfer records, we can see that there is a transfer in which the attacker (?0x74fc) sent 0 tokens to the ZhongHua token contract itself.

This transfer is not a regular transfer for adding liquidity. By looking at the source code of the token contract, it is found that a _getAmount function is implemented in it. This function is responsible for deducting money from the from address of the transfer and calculating the handling fee to be charged, and then sending the handling fee to the token address, and then triggering the Transfer event indicating that the token address has received the handling fee.

The _getAmount function will determine whether the sender of the transfer is the _owner. If it is the _owner, the handling fee will be set to 0. The _owner is assigned by the input parameter of the constructor when the Ownable contract is deployed.

The ZhongHua token contract inherits the Ownable contract and uses the deployer msg.sender as the input parameter of the Ownable constructor during deployment.

Therefore, the attacker’s address (?0x74fc) is the _owner of the token contract. The 0 token transfer that adds liquidity is sent through the _getAmount function, because _getAmount will be called in the transfer and transferFrom functions.

Permanently lock liquidity

At 1:51 (within 1 minute of the creation of the liquidity pool), the attacker address (?0x74fc) sent all 1.225 LP tokens obtained by adding liquidity directly to the 0xdead address to complete the permanent lock of the LP tokens.

As in the MUMI token case, when the LP is locked, the attacker's address (?0x74fc) theoretically no longer has the ability to perform a RugPull by removing liquidity. In the RugPull scam targeting new bots led by address 0xdf1a, this step is mainly used to deceive the anti-fraud script of the new bot.

At this point, it seems to the user that all pre-mined tokens are used to add to the liquidity pool, and no abnormal situation has occurred.

RugPull

At 2:10 a.m. (about 30 minutes after the ZhongHua token was created), the attacker address 2 (?0x5100) deployed an attack contract (?0xc403) specifically for RugPull.

As with the case of the MUMI token, the project owner did not use the attack address for deploying the ZhongHua token contract, and the attack contract used for RugPull was not open source. The purpose was to increase the difficulty for technical personnel to trace the source. Most RugPull scams have such characteristics.

At 7:46am (about 6 hours after the token contract was created), the attacker address 2 (?0x5100) performed a RugPull.

He transferred 999 billion ZhongHua tokens from the attack contract (?0xc403) and exchanged them for about 5.884 ETH by calling the “swapExactETHForTokens” method of the attack contract (?0xc403), draining most of the liquidity in the pool.

Since the attack contract (?0xc403) is not open source, we decompiled its bytecode and the results are as follows:

https://app.dedaub.com/ethereum/address/0xc40343c5d0e9744a7dfd8eb7cd311e9cec49bd2e/decompiled

The main function of the "swapExactETHForTokens" function of the attack contract (?0xc403) is to first use approve to grant the UniswapV2 Router the maximum number of ZhongHua token transfer permissions, and then use the Router to convert the caller's specified number of "xt" ZhongHua tokens (owned by the attack contract (?0xc403)) into ETH and send it to the "_rescue" address declared in the attack contract (?0xc403).

It can be seen that the address corresponding to “_rescue” is the deployer of the attack contract (?0xc403): attacker address 2 (?0x5100).

The input parameter xt of this RugPull transaction is 999,000,000,000,000,000,000,000, which corresponds to 999 billion ZhongHua tokens (ZhongHua’s decimal is 9).

In the end, the project used 999 billion ZhongHua to drain the WETH in the liquidity pool and complete the RugPull.

As with the MUMI case in the previous article, we need to first confirm the source of the ZhongHua tokens in the attack contract (?0xc403). From the previous article, we learned that the total supply of ZhongHua tokens is 1 billion. After the RugPull ended, the total supply of ZhongHua tokens we queried in the block browser was still 1 billion, but the number of tokens sold by the attack contract (?0xc403) was 999 billion, which is 999 times the total supply recorded in the contract. Where do these tokens that far exceed the total supply come from?

We checked the ERC20 transfer event history of the contract and found that, like the RugPull case of the MUMI token, the attack contract (?0xc403) in the ZhongHua token case also had no ERC20 token transfer events.

In the case of MUMI, the tokens of the tax contract come from the modification of the balance directly in the token contract, which makes the tax contract directly have tokens far exceeding the total supply. Since the MUMI token contract does not modify the totalSupply of the token when modifying the balance, nor does it trigger the Transfer event, we cannot see the transfer record of the tokens of the tax contract in the MUMI case, as if the tokens used by the tax contract for RugPull appeared out of thin air.

Back to the ZhongHua case, the ZhongHua token in the attack contract (?0xc403) also seemed to appear out of thin air, so we also searched for the keyword "balance" in the ZhongHua token contract.

The results show that there are only three modifications to the balance variable in the entire token contract, in the "_getAmount", "_transferFrom" and "_transferBasic" functions.

Among them, "_getAmount" is used to handle the logic of collecting transfer fees, while "_transferFrom" and "_transferBasic" are used to handle the transfer logic. There is no statement that directly modifies the balance as obviously as the MUMI token in the figure below.

More importantly, when the MUMI token contract directly modifies the balance of the tax contract, no Tranfer event is triggered. This is why we cannot query the token transfer event of the tax contract in the block browser, but the tax contract can have a large number of tokens.

However, in the ZhongHua token contract, no matter the "_getAmount", "_transferFrom" or "_transferBasic" functions, they all correctly trigger the Transfer event after modifying the balance. This conflicts with the situation that we could not find the Tranfer event of the token transfer when we queried the Transfer event related to the attack contract (?0xc403) earlier.

Could it be that unlike the MUMI case, the tokens in this attack contract (?0xc403) really appeared out of thin air?

Technique Revealed

Where do the tokens for attacking contracts come from?

In the process of analyzing the case, when we found that every time the balance was modified in the ZhongHua contract, the Transfer event was correctly triggered, but we could not find the token transfer record or Transfer event related to the attack contract (?0xc403), we needed to find a new analysis idea.

We queried a large number of transfer records and once used the "performZhongSwap" function in the contract as a breakthrough point. This function is responsible for selling tokens in the token contract. In other RugPull events we analyzed, there are many cases where this type of function is used as a RugPull backdoor.

Despite checking other functions, nothing was found. So we began to focus on the "transfer" function itself. No matter how the attacker performs RugPull, the implementation logic of the "transfer" function must contain the most important information.

Fatal Transfer

The "transfer" function in the token contract directly calls the "_transferFrom" function.

It looks like the "transfer" function performs a token transfer operation, and a Transfer event is triggered after the transfer is completed.

But before transferring tokens, the "transfer" function will first use the "_isNotTax" function to determine whether the sender of the transfer is a tax-free address: if not, the "_getAmount" function will be used to collect taxes; if so, no tax will be collected and the tokens will be sent directly to the recipient. And this is where the problem lies.

As mentioned earlier, in the implementation of "_getAmount", the token contract verifies the sender's balance, deducts the sender, and then sends the handling fee to the token contract.

The problem is that "_getAmount" is only called when the sender is not a tax-free address. When the sender is a tax-free address, the amount is directly added to the recipient's balance.

At this point, the problem becomes very clear: when the tax-free address is used as the sender to transfer funds, the token contract does not check whether the sender's balance is sufficient, and does not even subtract the amount from the sender's balance. This means that as long as it is a tax-free address defined by the token contract, any number of tokens can be sent to any address. This is why the attack contract (?0xc403) can directly transfer 999 times the total supply of tokens.

After inspection, it was found that the token contract only set _taxReceipt as the tax-free address in the constructor, and the address corresponding to _taxReceipt is exactly the attack contract (?0xc403).

Since then, the RugPull method of ZhongHua tokens has been confirmed: the attacker used specific logic to circumvent the balance check of the privileged address, allowing the privileged address to transfer tokens out of thin air, thereby completing the RugPull.

How to profit

Exploiting the above vulnerability, the attacker address 2 (?0x5100) directly calls the privileged attack contract (?0xc403) "swapExactETHForTokens" to complete the RugPull. In the "swapExactETHForTokens" function, the attack contract (?0xc403) grants token transfer permissions to the Uniswap V2 Router, and then directly calls the Router's token exchange function to exchange 5.88 ETH in the pool with 999 billion ZhongHua tokens.

In fact, in addition to the above-mentioned RugPull transaction, the project team also sold tokens 11 times in the middle through the attack contract (?0xc403), and obtained a total of 9.64ETH; plus the last RugPull transaction, a total of 15.52ETH was obtained. The cost was only 1.5 ETH for adding liquidity, a small amount of handling fees for deploying contracts, and a small amount of ETH used to induce new bots to actively exchange.

The project team even used different EOA addresses to call the attack contract (?0xc403) to sell tokens, making it look like different senders were selling tokens in order to disguise their true intention of continuously cashing out.

Summarize

Now, looking back at the RugPull case of ZhongHua tokens, we find that the method itself is very simple, which is just to cancel the token balance check of the privileged address. But why was it not so smooth when analyzing this case? There may be two main reasons:

1. Security protection and attack have different perspectives. For security practitioners, the balance check in the code is the most basic security guarantee that needs to be completed. Therefore, most security practitioners subconsciously believe that the "transfer" function will naturally complete the verification of the user's balance and relax their vigilance against such vulnerabilities (or think that such vulnerabilities are too basic and attackers will not use them).

However, from the attacker's perspective, the most effective attack method is often the simplest: not checking the balance is an effective and easily overlooked RugPull technique, and there is no reason not to use it. This is indeed the case. At least from the case characterization, the RugPull technique in the ZhongHua token case left the least traces, and it is much more difficult to track than other types of RugPull. In the end, it is still necessary to manually audit the code to locate the code backdoor.

2. The project owner intentionally concealed the backdoor code that does not require the verification of the balance of the privileged address. The project owner even implemented a complete set of tax transfer calculation logic and token address withdrawal and reinvestment logic for non-privileged addresses, making the complex transfer logic of the tokens look reasonable. The transfers to other ordinary addresses are no different from normal behavior, and no clues can be found without carefully reading the code.

Comparing the team's RugPull cases against MUMI tokens and ZhongHua tokens, both of them use relatively covert methods to allow privileged addresses to have the right to control a large number of tokens.

In the MUMI token RugPull case, the project party directly modified the balance without modifying the totalSupply or triggering the Transfer event, making it impossible for users to perceive that the privileged address already had a huge amount of tokens.

The ZhongHua token case is more thorough. By not checking the balance of the privileged address directly, it is impossible to find out that the privileged address already has unlimited tokens by any means other than looking at the source code (using balanceOf to query the balance of the privileged address will show 0, but unlimited tokens can be transferred).

The RugPull case of ZhongHua tokens reflects the potential security issues of token standards. In terms of security, the ERC20 token standard can only be used to constrain gentlemen but not to prevent villains. Attackers often hide backdoors that are difficult to detect while implementing business logic that meets the standards. If the token behavior is standardized, although the flexibility of the function is reduced, the possibility of hiding backdoors is avoided, providing more security guarantees.