*Part 5 of 12 in Web3 Security 101.*
In part 2 we looked at where the door of a contract is: the owner, the roles, the proxy admin. This part is about what happens when someone else gets the key to that door.
A stolen admin key is not an exploit in the usual sense. The code works exactly as written. The attacker simply uses functions that only the admin may call: transfer ownership, upgrade the implementation, add a minter, withdraw. The audit report does not help, because nothing in the code is wrong. What is wrong is who is holding the key.
The mechanism
An admin function is a function with a check like onlyOwner or onlyRole(MINTER_ROLE). The check compares msg.sender with a stored address. It cannot know whether a person or an attacker is behind that address.
How the key gets stolen is usually not blockchain-related: a compromised employee laptop, a phished developer, a leaked seed phrase, a signing tool that was tampered with. IoTeX says its owner key was taken from a compromised employee machine, most likely by social engineering, and that the attacker had been inside for a long time1.
What the attacker does next follows a pattern, and it is the pattern that makes it catchable:
- Take control. Transfer ownership to their own address, grant themselves a role, or replace the implementation of a proxy.
- Test. A small mint or a small withdrawal to check that everything works.
- Drain or mint. Move the funds, or mint tokens out of nothing and sell them.
Step 1 emits an event or changes a stored value, and on a healthy contract it almost never happens. A mature protocol changes its owner maybe once in several years, and the team knows about it in advance. That asymmetry is the whole idea of monitoring admin actions: the alert is rare, so you read it.
Between steps 1 and 3 there may be seconds or hours. The three cases below show both ends.
IoTeX ioTube, February 2026: 14 minutes, then almost seven hours
ioTube is the bridge between IoTeX and Ethereum. On Ethereum its Validator contract owned two other contracts: TokenSafe, which holds the real tokens, and MinterPool, which mints the bridge's wrapped tokens. The Validator itself had one owner: an ordinary address, with no multisig and no timelock in front of it23.
On February 21, 2026 at 01:07:35 UTC that address called transferOwnership on the Validator and handed it to an attacker's address. At 01:20:35 the attacker called the Validator's upgrade, which passes ownership of everything it owns to a new address, and took TokenSafe and MinterPool. At 01:25:47 they released the first tokens from TokenSafe, and the withdrawals and mints went on until 07:4323. About $4.4M of reserves left, and hundreds of millions of CIOTX were minted with nothing behind them14. IoTeX detected the incident at 08:01, almost seven hours after the first step1.
An alert on the one OwnershipTransferred event would have arrived at about 01:10–01:11. That is about 14 minutes before the first withdrawal and about 90 minutes before the larger ones. The full timeline with every transaction is in our IoTeX write-up.
PlayDapp, February 2024: a signal and no lever
PlayDapp's PLA token used OpenZeppelin 2.x access control. On February 9, 2024 at 13:39:23 UTC an attacker holding the admin key called addMinter, at 13:42:35 addPauser, and at 13:45:11 minted the first batch. A second wave followed on February 12 at 07:27:35 and 13:01:47. By Halborn's count the two waves were 36.5M and 253.9M PLA, a nominal $290M5.
An alert on MinterAdded would have come at about 13:42–13:43:30, 1.5 to 3 minutes before the first mint, and about 66 hours before the second wave. It would still not have stopped the second wave. In OZ 2.x only an account can renounce its own role, and nobody can revoke another account's minter role. The team removed its own roles; the attacker kept theirs. The way out was to migrate the token5.
The lesson is separate from monitoring: a signal and a lever are two different things. Here the lever was missing in the design of the contract.
Zoth, March 2025: 36 seconds
Zoth is a case with no time at all. On March 21, 2025 at 08:46:59 UTC the regular admin address, which was compromised, called an upgrade on Zoth's proxy. The withdrawal followed at 08:47:35, 36 seconds later. About $8.4M was lost67.
A monitor that waits for 12 confirmations on Ethereum, about 144 seconds, cannot beat a 36-second gap. The alert for Upgraded would have arrived after the money was gone.
There is one more detail, and we treat it with caution. At 13:12:35 on the day before, 19.5 hours earlier, an unknown address called upgradeToAndCall on the same proxy and was rejected with AccessControlUnauthorizedAccount. Whether this was the same attacker probing is not confirmed. We do not build a rule on it, and you should not either: failed calls to public admin functions are cheap to send (on BNB Chain the gas is a fraction of a cent), and scanners and bots probe them all the time. An alert on every failed attempt would be mostly noise. The signal worth reading is a successful admin action, or a call from a sender you did not expect.
What the three cases have in common
- The first on-chain sign was an admin action, not a transfer of funds.
- The key was one key. In IoTeX one address controlled the whole bridge.
- The time between the first sign and the loss ranged from 36 seconds to almost seven hours, and the result depended on whether anyone was listening.
How to catch it
Everything below uses the rules SmartAlert has today. Alerts arrive about 2.5–4 minutes after the block on Ethereum and under 2 minutes on BNB Chain, so the first rule is only useful when the attacker is slower than that.
- Ownership change. Alert when contract ownership is transferred on every contract that has an owner. It is offered automatically when the ABI has
OwnershipTransferred. This was the 14-minute signal at IoTeX. Add it on the contracts that own other contracts too, not only on the ones that hold funds. - Upgrade and proxy admin. Alert on proxy upgrade. Add the proxy address, not the implementation. For Zoth this is the alert that arrives late: 36 seconds is less than the delay.
- Roles. Alert when a role is granted or revoked covers
RoleGrantedon OpenZeppelin 4+ contracts. The templates do not know the old OZ 2.x eventsMinterAddedandPauserAdded. For a token like PLA you add a rule on them by hand from the ABI. It works, but it is not one click. - Pause. Alert when a contract is paused or unpaused. An attacker who adds a pauser, as at PlayDapp, may also use it.
- Unknown callers. The "Allowed senders" rule fires when a transaction reaches your contract from an address outside your list. Set it on admin functions such as
upgradeToAndCallorgrantRole, with your admin addresses and your Safe as the list. It also catches a successful call from a key that should not hold the role. Limits: it is available on Business and above, and a call through a Safe hasexecTransactionas the selector, so for Safe-owned contracts use the event form of the rule (OwnershipTransferred,Upgraded), which sees the call through any route8. - Money out. Transfer above an amount in $ from the vault or treasury. At IoTeX it fires at about 01:30 on the first million, as a confirmation that rule 1 was real.
What monitoring cannot do here is take the key back. The best answer is on the design side, before the incident: put the owner behind a multisig and a timelock (see part 4), use a role system where roles can be revoked, and write down who holds the pause key. We also have no check that tells you, when you add a contract, whether it has a pause or whether a role can be revoked. You have to know that from the code.
Next: part 6, why bridges are the biggest target: Ronin and Orbit.
Sources
- 1.IoTeX, How IoTeX responded to the ioTube bridge incident: a full month in review: the compromised machine, detection at 08:01, the response. ↩
- 2.SmartAlert, IoTeX ioTube, February 2026: transactions, block times, owner structure, checked on-chain. ↩
- 3.Rekt, IoTeX — Rekt: owner without multisig, the attack steps, the minted amount. ↩
- 4.IoTeX, Security incident update: ioTube bridge exploit and recovery roadmap: about $4.4M, 410M CIOTX. Our own count of the mints on-chain is higher; see our write-up. ↩
- 5.Halborn, Explained: The PlayDapp hack (February 2024): the two waves, the amounts, OpenZeppelin 2.x roles. Times of the transactions: SmartAlert, on-chain. ↩
- 6.Zoth on X, incident statement. ↩
- 7.Rekt, Zoth — Rekt: $8.4M, the admin key. ↩
- 8.SmartAlert, rules on transaction receipts: the "Allowed senders" rule (Business and above). ↩