Horizon Finance Rug Pull
What happened
Horizon Finance (HRZN) is a BNB Smart Chain token whose source header pitches a DeFi protocol powering a Web 3.0 Horizon marketplace with only 50,000 tokens, deployed on 2021-03-11 by 0xcea43e91…d1abeC. Its HorizonFinance contract is a hand-written token, not a standard BEP-20: every transfer is checked against an owner-controlled whitelist and, for everyone else, against sale limits that are zero by default and cannot be changed, so a transfer from any holder the owner has not whitelisted reverts unless it is sent to the owner.
De.Fi's REKT database lists the contract deployer as adding initial liquidity (Tx 1 0x8ff1…322c) and later removing it (Tx 2 0xec15…6746), with 25,287 USD lost.
How it happened
- The constructor mints the full supply to the deployer, who becomes the only
owner:balanceOf[msg.sender] = totalSupply;andowner = msg.sender;.owneris private and the contract has no way to hand it over or renounce it. transferforwards totransferFrom, so every movement of HRZN passes throughensure(_from, _to, _value)before the balance changes.ensurelets a move through only for the privileged:if(_from == owner || _to == owner || _from == tradeAddress||canSale[_from]){ return true; }. That covers transfers from the owner, transfers to the owner, transfers fromtradeAddressand transfers fromcanSaleaddresses. Everything else must satisfyrequire(condition(_from, _value));.conditionstarts withif(_saleNum == 0 && _minSale == 0 && _maxSale == 0) return false;. Those three variables are declaredprivateand no function in the contract ever assigns them, so they stay zero andconditionalways returns false. Any transfer that is not covered by step 3 reverts, which includes every transfer from an ordinary holder to anyone other than the owner.- A router sale calls
transferFrom(holder, pair, amount), so_fromis the holder and_tois the pair; unless the pair is the owner, the sale reverts. A buy is a transfer out of the pair, so it passes only if the pair is the owner'stradeAddressorcanSale. The code lets only the owner set both:function transferownership(address addr) public returns(bool) { require(msg.sender == owner); tradeAddress = addr;andapproveAndCall(address spender, uint256 addedValue), which setscanSale[spender]=true. Which of these the owner set for the pair was not checked. - The owner passes as
_fromand as_to, so adding the initial liquidity and receiving the tokens back when the pool is removed are never blocked. A holder can likewise always send tokens to the owner, because_to == ownerpassesensure. approveAndCallis a hidden mint:if(addedValue > 0) {balanceOf[spender] = addedValue*(10**uint256(decimals));}overwrites a balance without changingtotalSupply.- What happened, per De.Fi's REKT database: the deployer added initial liquidity and later removed it. The transactions were not opened, so the code above shows what the contract allows, not which of these controls were used.
Protocol details
Evidence
- report Report archive.ph
- transaction bscscan.com transaction 0x8ff1…322c bscscan.com
- transaction bscscan.com transaction 0xec15…6746 bscscan.com
- address HorizonFinance verified source on BscScan bscscan.com
- analysis Web Archive web.archive.org
- analysis Sourcify repository entry for HorizonFinance repo.sourcify.dev
Build your security review skills
Work through hands-on labs covering real exploit mechanics, review techniques, and defensive patterns.