Horus Finance Honeypot
What happened
HORUS (HRS) was a token on Ethereum whose contract gave its owner two unrestricted powers: an owner-only addWork() function that creates tokens for any address, and an owner-managed sub list that stops listed addresses from sending tokens. The 80,000-token supply was created for the deployer, who paired it with 30 ETH on Uniswap V2 on 2020-10-30 (tx 0x61d5d655…6a01a4). On 2020-10-31 at 04:45:54 UTC the deployer wallet called addWork() to mint 500 million HRS to itself (tx 0x9cfdfc0b…85bb2f), and 46 seconds later swapped those 500 million HRS through the Uniswap V2 router for 32.26 WETH (tx 0x39dcc40a…482215).
The mint was roughly 6,250 times the original supply, so it overwhelmed the pool's HRS reserves. De.Fi's REKT database lists the incident as an exit scam / honeypot with a 991 USD loss.
How it happened
- The constructor calls
_supply(initialSupplySend, _totalSupply)and sets_owner = msg.sender; in the opened transactions the deployer 0x79018eBC…6f738E holds the supply and is the caller of the later owner-only functions. - On 2020-10-30 the deployer called
addLiquidityETHon the Uniswap V2 router with 80,000 HRS and 30 ETH, creating the HRS/WETH pool (tx 0x61d5d655…6a01a4). - The contract contains an owner-only mint with no cap:
function addWork(address account, uint256 amount) public { require(msg.sender == _owner, "!warning"); _supply(account, amount); }._supplydoes_balances[account] = _balances[account].add(amount)and never changes_totalSupply, sototalSupply()keeps reporting 80,000 tokens however much is minted. - On 2020-10-31 at 04:45:54 UTC the deployer called
addWork()and 500,000,000 HRS were minted to the deployer's own address (tx 0x9cfdfc0b…85bb2f). - At 04:46:40 UTC the deployer sent those 500 million HRS to the Uniswap V2 router in a
swapExactTokensForETH-type swap and received 32.26 WETH (tx 0x39dcc40a…482215). Because the minted amount dwarfed the pool's 80,000 HRS, the swap paid out nearly the whole ETH side of the pool. - Separate from the mint, the transfer path can also be blocked per address:
_executeTransferbegins withrequire(!sub[_from], "error");, andaddSub(address[] memory account)lets only the owner setsub[account[i]] = true. Because the check is on_from, a listed holder cannot send tokens, including selling into the pool throughtransferFrom; purchases (where the pair is_from) are not blocked. The opened transactions do not showaddSubbeing called, so this is what the code allows, not something shown to have happened.
Protocol details
Evidence
- report Report horus.finance
- report HORUS contract source (Sourcify) repo.sourcify.dev
- transaction Etherscan transaction: addLiquidityETH etherscan.io
- transaction Etherscan transaction: addWork mint etherscan.io
- transaction Etherscan transaction: swap of 500M HRS for WETH etherscan.io
- address HORUS contract source (Etherscan verified code) etherscan.io
- analysis Web Archive web.archive.org
Build your security review skills
Work through hands-on labs covering real exploit mechanics, review techniques, and defensive patterns.