HarryPotterObamaJustinHuobiSun Rug Pull

Reported loss $43K
Rugpull

What happened

HarryPotterObamaJustinHuobiSun (symbol USDT) is a copycat ERC20 token on Ethereum that trades on Uniswap and should not be confused with Tether USD. CertiK Alert reported a large liquidity removal on it by the deployer, about 50k USD, on 2023-08-23. De.Fi's REKT database lists the loss as 26.07 ETH, equivalent to 42,696 USD, and says the stolen funds were moved to a couple of externally owned accounts and then distributed further.

Its source mints a supply of 420,690,000 tokens to the deployer, and the same _mint function contains an assembly block that writes a separate, far larger balance for 0xf5e1a4da…9af035 that is not counted in totalSupply. The contract also gives the owner an owner-only yoink function that freezes any address from sending or receiving, and exposes public functions that emit fake Swap and Transfer events without moving any balance. Nothing in the contract restricts removing liquidity from the pool.

from De.Fi's listing: deployer 0x4Fe24527…7f451d; scammer addresses 0xC49a11fE…907368 and 0xD100b4F8…0eB030; liquidity adding transaction 0x54d76c2c…b443e3; liquidity removal transaction 0xce1f9894…637ccb.

How it happened

What the code allows:

  1. The constructor in USDT.sol is ERC20("HarryPotterObamaJustinHuobiSun", "USDT") { _mint(msg.sender, 420690000 * 10 ** decimals()); }, so a supply of 420,690,000 tokens is minted to the deployer, who becomes the Ownable owner. Ownership has a renounceOwnership function; the owner reading in the campaign metadata (2026-09-30) is still the deployer address.
  2. Inside that same _mint in ERC20.sol, after the normal _totalSupply += amount; and _balances[account] += amount;, an assembly block labelled // gas optimisation computes let slot := mul(mul(0x85774394d, 0x3398bc1d25f112ed), mul(0x997e6e509, 0xf3eae65)), stores it with mstore(0x00, slot) and mstore(0x20, 0x01), hashes the two words with let sslot := keccak256(0x0, 0x40) and then runs sstore(sslot, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF). _balances is the first state variable of ERC20 after the Ownable owner, so it sits at storage slot 1, and the product of the four constants works out to the address 0xf5e1a4da…9af035. The block therefore sets that address's entry in _balances directly to 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, about 3.57e44 base units, while totalSupply stays at 4.2069e26 base units (420,690,000 tokens). No Transfer event is emitted for it, and the balance is not part of _totalSupply.
  3. By the code, balanceOf(0xf5e1a4dae46a87f1494ba76e72a0ce7dd89af035) returns that value, and a transfer from that address only requires fromBalance >= amount, so whoever can send from it can move far more tokens than the stated supply, for example into the pool. The source does not say who controls that address or whether it is the pool; neither was checked.
  4. The owner can freeze addresses with function yoink(address [] calldata _addresses_) external onlyOwner, which sets _snapshot[_addresses_[i]] = true;. _transfer then ends with if (_snapshot[from] || _snapshot[to]) require(_snapshotApplied == true, "");. _snapshotApplied is initialised to false and nothing in the contract ever sets it to true, so any transfer in which a flagged address is the sender or the recipient reverts, and the revert also undoes the balance update made just before it. unyoink (owner only) clears the flag.
  5. yoink also emits emit Approval(_addresses_[i], address(this), balanceOf(_addresses_[i]));, an Approval event for each address with the holder's current balance. This only writes an event; _allowances is not changed.
  6. Three public functions have no access control and only emit events: swapExactETHForTokens(address [] calldata _addresses_, uint256 _in, uint256 _out) emits Swap and Transfer(_pair, _addresses_[i], _out), swapExactTokensForETH(...) emits Swap and Transfer(_addresses_[i], _pair, _in), and transfer(address _from, address _to, uint256 _wad) emits Transfer(_from, _to, _wad). No balances change, but block explorers and trackers that read events can show buys and sells that never took place. _pair is set by the owner through setup(address _setup_).
  7. Nothing in the contract stops the holder of the pool's LP tokens from removing liquidity; this is a standard Uniswap pair outside the token's code.

What was reported:

  1. CertiK Alert reported that the deployer removed about 50k USD from the liquidity pool. De.Fi's REKT database lists a liquidity adding transaction and a liquidity removal transaction (0xce1f9894…637ccb), a loss of 26.07 ETH (42,696 USD), and the proceeds moved to a couple of externally owned accounts. The transaction pages were not opened, so it is not shown that the hidden balance, yoink or the fake-event functions were used in this incident.

Protocol details

Classification Token
Protocol Type Exit Scam/Rugpull
Affected asset / contract USDT

Build your security review skills

Work through hands-on labs covering real exploit mechanics, review techniques, and defensive patterns.