An audit answers one question: does this code, as written on this day, have bugs the auditors could find. It says nothing about what happens next. The owner key can leak a year later. A new implementation can ship with a skipped initializer. A signer can approve something they did not read.
Most of the incidents in part 5 and part 6 were not code bugs in the audited sense. They were someone with the right permissions doing the wrong thing, or a legitimate change that broke an assumption. That is what monitoring is for: not to find bugs, but to tell you when something with power was used.
This part is a checklist. Find your contract type, and turn on what is listed. If you have never set up a monitor, read How to set up smart contract monitoring in 5 minutes first. This part does not repeat the steps, it says what to point them at.
The rule of thumb
Start from the doors you found in part 2: every function that only an owner, a role holder or a multisig can call. For each one, ask two questions.
- Does it emit an event? If yes, alert on any occurrence. Admin events are rare on a healthy contract, so this alert is almost free of noise.
- What would I do if it fired? If the answer is "nothing", you do not need an alert, you need a lever. That is part 12.
Everything below is an application of these two questions.
1. Token (ERC-20 or similar)
What an attacker with a privileged key does here: mints, pauses, blacklists, or hands the key on.
| Watch | Why | Recipe |
|---|---|---|
OwnershipTransferred | New owner means new power over every admin function | ownership-transferred |
RoleGranted / RoleRevoked | A new minter or admin on an AccessControl token | role-granted |
Custom role events, such as MinterAdded | Older tokens use their own events instead of AccessControl. PlayDapp's addMinter was one: the alert would have come 1.5 to 3 minutes before the first unauthorized mint1 | any event from the ABI, Critical |
Paused / Unpaused and paused() | A pause stops transfers. Unexpected means someone else holds the switch | contract-paused, contract-state-paused |
| Large transfers | A mint followed by a dump shows up as a big outflow | large-token-transfer |
OwnershipTransferredMinterAddedAccessControl. PlayDapp's addMinter was one: the alert would have come 1.5 to 3 minutes before the first unauthorized mint1Paused / Unpaused and paused()Add transfer-spike for an unusual rate of transfers. It uses a norm instead of a fixed number, see part 11.
2. Upgradeable proxy
The address stays, the code behind it can change (part 3). The upgrade is the most powerful single action.
Upgraded: alert on every occurrence, Critical. Use the proxy-upgraded recipe.AdminChanged: the proxy admin is the key to the next upgrade. If your proxy emits it and it is in the ABI, add an any-event rule.- The state your new code depends on. An upgrade you made yourself will fire
Upgraded, and you will read the alert as "yes, that is ours". The Ronin upgrade in August 2024 was exactly that: the plannedUpgradedevent was fine, and a threshold inside the contract was now 02. For each upgrade, list the two or three values that must not change, and watch them with a state rule. That is part 10.
3. Safe or another multisig as owner
If a Safe is the owner of your contracts, the Safe is the contract worth watching most closely (part 4).
- Owners and threshold changes (
AddedOwner,RemovedOwner,ChangedThreshold): safe-owners-changed. At UXLINK in September 2025 the attacker removed three owners first, and the alert would have arrived 1 to 2.5 minutes before the first withdrawal3. - A failed
execTransaction. Someone tried and failed. SmartAlert has a rule on the share of failed transactions to a contract: withminTxs1 and a threshold above 0 it fires on a single failed direct call. In Safe 1.3 and later a failedexecTransactionreverts as a whole and emits noExecutionFailure, so the safe-transaction-failed recipe does not see it, and this rule does. At WazirX, a failed call came 3 to 4.5 minutes before the Safe was upgraded to the attacker's code4. - An executor you do not know. A rule "allowed senders" on
ExecutionSuccess: list the addresses of owners and relayers who may sendexecTransaction, and it fires on anyone else. At Bybit, an executor the Safe had never used appeared in the same block as the drain5.
4. Bridge or vault
Money sits in one place, and it is the target. Add both the doors and the exits.
- Everything from the proxy and Safe lists, for the gateway and for whoever owns it.
- Safety parameters through view functions. A signature threshold, a withdrawal limit, a paused flag, a guardian address. If these live in storage and no event announces a change, you need a state rule. Ronin's
minimumVoteWeight()is the example, the next part is about it. - Outflows in dollars. large-transfer-usd on each token, treasury-below-usd on the total, which also covers native ETH, because native transfers emit no
Transferevent. - The pause lever itself. Watch
Paused, and watch who holds the key to it.
5. Keepers, relayers and bots
Not a contract you wrote, a wallet your system depends on. When its gas runs out, nothing happens, and nothing emits an event about it.
- keeper-gas-runway: alert when the balance lasts fewer than N days at the current spending rate. A fixed floor is too late on a busy day and too early on a quiet one.
- low-balance-no-topup: balance below a floor and no top-up in an hour, so an auto top-up bot that works does not wake anyone.
- contract-went-quiet: the contract the keeper serves received nothing for N hours.
What this checklist does not give you
- Alerts on failed calls to admin functions. At Zoth, an unknown address called
upgradeToAndCalland failed on an access check 19.5 hours before the real upgrade. Whether that was the attacker is not confirmed. We do not build a rule on this: a failed call costs a fraction of a cent, scanners probe admin functions on every contract all the time, and anyone could flood such an alert. A *successful* call from an address you do not expect is a real signal, and the allowed-senders rule above catches it. - Attacks inside one transaction. See part 8.
- A lever. SmartAlert shows on the contract page what you can do in an incident: pause, who owns it, who can replace the code, whether roles can be revoked. Where a lever is missing, that is a design fix, not an alert. See part 12.
How to catch it
The minimum set for a new contract, in the order we would add it:
- Add the contract (and your Safe, and your keeper wallets) as in the setup guide.
- Turn on the suggested admin rules: ownership, roles,
Upgraded, pause. Severity Critical, a channel someone reads at night. - For a Safe: safe-owners-changed, a failed-
execTransactionrule and an allowed-senders rule onExecutionSuccess. - For a bridge or vault: large-transfer-usd, treasury-below-usd, and one state rule per safety parameter (Pro and above).
- For keepers: keeper-gas-runway and low-balance-no-topup.
- Check each rule on history before relying on it. Backtest shows how often it would have fired in the last days.
Sources
- 1.PlayDapp, February 2024:
addMinterat 13:39:23, first unauthorized mint at 13:45:11. Halborn, Explained: The PlayDapp hack. Alert timing is SmartAlert's estimate: 12 confirmations on Ethereum plus polling and rule check, about 2.5 to 4 minutes. ↩ - 2.SmartAlert, Ronin Bridge hack, August 2024, checked on-chain. ↩
- 3.UXLINK, September 22, 2025: owners removed at 14:45:59, first withdrawals from 14:50:59. Rekt, UXLINK. ↩
- 4.WazirX, July 18, 2024: failed
execTransactionat 06:10:47, Safe upgraded at 06:17:47. Rekt, WazirX. The link between the failed call and the attack is our inference from timing and calldata. ↩ - 5.CertiK, Bybit incident technical analysis. ↩