Prophet Project Rug Pull

Reported loss $46K
Ethereum
Rugpull

What happened

This PRY token on Ethereum (contract 0xd7d84e9d…24b325) was an imitation of the Prophecy project: on 2020-12-18 the project's official account warned that a token using its old domain was a scam and gave its own contract as 0x3c81d482…d00521. The imitation's source gives its owner a hidden transfer restriction that can stop non-whitelisted holders from selling, while the owner's wallet is exempt.

On the same day the deployer 0xb3408AD8…B9182A added liquidity on Uniswap V2 (200 ETH and 10,000,000 PRY, receiving 44,721.3595 UNI-V2 LP tokens; 08:14 UTC) and about four hours later removed liquidity in a single transaction (12:20 UTC), receiving 270.847 ETH and 7,396,670.75 PRY. De.Fi's REKT database lists the incident as an exit scam/rugpull with $46,356 lost.

Add liquidity: Tx 1 0x1078…7d8d Remove liquidity: Tx 2 0xf634…0750

How it happened

  1. The constructor sets both control addresses to the deployer and mints the full supply to it: _address0 = owner; _address1 = owner; _mint(_address0, initialSupply*(10**18));. While the two are equal, transfers are unrestricted.
  2. The safeCheck modifier changes that on the owner's first outgoing transfer: if(sender==_address0 && _address0==_address1){_address1 = recipient;}. The deployer's first transfer to the Uniswap pool (the liquidity deposit, if no earlier transfer was made) therefore makes the pool the new _address1, which is the only way this restriction switches on.
  3. From then on, any transfer where neither side is the owner must satisfy if(recipient != _address0 && sender != _address0 && _address0!=_address1 && amount > _zero){require(sender == _address1 ||sender==_router || _Addressint[sender], "ERC20: transfer from the zero address");}. A buy comes from the pool (sender == _address1) and passes. A sale through the router is a transferFrom whose sender is the holder, not the router or the pool, so it reverts unless the holder is whitelisted. The holder is debited nothing because the whole transfer fails.
  4. The owner controls the exceptions: upint(address addressn,uint8 Numb) whitelists an address, intnum(uint8 Numb) sets _zero so amounts up to that size skip the check, and ints(address addressn) replaces _address1. multiaddress(uint8 AllowN,address[] memory receivers, uint256[] memory amounts) sends tokens from the owner to a list and, for the first AllowN receivers, whitelists them and gives the router an unlimited allowance: _Addressint[receivers[i]] = true; _approve(receivers[i], _router, _valuehash);. All of these check msg.sender == _address0. Transfers to the owner (recipient != _address0) are not restricted. No function renounces the owner role.
  5. What happened, per the transactions: on 2020-12-18 at 08:14 UTC the deployer called addLiquidityETH on Uniswap V2 Router 2 with 200 ETH and 10,000,000 PRY and received 44,721.3595 LP tokens; at 12:20 UTC the same address called removeLiquidityETHWithPermit and received 270.847373774939170577 ETH and 7,396,670.7539 PRY from the pool. De.Fi's REKT database lists $46,356 lost. The sources opened do not show whether any holder's sale was blocked or which whitelist calls the owner made; the restriction above is what the code allows.

Protocol details

Classification Token
Protocol Type Exit Scam/Rugpull
Affected asset / contract PRY
Protocol links Website

Build your security review skills

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