Reproduced Exploit

Suzaku Core — a blacklisted delegator version still becomes an entity and pulls 1000 SZK

The owner whitelists an entity implementation as version 0, then blacklists that version. DelegatorFactory.create never loads blacklisted[type_]. The call still cloneDeterministics the implementation, pushes it into the entity set, and calls initialize. The treasury treats every registered entity a…

Jul 2025•Other•access-control•4 min read

Chain

Other

Category

access-control

Date

Jul 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: 61232-in-delegatorfactory-new-entity-can-be-created-for-a-blacklis. Standalone Foundry PoC and full write-up: 61232-in-delegatorfactory-new-entity-can-be-created-for-a-blacklis_exp in the evm-hack-registry mirror.


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

Reproduction: a self-contained Foundry PoC that compiles and runs offline with only forge-std — no fork and no RPC. Trace: output.txt. PoC: test/61232-in-delegatorfactory-new-entity-can-be-created-for-a-blacklis_exp.sol.


Key info#

ImpactHIGH — version 0 is blacklisted and create still registers an entity. That entity pulls 1000 SZK out of the treasury
ProtocolSuzaku Core DelegatorFactory
Vulnerable codecreate clones and initializes without reading blacklisted[type_]
Bug classA governance blacklist does not bind the factory
FindingCyfrin (0kage) — Suzaku Core, 7 July 2025 · id 61232
Report2025-07-07-cyfrin-suzaku-core-v2.0.md
SourceAuditVault finding
StatusFixed in suzaku-core commit 292d5b7 (DelegatorFactory__VersionBlacklisted). Reproduced here as a local synthetic.
Compilersolc 0.8.27, cancun, via IR

TL;DR#

The owner whitelists an entity implementation as version 0, then blacklists that version. DelegatorFactory.create never loads blacklisted[type_]. The call still cloneDeterministics the implementation, pushes it into the entity set, and calls initialize. The treasury treats every registered entity as allowed to release funds, so the new entity transfers 1000 SZK to the caller. The treasury balance goes from 1000 to 0 while blacklisted[0] stays true.

The fixed factory reverts DelegatorFactory__VersionBlacklisted on the same inputs. No entity is created and the 1000 SZK stays in the treasury.

Vulnerable code#

Pre-fix create, copied from the factory body Suzaku patched in 292d5b7:

SOLIDITY
function create(uint64 type_, bytes calldata data) external returns (address entity_) {
    entity_ = implementation(type_).cloneDeterministic(keccak256(abi.encode(totalEntities(), type_, data)));
    _addDelegatorEntity(entity_);
    IEntity(entity_).initialize(data);
}

function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
    // @> VULN: create2 still deploys this version after it was blacklisted
    assembly ("memory-safe") {
        mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
        mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
        instance := create2(0, 0x09, 0x37, salt)
    }
}

The patch that shipped:

DIFF
 function create(uint64 type_, bytes calldata data) external returns (address entity_) {
+    if (blacklisted[type_]) {
+        revert DelegatorFactory__VersionBlacklisted();
+    }
     entity_ = implementation(type_).cloneDeterministic(keccak256(abi.encode(totalEntities(), type_, data)));
     _addDelegatorEntity(entity_);
     IEntity(entity_).initialize(data);
 }

Attack walkthrough#

  1. The operator mints 1000 SZK into the treasury. The treasury's release pays any address the factory has registered with isEntity.
  2. whitelist accepts the implementation as version 0. FACTORY() and TYPE() match.
  3. blacklist(0) sets blacklisted[0] = true.
  4. create(0, abi.encode(treasury, operator, 1000 ether)) still clones the implementation. The clone is registered, then initialize runs.
  5. initialize calls Treasury.release. The clone is already an entity, so the treasury transfers 1000 SZK to the operator.
  6. blacklisted[0] is still true, isEntity(clone) is true, the operator holds 1000 SZK, and the treasury holds 0.

Control: DelegatorFactoryFixed.create hits DelegatorFactory__VersionBlacklisted and returns no entity. The treasury still holds 1000 SZK. The caller receives 0.

Diagrams#

flowchart TD A["Owner whitelists version 0"] --> B["Owner sets blacklisted of 0"] B --> C["create skips the blacklist and clones the implementation"] C --> D["Clone is registered as an entity"] D --> E["initialize calls Treasury.release"] E --> F["Operator receives 1000 SZK and the treasury is empty"]

Impact#

The operator receives 1000 SZK from the treasury by deploying an entity of a version the owner has already blacklisted. The blacklist flag does not stop create, and the entity set is what the treasury trusts. The same call on the fixed factory reverts and moves 0.

Remediation#

Reject blacklisted versions at the top of create with DelegatorFactory__VersionBlacklisted, which is what suzaku-core commit 292d5b7 does. Do not register or initialize the clone when the flag is set.

Sources#

Taxonomy#

  • severity/high
  • vuln/access-control/missing-check · vuln/access-control/broken-logic · vuln/logic/missing-check

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.