Reproduced Exploit
Levyathan Finance Exploit — Leaked Deployer Key → Timelock-Gated Ownership Hijack → Unlimited `mint()`
Levyathan's LEVToken is a standard Ownable ERC20 whose owner is the only address that can mint() (LEVToken.sol:33-35). That owner is the MasterChef contract, which in turn is Ownable and whose owner is an OpenZeppelin TimelockController. The timelock's proposer/executor was the team's Deployer EOA…
Loss
~$1.5M (rekt) — attacker minted 100,000,000 LEV (≈6.1× the entire prior supply) and dumped it; LP/stakers' fu…
Chain
BNB Chain
Category
Logic / State
Date
Jul 2021
Source & credit. Exploit reproduction, trace data, and analysis adapted from DeFiHackLabs by SunWeb3Sec — an open registry of reproduced on-chain exploits. Standalone Foundry PoC and full write-up: 2021-07-Levyathan_exp in the
evm-hack-registrymirror. Upstream DeFiHackLabs PoC:src/test/…/Levyathan_exp.sol.
Vulnerability classes: vuln/access-control/secret-exposure · vuln/access-control/centralization
Reproduction: the PoC compiles & runs in an isolated Foundry project at this project folder (the umbrella DeFiHackLabs repo contains several unrelated PoCs that fail to whole-compile, so this one was extracted). Full verbose trace: output.txt. Verified vulnerable sources: contracts_staking_MasterChef.sol, contracts_tokens_LEVToken.sol.
Key info#
| Loss | ~$1.5M (rekt) — attacker minted 100,000,000 LEV (≈6.1× the entire prior supply) and dumped it; LP/stakers' funds drained |
| Vulnerable contract | MasterChef — 0xA3fDF7F376F4BFD38D7C4A5cf8AAb4dE68792fd4 (owns the LEV mint key; also holds emergencyWithdraw bug) |
| Token minted | LEVToken — 0x304c62b5B030176F8d328d3A01FEaB632FC929BA |
| Governance gate | OZ TimelockController — 0x16149999C85c3E3f7d1B9402a4c64d125877d89D |
| Compromised key (proposer/executor) | Deployer EOA 0x6DeBA0F8aB4891632fB8d381B27eceC7f7743A14 |
| Attacker EOA (new owner) | 0x7507f84610f6D656a70eb8CDEC044674799265D3 |
| Schedule tx | 0xfd30def124c1345606598ae4817ae184fc1918fc638111c6e71bc9752361fd87 |
| Execute tx | 0xe6e504208ba90d121c3212a4f2547ae28e69790ab541d459c080ec8b1f3efab2 |
| Chain / fork block / date | BSC / 9,545,966 / July 30, 2021 |
| Compiler | Solidity v0.8.4 (MasterChef/Timelock), v0.8.4 (LEVToken), optimizer 200 runs |
| Bug class | Operational key compromise (leaked private key) + privileged minter via Ownable; plus a logic bug in emergencyWithdraw (transfers rewardDebt, not amount) |
| Post-mortem | https://levyathan-index.medium.com/post-mortem-levyathan-c3ff7f9a6f65 |
TL;DR#
Levyathan's LEVToken is a standard Ownable ERC20 whose owner is the only address that can mint()
(LEVToken.sol:33-35). That owner is the
MasterChef contract, which in turn is Ownable and whose owner is an OpenZeppelin TimelockController.
The timelock's proposer/executor was the team's Deployer EOA 0x6DeBA0…3A14 — and the Levyathan
developers committed that EOA's private key to a public GitHub repository (per the rekt/post-mortem
write-up quoted in the PoC header).
With the key in hand the attacker did not need any contract bug to reach the minter. They simply used the legitimate governance path:
- From the compromised Deployer,
schedule()aMasterChef.transferOwnership(attacker)call through the timelock. - Wait out the timelock delay (the PoC fast-forwards
block.timestampby 172,800 s = 2 days), thenexecute()it —MasterChef's owner is now the attacker EOA. - As the new
MasterChefowner, callrecoverLevOwnership()(MasterChef.sol:344-346), which handsLEVToken's ownership tomsg.sender(the attacker). - Now the direct owner of
LEVToken,mint(attacker, 1e26)— 100,000,000 LEV, roughly 6.1× the entire prior supply of 16,398,829.70 LEV.
The freshly minted LEV was dumped on the market, collapsing the price and draining the protocol's
liquidity. The PoC also surfaces a second, independent code bug: MasterChef.emergencyWithdraw()
transfers user.rewardDebt instead of user.amount
(MasterChef.sol:297-308), so the
"emergency exit" returns the wrong (and generally tiny/wrong) number of tokens while normal
withdraw/leaveStaking revert with withdraw: not good — the staking accounting was already broken
post-mint.
Background — the trust chain#
Levyathan is a Pancake/Sushi-style yield farm. MasterChef
(source) mints LEV rewards every block to
stakers. Two facts about the ownership graph are the whole story:
| Object | Ownable owner | Why it matters |
|---|---|---|
LEVToken | MasterChef | only the owner may mint() — unlimited, unbounded |
MasterChef | TimelockController | only the owner may call recoverLevOwnership() (and other admin setters) |
TimelockController | proposer/executor = Deployer EOA 0x6DeBA0…3A14 | the human key that drives the whole graph |
The timelock was meant to be the safety mechanism: any change to MasterChef (including pulling the LEV
mint key out via recoverLevOwnership) has to be schedule()d and then execute()d after a delay,
giving the community time to react. That design is sound only if the proposer/executor key is secret.
The team leaked it, so the attacker became the governance.
On-chain state at the fork block (decoded from the trace's storage diffs in output.txt):
| Parameter | Value |
|---|---|
LEVToken.totalSupply (before) | 16,398,829.70 LEV (0x…0d909656ec03ea680a0197 at slot 2) |
LEVToken.owner (before) | MasterChef (0xA3fDF7…92fd4, slot 5) |
MasterChef.owner (before) | Timelock (0x161499…7d89D, slot 0) |
| Timelock min delay | satisfied by the scheduled delay = 172,800 s (2 days) |
The vulnerable code#
1. The minter is gated only by Ownable — whoever owns LEV can mint anything#
// LEVToken.sol
contract LEVToken is ERC20, IBurnable, IMintable, Ownable {
...
// owner should be MasterChef
function mint(address receiver, uint256 amount) override external onlyOwner {
_mint(receiver, amount); // ⚠️ no cap, no max-supply, no schedule
}
}
LEVToken.sol:33-35. There is no maximum
supply, no per-call cap, and no rate limit — once you are owner, one call mints an arbitrary amount.
2. MasterChef will hand the LEV mint key to whoever owns MasterChef#
// MasterChef.sol
// owner of masterchef can recover ownership in order to change how LEV is minted
function recoverLevOwnership() external onlyOwner {
lev.transferOwnership(msg.sender); // ⚠️ moves the minter key to the caller (an EOA)
}
MasterChef.sol:344-346. This is the
escape hatch the attacker rides out of the timelock: rather than minting through the slow governance,
they take ownership of MasterChef once (via timelock), then pull the LEV key directly to their EOA, where
minting is instant and unbounded.
3. MasterChef.emergencyWithdraw() returns the wrong field#
// MasterChef.sol — "EMERGENCY ONLY"
function emergencyWithdraw(uint256 _pid) public {
PoolInfo storage pool = poolInfo[_pid];
UserInfo storage user = userInfo[_pid][msg.sender];
if (_pid == 0)
syrup.burn(msg.sender, user.amount);
user.amount = 0;
uint256 rewardDebt = user.rewardDebt;
user.rewardDebt = 0;
pool.lpToken.transfer(address(msg.sender), rewardDebt); // ⚠️ should transfer `user.amount`
emit EmergencyWithdraw(msg.sender, _pid, rewardDebt); // transfers the accounting field instead
}
MasterChef.sol:297-308. The canonical
PancakeSwap/Sushi emergencyWithdraw transfers user.amount (the staked principal) and zeroes both fields.
Here it transfers user.rewardDebt — a derived bookkeeping value
(amount * accCakePerShare / 1e12) that has no relationship to the principal the user is owed. The trace
shows this concretely: emergencyWithdraw(4) transfers rewardDebt = 1,539.18 LEV
(output.txt:84-91), and a second call transfers 0. Meanwhile a normal
withdraw/leaveStaking reverts with withdraw: not good
(output.txt:74-79) — the require(user.amount >= _amount) guard
(MasterChef.sol:248,
:283) fails because the pool no longer
holds enough LP to honor the request after the mint/dump and others' exits.
Root cause — why it was possible#
The primary root cause is operational, not a Solidity bug: the timelock proposer/executor private
key was published. Once an attacker controls the only key that can schedule()/execute() timelock
operations, the timelock provides no protection at all — it is simply a 2-day waiting room that the
attacker walks through using the front door.
The architectural decisions that turned a key leak into total loss are:
- The mint key is reachable from a single EOA.
LEVToken.mintisonlyOwner; the owner isMasterChef;MasterChefisonlyOwner-controlled and exposesrecoverLevOwnership()which transfers the mint key tomsg.sender. So a single compromised governance key collapses, in two hops, to "this EOA can mint infinite LEV instantly." - No supply cap on
mint().LEVToken.minthas noMAX_SUPPLYcheck, no per-interval cap, and no monotonic guard. The attacker minted 100,000,000 LEV — about 6.1× the prior total supply — in one call (output.txt:62-67). A cap would have bounded the blast radius even after key compromise. recoverLevOwnership()is a single-call mint-key escape hatch. It exists "to change how LEV is minted," but in practice it lets theMasterChefowner exit the timelock entirely by relocating the privileged minter to an EOA, where governance delays no longer apply.
Independently, the emergencyWithdraw logic bug (transferring rewardDebt instead of amount) means
the protocol's own emergency exit could not safely return user principal — a latent correctness defect that
compounded the damage during the incident.
Preconditions#
- Possession of the timelock proposer/executor key — here the leaked Deployer EOA
0x6DeBA0…3A14. This is the only hard precondition for the mint; everything else is the legitimate governance flow. - One timelock delay elapses between
schedule()andexecute(). The PoC reproduces this by warpingblock.timestampforward by172,800s (2 days) (Levyathan_exp.sol:71-72); on-chain the attacker simply waited. - No capital is required — the attack mints value out of thin air.
Step-by-step attack walkthrough (with on-chain values from the trace)#
All values below are taken directly from the storage diffs and events in output.txt.
| # | Step | Actor | On-chain effect |
|---|---|---|---|
| 0 | Initial | — | MasterChef.owner = Timelock; LEV.owner = MasterChef; LEV.totalSupply = 16,398,829.70 |
| 1 | Timelock.schedule(MasterChef, 0, transferOwnership(attacker), 0x0, salt, 172800) | Deployer (leaked key) | Operation 0x691de0…d2bc queued; isOperationPending = true; ETA slot set to 0x6103da4c (output.txt:22-30) |
| 2 | warp +172,800 s, roll to block 9,600,775 | — | Timelock delay satisfied (Levyathan_exp.sol:71-72) |
| 3 | Timelock.execute(... same args ...) | Deployer (leaked key) | Inner call MasterChef.transferOwnership(attacker) runs; MasterChef.owner slot 0 flips Timelock → 0x7507…265D3; OwnershipTransferred emitted; op marked done (output.txt:37-48) |
| 4 | MasterChef.recoverLevOwnership() | Attacker (now MasterChef owner) | LEV.transferOwnership(attacker); LEV.owner slot 5 flips MasterChef → 0x7507…265D3 (output.txt:55-61) |
| 5 | LEV.mint(attacker, 1e26) | Attacker (now LEV owner) | Transfer(0x0 → attacker, 100,000,000 LEV); totalSupply slot 2 jumps 16,398,829.70 → 116,398,829.70 LEV; attacker LEV balance slot set to 1e26 (output.txt:62-67) |
| 6a | MasterChef.leaveStaking(10000) as a normal user | user1 | Reverts withdraw: not good — staking accounting broken (output.txt:74-75) |
| 6b | MasterChef.withdraw(3, 272,356 LEV) as a normal user | user1 | Reverts withdraw: not good (output.txt:78-79) |
| 7 | MasterChef.emergencyWithdraw(4) | user2 | Transfers rewardDebt = 1,539.18 LP tokens (not principal), zeroes amount/rewardDebt (output.txt:84-95) |
| 8 | MasterChef.emergencyWithdraw(4) again | user2 | Transfers 0 (fields already zeroed) (output.txt:100-105) |
Steps 1–5 are the actual theft (mint key acquisition + unlimited mint). Steps 6–8 are the PoC demonstrating the collateral damage: legitimate stakers can no longer exit normally, and the "emergency" path is itself buggy.
Profit / loss accounting#
| Quantity | Value |
|---|---|
| LEV supply before | 16,398,829.70 LEV |
| LEV minted by attacker (one call) | 100,000,000.00 LEV |
| LEV supply after | 116,398,829.70 LEV |
| Minted ÷ prior supply | ≈ 6.1× |
| Reported loss (rekt/post-mortem) | ~$1.5M |
The attacker conjured ~6.1× the existing supply for free and sold it into the protocol's liquidity,
collapsing the LEV price and extracting the pooled value. The emergencyWithdraw defect meant stakers
could not cleanly recover principal even in the emergency path.
Diagrams#
Sequence of the attack#
Ownership / privilege graph collapse#
State machine: how the timelock is rendered useless#
Remediation#
- Treat key custody as the primary control. The root cause was a leaked proposer/executor key. Never commit secrets to source control; use HSMs / hardware wallets / a multisig as the timelock proposer/executor so that no single leaked EOA can drive governance. After this incident the only reliable mitigation is rotating to a multisig-controlled timelock.
- Cap the minter. Add a
MAX_SUPPLY(or per-interval mint cap) toLEVToken.mintso that even a compromised owner cannot mint unbounded supply in one call:function mint(address to, uint256 amount) external onlyOwner { require(totalSupply() + amount <= MAX_SUPPLY, "cap"); _mint(to, amount); } - Remove the EOA escape hatch.
recoverLevOwnership()lets theMasterChefowner relocate the mint key to an arbitrarymsg.sender, side-stepping the timelock. Either delete it, or constrain its target to another timelock/governance contract (never an EOA), so the mint key can never leave a delayed, reviewable governance context. - Keep mint authority behind the timelock, not in front of it. Minting should itself be a timelock-gated operation, so that every mint (not just ownership changes) is subject to the public delay and can be vetoed.
- Fix
emergencyWithdrawindependently. It must return the staked principal and zero both fields:(Note the existing code already sets- uint256 rewardDebt = user.rewardDebt; - user.rewardDebt = 0; - pool.lpToken.transfer(address(msg.sender), rewardDebt); - emit EmergencyWithdraw(msg.sender, _pid, rewardDebt); + uint256 amount = user.amount; + user.amount = 0; + user.rewardDebt = 0; + pool.lpToken.transfer(address(msg.sender), amount); + emit EmergencyWithdraw(msg.sender, _pid, amount);user.amount = 0before readingrewardDebt, so captureamountfirst.)
How to reproduce#
The PoC was extracted into a standalone Foundry project (the umbrella DeFiHackLabs repo has several
unrelated PoCs that fail to compile under forge test's whole-project build):
_shared/run_poc.sh 2021-07-Levyathan_exp --mt test_Timelock -vvvvv
- RPC: a BSC archive endpoint is required (fork block 9,545,966 is from July 2021).
foundry.tomluseshttps://bsc-mainnet.public.blastapi.io, which serves historical state at that block; most public BSC RPCs prune it. - Result:
[PASS] test_Timelock(). The trace shows the ownership flips, themintof1e26LEV, the reverts on normalwithdraw/leaveStaking, and the buggyemergencyWithdraw.
Expected tail:
Ran 1 test for test/Levyathan_exp.sol:ContractTest
[PASS] test_Timelock() (gas: 174648)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.17s (5.88s CPU time)
References: rekt (PoC header), Levyathan post-mortem — https://levyathan-index.medium.com/post-mortem-levyathan-c3ff7f9a6f65
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 2021-07-Levyathan_exp (evm-hack-registry mirror).
- Upstream DeFiHackLabs PoC:
Levyathan_exp.sol. - Attack transaction: view on explorer.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Levyathan Finance Exploit".
- Web3Sec X hacked database: search.
- Rekt leaderboard: search.
- Solodit incident search: search.
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.