Photon-Swap Rug Pull
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.
- Stakers call
deposit(_pid, _amount, _referrer), which runspool.lpToken.safeTransferFrom(address(msg.sender), address(this), _amount);. Their LP tokens are then held by the MasterChef contract itself. - The owner can call
CreateGame(address newGame). It isonlyOwner, requires onlynewGame != address(0)and that the game is not already listed, then runsgames[newGame] = newGame;andexToken.CreateGame(newGame);. The token side addsrequire(newGame.isContract(), "Not a contract");. Nothing else restricts what the game contract's code does. - Any caller can then invoke
PlayGame(address game, bytes memory playGameData), which checks onlyrequire(games[game] != address(0), "That game doesn't exist");and runsreturn game.delegatecall(abi.encodeWithSignature("PlayGame(address,address,bytes)", msg.sender, game, playGameData));.delegatecallexecutes 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 infulfillRandomness, which delegatecallsResolveGameon a stored game address. - Registration also sets
casinoGames[newGame]on the PHOTON token.GameMint(address to, uint256 amount)then mints with_mint(to, amount);andGameBurn(address from, uint256 amount)runs_burn(from, amount);. Both are guarded only byonlyGame, so a registered game can create tokens for any address or burn any holder's balance without an allowance. emergencyWithdraw(uint256 _pid)pays outuser.amountfrom the pool's LP balance held by the MasterChef, so it can only return what is still in the contract.- The owner is not locked out of these powers:
renounceOwnershipexists but the owner reads as 0x84822221…8a2f19 in an eth_call on 2026-09-30 (current state, not historical).
Protocol details
Build your security review skills
Work through hands-on labs covering real exploit mechanics, review techniques, and defensive patterns.