#44355 [SC-High] announcer owner can inflate announcers registry entries via mutate and register loop to claim most of rewards
#44355 [SC-High] Announcer Owner Can Inflate Announcers Registry Entries via Mutate and Register Loop to Claim Most of Rewards
Submitted on Apr 22nd 2025 at 07:02:34 UTC by @perseverance for IOP | CircuitDAO
Report ID: #44355
Report Type: Smart Contract
Report severity: High
Target: https://github.com/immunefi-team/CircuitDAO-IoP/tree/main/circuit_puzzles
Impacts:
Theft of unclaimed yield
Description
Short Summary
The announcer owner can repeatedly use the announcer_mutate operation to change their atom_announcer coin's INNER_PUZZLE_HASH and then re-register the announcer using the announcer_register operation with the new hash in the announcer_registry. Because the registry uses the mutable inner_puzzle_hash as the unique identifier instead of the immutable LAUNCHER_ID, the owner can create multiple distinct entries in the registry, all ultimately controlled by them. This inflates the apparent number of unique announcers, causing the reward distribution mechanism (MINT operation in the registry) to allocate them a disproportionately large share of the CRT rewards, effectively draining rewards intended for other legitimate participants.
Background Information
The announcer_registry.clsp puzzle serves as a central registry tracking active and approved Announcers within the Circuit DAO protocol. One of its key functions, triggered by the MINT operation, is to facilitate the distribution of CRT (Circuit DAO Token) rewards to registered announcers. The intended mechanism involves calculating the total rewards for an interval (crt_credits_per_interval) and dividing this amount proportionally among the currently registered announcers based on the count of entries in the ANNOUNCER_REGISTRY list. This aims to fairly compensate active participants.
Reference: https://docs.circuitdao.com/technical-manual/announcer-registry#mint and https://github.com/immunefi-team/CircuitDAO-IoP/blob/d2c3171f08864c29fdd436e25a39c95b371df860/circuit_puzzles/announcer_registry.clsp#L102-L121
The Vulnerability and Vulnerability Details
The vulnerability stems from the interaction between three components: announcer_mutate.clsp, announcer_register.clsp, and announcer_registry.clsp.
Mutable Identifier in Registry: The
announcer_registry.clspuses theannouncer_inner_puzzle_hashprovided in theRECEIVE_MESSAGE 0x12fromannouncer_registeras the unique key to identify and register an announcer. It checks if this specific hash alreadycontainsin theANNOUNCER_REGISTRYlist before adding it.https://github.com/immunefi-team/CircuitDAO-IoP/blob/d2c3171f08864c29fdd436e25a39c95b371df860/circuit_puzzles/announcer_registry.clsp#L133-L134
Owner Can Mutate "Inner Puzzle Hash": The
announcer_mutate.clsppuzzle explicitly allows the owner (who can provide the currentinner_puzzle_hash) to change theINNER_PUZZLE_HASHof theiratom_announcercoin to anew_puzzle_hash. This is a legitimate operation for transferring ownership or updating control logic.
https://github.com/immunefi-team/CircuitDAO-IoP/blob/d2c3171f08864c29fdd436e25a39c95b371df860/circuit_puzzles/programs/announcer_mutate.clsp#L34-L40
Mutation Doesn't Affect Registry: The
announcer_mutateoperation only affects the state of theatom_announcercoin itself. It does not communicate with theannouncer_registryto inform it about the change in theinner_puzzle_hashor to de-register the old hash.
Exploitation
This allows a malicious owner to perform the following loop:
Reward Distribution Impact:
When the MINT operation runs on the announcer_registry, it calculates announcers_count by counting the number of entries in ANNOUNCER_REGISTRY.
Supposed the total of announcers is 3. T
The total reward crt_credits_per_interval is divided by this count. The generate-offer-assert function then creates payment conditions for each entry (A, B, and C). Since the owner controls all these inner puzzle hashes, they effectively receive 3 shares of the reward pool instead of the intended 1 share.
Normal operation: Reward for the announcer should be : crt_credits_per_interval /3
After the hack: Reward for the announcer is: crt_credits_per_interval * 3 / 5 that is 60/33.3 = 1.8 times bigger
https://github.com/immunefi-team/CircuitDAO-IoP/blob/d2c3171f08864c29fdd436e25a39c95b371df860/circuit_puzzles/announcer_registry.clsp#L102-L121
This fundamentally breaks the reward distribution fairness, allowing a single announcer owner to drain rewards intended for the broader set of unique participants.
Severity assessment
Bug Severity: High
Impact category:
Theft of unclaimed yield
Unfair reward distribution.
Theft/Drain of rewards intended for other participants.
Likelihood: High
The attack requires only standard owner operations (
announcer_mutate,announcer_register).There is a clear financial incentive for an owner to perform this attack.
Proof of Concept
Proof of concept (Conceptual Steps):
Initial State: Owner controls
AtomAnnouncer_A(Launcher ID:L1, Inner Puzzle Hash:IPH_A).ANNOUNCER_REGISTRYstate is empty.Register A: Owner calls
registeronAtomAnnouncer_A. Registry receives message withIPH_A, checks(not (contains reg IPH_A))(true), addsIPH_A. ANNOUNCER_REGISTRY state:(IPH_A).Mutate A to B: Owner calls
mutateonAtomAnnouncer_A, providingnew_puzzle_hash = IPH_B. A new coinAtomAnnouncer_Bis created (Launcher ID:L1, Inner Puzzle Hash:IPH_B). ANNOUNCER_REGISTRY state remains(IPH_A).Register B: Owner calls
registeronAtomAnnouncer_B. Registry receives message withIPH_B, checks(not (contains reg IPH_B))(true), addsIPH_B. Registry state:(IPH_B IPH_A).Mutate B to C: Owner calls
mutateonAtomAnnouncer_B, providingnew_puzzle_hash = IPH_C. A new coinAtomAnnouncer_Cis created (Launcher ID:L1, Inner Puzzle Hash:IPH_C). ANNOUNCER_REGISTRY state remains(IPH_B IPH_A).Register C: Owner calls
registeronAtomAnnouncer_C. Registry receives message withIPH_C, checks(not (contains reg IPH_C))(true), addsIPH_C. ANNOUNCER_REGISTRY state:(IPH_C IPH_B IPH_A).Another announcer
registerwithIPH_D. ANNOUNCER_REGISTRY state:(IPH_D IPH_C IPH_B IPH_A).Another announcer
registerwithIPH_E. ANNOUNCER_REGISTRY state:(IPH_E IPH_D IPH_C IPH_B IPH_A).Claim Rewards (MINT):
MINToperation triggers onAnnouncerRegistry.announcers_countis calculated as 5.Total rewards
Rare divided by 3 (R/5).generate-offer-assertcreates payments ofR/5forIPH_A,IPH_B, andIPH_C.Owner, controlling all three puzzle hashes, claims
3 * (R/5)=60% Rtotal rewards, instead of the intendedR / (number of *unique* announcers).
Mermaid Sequence Diagram: or attached png below illustrates the attack sequence.
Was this helpful?