Reproduced Exploit

beraBTC — a blacklisted holder still moves 1000 tokens through an approval

notBlacklisted reverts when isBlacklisted[msg.sender] is true. transfer and transferFrom both use it. The holder's own transfer reverts after they are blacklisted. transferFrom does not look at from, so a spender they approved beforehand moves the 1000. The fixed transferFrom also rejects a blackli…

Apr 2025•Other•access-control•2 min read

Chain

Other

Category

access-control

Date

Apr 2025

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 55100-h-01-blacklisted-user-can-transfer-tokens-via-approved-addre. Standalone Foundry PoC and full write-up: 55100-h-01-blacklisted-user-can-transfer-tokens-via-approved-addre_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/access-control/missing-auth · vuln/logic/missing-check

Key info#

ProtocolTerplayer beraBTC
FindingShieldify H-01, AuditVault 55100
Vulnerable codenotBlacklisted on transferFrom checks only msg.sender
Harm1000 tokens move from a blacklisted holder to an approved spender
Chainlocal synthetic

TL;DR#

notBlacklisted reverts when isBlacklisted[msg.sender] is true. transfer and transferFrom both use it. The holder's own transfer reverts after they are blacklisted. transferFrom does not look at from, so a spender they approved beforehand moves the 1000. The fixed transferFrom also rejects a blacklisted from, and the 1000 stay.

The vulnerable code#

SOLIDITY
modifier notBlacklisted() {
    // @> VULN: only the caller is checked. transferFrom never looks at `from`.
    if (isBlacklisted[msg.sender]) revert AccountIsBlacklisted();
    _;
}

function transferFrom(address from, address to, uint256 amount) public notBlacklisted returns (bool) {
    // spends `from`, who was never checked
}

Root cause#

The blacklist is applied to the caller of the token function. transferFrom's caller is the spender, not the owner. An approval created before the blacklist is still a valid allowance, and the modifier never reads from.

Preconditions#

The holder has 1000 tokens and has approved the spender for 1000. The holder is then blacklisted. The spender is not blacklisted.

Attack walkthrough#

  1. The holder is minted 1000 and approves the spender.
  2. The holder is blacklisted. Their own transfer reverts.
  3. The spender calls transferFrom. The modifier sees a non-blacklisted caller and the 1000 move.

The control uses a transferFrom that reverts when from is blacklisted. The same call reverts and the holder keeps 1000.

Diagrams#

flowchart TD A["Holder approves spender, then is blacklisted"] --> B["Holder transfer reverts"] B --> C["VULN: transferFrom checks only the caller"] C --> D["Spender receives 1000"] D --> E["Fixed transferFrom also checks from and the 1000 stay"]

Impact#

A blacklisted holder moves 1000 tokens through a pre-existing approval. The blacklist does not freeze that balance.

Remediation#

In transferFrom, reject the call when from is blacklisted as well as when msg.sender is blacklisted.

Sources#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • 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.