Background
In CertiK's previous article "On-chain IPO game, large-scale RugPull techniques 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 did not only use one RugPull technique.
The previous article used the MUMI token as an example to describe the RugPull technique of the gang behind the address: Directly modify the token balance of the tax address through the code backdoor, but did not modify the total supply of the token, nor send a Transfer event, so that users who viewed etherscan could not discover the project party's behavior of secretly minting tokens.
Today's article uses the token "ZhongHua" as an example to analyze another RugPull method of 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.
In-depth scam
In this case, the project party 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, let's sort out the context of the incident from the beginning.
Deploy tokens
At 1:40 am on January 18 (UTC time, the same below), the attacker address (?0x74fc) deployed an ERC20 token named 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 the token was created), the attacker address (?0x74fc) granted the approve permission for ZhongHua tokens to Uniswap V2 Router in preparation for adding liquidity.
One minute later, the attacker address (?0x74fc) called the addLiquidityETH function in Router to add liquidity to create the ZhongHua-WETH liquidity pool (?0x5c8b), added 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 to add liquidity. By looking at the source code of the token contract, it is found that a _getAmount function is implemented, which 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 is set to 0. _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 when deployed.
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) will send all 1.225 LP tokens obtained by adding liquidity directly to the 0xdead address to complete the permanent lock of LP tokens.
As in the case of MUMI tokens, when LP is locked, in theory, the attacker address (?0x74fc) no longer has the ability to perform 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.
So far, from the user's point of view, all pre-mined tokens are used to add to the liquidity pool, and no abnormal situation occurs.
RugPull
At 2:10 am (about 30 minutes after the ZhongHua token was created), the attacker address 2 (?0x5100) deployed an attack contract (?0xc403) specifically for RugPull.
As in the case of MUMI tokens, the project did not use the attack address that deployed the ZhongHua token contract, and the attack contract used for RugPull was not open source, the purpose was to increase the difficulty of technical personnel to trace the source, most of the RugPull scams have such characteristics.
At 7:46 am (about 6 hours after the token contract was created), the attacker address 2 (?0x5100) performed a RugPull.
By calling the "swapExactETHForTokens" method of the attack contract (?0xc403), he transferred 999 billion ZhongHua tokens from the attack contract to exchange for about 5.884 ETH, and exhausted 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 for UniswapV2 The Router grants the maximum number of ZhongHua token transfer permissions, and then converts the caller's specified number of "xt" ZhongHua tokens (owned by the attack contract (?0xc403)) into ETH through the Router and sends 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, corresponding to 999 billion ZhongHua tokens (ZhongHua's decimal is 9).
Finally, the project party 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 know 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 is still 1 billion, but the number of tokens sold by the attack contract (?0xc403) is 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 MUMI tokens, the attack contract (?0xc403) in the ZhongHua token case also had no ERC20 token transfer event.
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 in 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 tokens in the attack contract (?0xc403) also seem to appear out of thin air, so we also search 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, which are 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 shown in the MUMI token in the figure below.
More importantly, the MUMI token contract did not trigger the Tranfer event when directly modifying the balance of the tax contract. 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, whether it is the "_getAmount", "_transferFrom" or "_transferBasic" function, they all correctly trigger the Transfer event after modifying the balance, which conflicts with the situation that we could not find the Tranfer event of the token transfer when querying the Transfer event related to the attack contract (?0xc403) earlier.
Is it possible that unlike the MUMI case, the tokens in this attack contract (?0xc403) really appeared out of thin air?
Method Revealed
Where did the tokens in the attack contract come from?
In the process of analyzing the case, when we found that every modification of the balance in the ZhongHua contract correctly triggered the Transfer event, 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 checked 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 seems that the "transfer" function performs a token transfer operation, and the Transfer event will be triggered after the transfer is completed.
Butbefore 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 it is, no tax will be collected and the token 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 money from 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.
The problem becomes very clear at this point: When the tax-free address is used as the sender to transfer money, 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 was exactly the attack contract (?0xc403).
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
Using 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 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 party also sold tokens 11 times through the attack contract (?0xc403) in the middle, and obtained a total of 9.64ETH; plus the last RugPull transaction, a total of 15.52ETH was obtained. The cost is only 1.5 ETH for adding liquidity, a small amount of handling fees for deploying contracts, and a small amount of ETH for inducing new robots to actively exchange.
The project party even used different EOA addresses to call the attack contract (?0xc403) to sell tokens in the middle, which seemed to be different senders selling tokens to disguise their true intention of continuous cashing out.
Summary
Now looking back at the entire ZhongHua token RugPull case, we find that the method itself is very simple, just canceling 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. The perspectives of security protection and attack are different. For security practitioners, the balance check in the code is the most basic security guarantee that needs to be completed. Therefore, most security practitioners will subconsciously think 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 representation, the RugPull technique in the ZhongHua token case leaves the least traces, and it is much more difficult to track than other types of RugPull. In the end, it is still necessary to locate the code backdoor through manual audit code.
2. The project party is consciously covering up the backdoor code that does not require the verification of the balance of the privileged address. The project party 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 token seem reasonable. When other ordinary addresses transfer money, it is no different from normal behavior. Without carefully reading the code, no clues can be found.
Comparing the RugPull cases of this team for 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 RugPull case of MUMI tokens, the project party directly modified the balance without modifying the totalSupply, and did not trigger the Transfer event, so that users could not perceive that the privileged address already had a huge amount of tokens.
The ZhongHua token case is more thorough. By not directly checking the balance of the privileged address, any means other than looking at the source code cannot find that the privileged address already has unlimited tokens (using balanceOf to query the balance of the privileged address will show 0, but it can transfer unlimited tokens).
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 cannot prevent villains. Attackers often hide hard-to-find backdoors while implementing standard business logic. If the token behavior is standardized, although the flexibility of the function is reduced, the possibility of hiding backdoors is avoided, providing more security.