Photon-Swap Rug Pull

Reported loss Not disclosed
Rugpull

What happened

Photon Swap's MasterChef farm (0x099c7ff2…69285d on BNB Smart Chain) combines LP-token staking with a casino-game feature. The owner can register a game contract with CreateGame, and the public PlayGame function then runs that contract's code with delegatecall inside the MasterChef, where staked LP tokens are held. The same registration also lets the game call GameMint and GameBurn on the PHOTON token, and GameBurn takes tokens from any address without an allowance.

De.Fi's REKT database lists Photon-Swap as an exit scam/rugpull dated 2021-08-08 with no loss figure; the source alone does not show that a game contract was ever registered or used.

How it happened

These steps describe what the code allows, not a recorded sequence of transactions.

  1. Stakers call deposit(_pid, _amount, _referrer), which runs pool.lpToken.safeTransferFrom(address(msg.sender), address(this), _amount);. Their LP tokens are then held by the MasterChef contract itself.
  2. The owner can call CreateGame(address newGame). It is onlyOwner, requires only newGame != address(0) and that the game is not already listed, then runs games[newGame] = newGame; and exToken.CreateGame(newGame);. The token side adds require(newGame.isContract(), "Not a contract");. Nothing else restricts what the game contract's code does.
  3. Any caller can then invoke PlayGame(address game, bytes memory playGameData), which checks only require(games[game] != address(0), "That game doesn't exist"); and runs return game.delegatecall(abi.encodeWithSignature("PlayGame(address,address,bytes)", msg.sender, game, playGameData));. delegatecall executes the game's code in the MasterChef's own context, so that code acts with the MasterChef's storage and token balances, including the pooled LP tokens from step 1. The same pattern appears in fulfillRandomness, which delegatecalls ResolveGame on a stored game address.
  4. Registration also sets casinoGames[newGame] on the PHOTON token. GameMint(address to, uint256 amount) then mints with _mint(to, amount); and GameBurn(address from, uint256 amount) runs _burn(from, amount);. Both are guarded only by onlyGame, so a registered game can create tokens for any address or burn any holder's balance without an allowance.
  5. emergencyWithdraw(uint256 _pid) pays out user.amount from the pool's LP balance held by the MasterChef, so it can only return what is still in the contract.
  6. The owner is not locked out of these powers: renounceOwnership exists but the owner reads as 0x84822221…8a2f19 in an eth_call on 2026-09-30 (current state, not historical).

Protocol details

Classification Exchange (DEX)
Protocol Type Exit Scam/Rugpull
Affected asset / contract Photon-Swap
Protocol links Website

Build your security review skills

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