The Access Control Check That Let Every Caller Through

An inverted require in SuperRare's staking contract passed for every caller; a static detector catches the shape automatically.

Share this post

access_control_vulnerability_detection_superrare_kostasferles_audithub.png

Pre-Mortem is a weekly series by Kostas Ferles, CTO of AuditHub. This article looks at access control checks that are present but logically broken, and a static pass that catches them before deployment. New breakdown every week. Find Kostas at @KFerles.

The audit firm Veridise just published the auditor’s view of this bug: its CEO, Jon Stephens, shows how a senior reviewer catches an inverted access control check by hand (the Auditor’s Take). This post takes the same bug from the AuditHub side, where that judgment becomes a check that runs automatically. The function that set the rewards root was guarded by an access control check, and it still let any address on the chain through. Nothing about it looks broken. The owner can update the root and the tests pass, so a review moves on. What catches a check like that before the contract ships, and what keeps catching it after the next change lands?

TL;DR

  • Privileged setters with a msg.sender guard can pass for every caller.
  • A Vanguard detector flags any guard whose msg.sender clause is not ==.
  • Run on every commit, it catches the post-audit change before deploy.

A present guard is not a restriction

A guard can be present and still restrict no one. A require on msg.sender with a clean revert string: that is what a reviewer sees at a glance, and seeing it is what ends the review early. But it settles nothing about who actually gets through. That comes down to the logic, and the logic breaks quietly: a comparison written backwards, an || where an && belongs, a condition that happens to hold for every input. The check still reads like access control. It just excludes no one.

This is not a SuperRare problem, and it is rarely a cheap one. In August 2021, Poly Network lost around $611 million this way. The function that replaced the bridge’s trusted signers was meant to be called only by a management contract, but that contract could be directed to call arbitrary targets. An attacker routed a crafted message through it, installed themselves as the signer, and drained the bridge (SlowMist’s analysis walks the full path). Rikkei Finance, in April 2022, lost about $1.1 million to a plainer version: its price-feed setter shipped with no caller check at all, so anyone could point the protocol at a feed they controlled and empty the lending pools (CertiK’s breakdown). SuperRare is the quietest of the three. The guard was present and looked right. It admitted everyone anyway.

Why every caller passes the check

The condition is short enough to check by hand, and that is the quickest way to see the bug. Here is the deployed guard on the function that sets the rewards root:

For this require to revert, both inequalities have to be false in the same call, which would need msg.sender to equal the owner and the authorized address at once. No address is both, so the condition is always true and every caller passes, the two privileged ones included.

Written correctly, the guard admits only the two privileged addresses and reverts for everyone else:

The slip is a half-finished negation: the comparisons flipped from == to !=, but the || stayed where the exclusion needed &&. The owner can still update the root, and the suite stays green, since no test in it checks who the guard turns away.

The failure was the predicate, not a missing guard

The contract was RareStakingV1, which handled staking for the SuperRare NFT marketplace. Stakers locked RARE and claimed rewards each round against a Merkle root, and updating that root was supposed to be limited to the owner or a single authorized address. On July 28, 2025, someone called updateMerkleRoot with a root of their own, set themselves as the recipient of the entire pool, and walked off with roughly $730,000 in RARE. A front-runner caught the pending transaction and landed a copy one block after the exploit contract went live.

One detail matters more than the rest. The team has said the broken guard arrived in a commit made after the contract’s first review, so no auditor ever had it in front of them. The manual cue, the inequality against msg.sender that tells a senior reviewer to stop and read what a guard actually permits, is what Jon Stephens works through in the Auditor’s Take. Stay here for the version that does not wait for someone to look.

What a static pass settles

This bug is a property of the source code itself, not of any state the contract has to reach. The condition is already wrong when you read it, before a single transaction runs. That makes it a job for static analysis. The check a reviewer performs by eye, stopping on a msg.sender inequality, is structural, so it can be written down once and run on every function.

The shape we need to flag is a state-changing function that anyone can call and whose only msg.sender guard uses something other than ==. A custom detector written in the query language of Vanguard, our static analyzer, encodes exactly that property:

The query keeps every concrete, externally callable, state-changing function that is not guarded by a require whose msg.sender clauses are all equality checks. Run on RareStakingV1, it returns updateMerkleRoot: the require is there, but its clauses compare with !=. It flags the simpler case from earlier just as well: a privileged function with no msg.sender check at all, the shape behind Rikkei’s loss.

The detector has two limits. It over-reports by design: a function that is public on purpose will land in the results, and so will one guarded through a pattern the query does not model yet, OpenZeppelin’s role-based modifiers being the common case. Each of those takes a moment to dismiss. It also checks the shape of a guard, not its meaning: whether a well-formed == compares against the right address is a question only a human can ultimately answer. What it does settle is the thing that mattered here: no state-changing function slips through with a msg.sender guard that is not an equality check.

From a habit to a check on every commit

What makes a detector like this worth having is when it runs, not just what it finds. Any review, manual or automated, certifies the code that existed on the day it ran. The code keeps moving after that day, and this guard is exactly the kind of change that lands in the gap: small, plausible, green in CI.

That is the argument for continuous security: these checks belong in the development workflow itself, running on every push and every pull request. Then the commit that turns == into != fails review before merge, with the finding in front of the developer who made the change, while it still costs a review comment instead of a pool. The property never changes and the code never stops changing, so the check has to travel with the code.

Want checks like this running on your codebase?

One inverted operator opened a privileged function to the whole chain. If you want this check, and many others like it, running on your codebase on every commit, try AuditHub for free or book a demo.

What to remember

Access control vulnerability detection is the practice of confirming that a contract’s caller guards actually restrict who can reach privileged functions, instead of assuming a guard works because it exists. It flags inverted or tautological conditions, the kind that let SuperRare’s reward setter accept every caller. Run on every commit, it surfaces the broken guard before deploy.

Sign up to our newsletter

Stay up to date with the latest news and developments from AuditHub

No spam. Always free. We respect privacy.

About author

Picture of Kostas Ferles

Kostas Ferles

Chief Technology Officer (CTO)

More articles

AuditHub Announcements

Run AuditHub’s Security Tools From Your AI Agent

An open-source MCP server that connects Claude Code and Codex to AuditHub for static analysis and fuzzing runs.
Security Analysis

The Access Control Check That Let Every Caller Through

An inverted require in SuperRare’s staking contract passed for every caller; a static detector catches the shape automatically.
Security Analysis

How the AuditHub Approach Would Have Flagged Resupplyfi’s $9.6m Bug: Pre-Mortem

Static detector plus fuzzer, custom configured, surfaced the floored exchange rate before deployment. Same setup reproducible on your lending market.

Ready to automate your security?

Join leading Web3 teams who’ve already embedded continuous security into their development process.

 Get started in 30 minutes / No setup required / See results immediately