MoltenSwap Rug Pull

Reported loss $122K
BNB Chain
Rugpull

What happened

MoltenSwap's MasterChef farming contract, which is also the owner of the MOLT token, has a set() function that looks like a routine pool-settings update but also sends the pool's entire LP-token balance to a feeAddress. LP tokens that users stake in a MasterChef pool are held by the contract, so an owner call to set() can move everything staked in that pool to one wallet. Because it first calls safeApprove(feeAddress, bal), a repeat call on the same LP token reverts while the earlier allowance is unspent, unless an intervening call with a zero balance resets it.

De.Fi's REKT database lists MoltenSwap as an exit scam dated 2021-03-07 with $121,994 lost, and says the contract deployer called set() several times, that calling set() moved LP tokens to an external wallet (example transaction 0x3b8ed32a…53de91), and that this wallet then removed the liquidity in several transactions: 0x31c99d77…e84d55, 0xde655409…4887cc, 0x6c9d7e8b…e4de6b, 0x21d2c374…e69a10, 0x3a0c56b9…8cb1bb, 0xdcd892c2…81b640, 0x9033ed6a…9f1865, 0x0deb3c97…d73e5e and 0x53a1f15d…7da8e0. The listing's list of set() calls is a Bitquery query for method d9638422 on the MasterChef contract. These transactions were not opened here.

How it happened

Steps 1 to 4 describe what the verified code allows; step 5 is what the De.Fi listing reports.

  1. Users stake LP tokens in the MasterChef: deposit pulls them in with pool.lpToken.safeTransferFrom(address(msg.sender), address(this), _amount); and records only a number in user.amount. The staked LP tokens sit in the MasterChef's own balance.
  2. The owner-only set() is documented as updating a pool's allocation points and deposit fee, but its last lines read: uint256 bal = lpToken.balanceOf(address(this)); ... lpToken.safeApprove(address(feeAddress), bal); and pool.lpToken.safeTransfer(address(feeAddress), bal);. A call that gets through sends the pool's whole LP balance to feeAddress, whatever values are passed for _allocPoint and _depositFeeBP.
  3. There is a limit on repeating this. safeApprove in the SafeBEP20 library contains require((value == 0) || (token.allowance(address(this), spender) == 0), "SafeBEP20: approve from non-zero to non-zero allowance");, and the MasterChef's safeTransfer to feeAddress does not spend the allowance it just set. So after one call with a positive balance, another set() on the same LP token reverts while that allowance is unspent, unless a call made when the pool's balance is zero resets it (or feeAddress spends it). Later deposits into that pool can therefore be swept only after such a reset. Pools with different LP tokens are unaffected by each other's allowance.
  4. Users' recorded balances are not reduced. withdraw and emergencyWithdraw still pay out of the contract's own LP balance (pool.lpToken.safeTransfer(address(msg.sender), amount);), which stays empty after a sweep unless tokens are returned. The deposit-fee check also permits up to 100%: require(_depositFeeBP <= 10000, ...). setFeeAddress can only be called by the current feeAddress, so the destination is controlled by whoever holds that address.
  5. Reported: De.Fi says the deployer invoked set() multiple times, that calling set() led to an LP-token transfer to an external wallet, and that the wallet removed the liquidity in nine transactions listed above. The loss figure is $121,994.

Protocol details

Classification Exchange (DEX)
Protocol Type Exit Scam/Rugpull
Affected asset / contract MOLT

Build your security review skills

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