Web3 Security 101Part 9 of 12SmartAlert7 min read

What to monitor after an audit: a checklist by contract type

An audit checks the code on one day. A checklist of what to watch after it, by contract type: token, proxy, Safe, bridge or vault, keeper wallet.

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.

  1. 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.
  2. 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.

OwnershipTransferred
WhyNew owner means new power over every admin function
RoleGranted / RoleRevoked
WhyA new minter or admin on an AccessControl token
Custom role events, such as MinterAdded
WhyOlder 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
Recipeany event from the ABI, Critical
Paused / Unpaused and paused()
WhyA pause stops transfers. Unexpected means someone else holds the switch
Large transfers
WhyA mint followed by a dump shows up as a big outflow

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 planned Upgraded event 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: with minTxs 1 and a threshold above 0 it fires on a single failed direct call. In Safe 1.3 and later a failed execTransaction reverts as a whole and emits no ExecutionFailure, 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 send execTransaction, 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 Transfer event.
  • 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 upgradeToAndCall and 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:

  1. Add the contract (and your Safe, and your keeper wallets) as in the setup guide.
  2. Turn on the suggested admin rules: ownership, roles, Upgraded, pause. Severity Critical, a channel someone reads at night.
  3. For a Safe: safe-owners-changed, a failed-execTransaction rule and an allowed-senders rule on ExecutionSuccess.
  4. For a bridge or vault: large-transfer-usd, treasury-below-usd, and one state rule per safety parameter (Pro and above).
  5. For keepers: keeper-gas-runway and low-balance-no-topup.
  6. Check each rule on history before relying on it. Backtest shows how often it would have fired in the last days.

Sources

  1. 1.PlayDapp, February 2024: addMinter at 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. 2.SmartAlert, Ronin Bridge hack, August 2024, checked on-chain. ↩
  3. 3.UXLINK, September 22, 2025: owners removed at 14:45:59, first withdrawals from 14:50:59. Rekt, UXLINK. ↩
  4. 4.WazirX, July 18, 2024: failed execTransaction at 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. 5.CertiK, Bybit incident technical analysis. ↩

Try it free

An address is enough. The first 14 days get Pro limits, and rules are checked on history before they go live.