SafeMoonX Rug Pull

Reported loss $25K
Rugpull

What happened

SafeMoonX (SFMX) is an ERC-20 token on Ethereum with a 1 trillion supply, deployed on 2023-07-25 by 0x892F10E3…A7fC93. Its openTrading function takes a router address as a parameter, approves that address for the pair's entire token balance, and then makes it the token's router, so the owner can hand the pool to a contract of their choosing.

On 2023-07-26 at 00:48 UTC, address 0x9aF4D799…DaDFCf sent transaction 0x566e0693…04dbee to its own verified contract 0xa90cf3cA…039114. In that transaction about 100.9B SFMX moved from the Uniswap pair to that contract and back, and the pair paid out 14.797 ETH, which was forwarded to 0x9aF4D799…DaDFCf. De.Fi's REKT database lists the loss as 25,018 USD (14.79 ETH), the deployer as 0x892F10E3…A7fC93, the funds as passing through a couple of EOA addresses and sitting at 0xc1eff258…bce480 as of 2023-08-18. Cyvers' alert on the same transaction put the amount at about 27K USD and named 0x83148f56…828053 as the distribution address.

How it happened

  1. openTrading(address _uniswapV2Router) is owner-only and accepts any address as its argument; the argument is never checked against the router constant 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D.
  2. It then makes the pair approve that address for the entire supply: _approve(uniswapV2Pair, _uniswapV2Router, _tTotal);. The pair is a contract that cannot approve by itself; the token writes the allowance into its own table on the pair's behalf, so the supplied address can call transferFrom(pair, ...) for any amount up to the full supply.
  3. The LP tokens from the first liquidity are sent to the owner: uniswapV2Router.addLiquidityETH{value: address(this).balance}(address(this),balanceOf(address(this)),0,0,owner(),block.timestamp);.
  4. Last, the token swaps its router for the supplied address: uniswapV2Router = IUniswapV2Router02(_uniswapV2Router);. Every later transfer not involving the owner calls it through isContract, which runs IERC20(address(uniswapV2Router)).balanceOf(account). Its answer decides whether the max-transaction and max-wallet checks apply to buys, and a revert in it would revert the transfer.
  5. The transferFrom that a holder of the allowance sends reaches _transfer(from=pair, ...), where tax rates are 0 and _balances[from]=_balances[from].sub(amount); debits the pair directly.
  6. What happened: transaction 0x566e0693…04dbee shows the pair 0xDc75bAc8…6C64fE sending 100.9B SFMX to contract 0xa90cf3cA…039114, that contract sending the same amount back, and the pair sending 14.797 ETH to the Uniswap router, then to the contract, then to 0x9aF4D799…DaDFCf. The transaction's calls to the token were not decoded, and it was not confirmed that 0xa90cf3cA... is the address passed to openTrading.
  7. renounceOwnership exists and owner() reads as the zero address, but renouncing does not clear the pair's allowance, which lives in _allowances, so it does not close this path.

Protocol details

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

Build your security review skills

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