Full Report
Dark pools are private asset exchanges designed to provide additional liquidity and anonymity for trading large blocks of securities away from the public eye. Zellic was auditing a dark pool to withdraw and deposit when they found a bug. The darkpool could hold funds for ETH, ERC20 and ERC712 tokens. The LP positions were being held as non-fungible ERC-712 tokens. For all of these funds, they consisted of three fields: asset type, amount and footer (hash). All of this information is stored within a single merkle tree but isn't domain separated at all. Since these are not domain separated, type confusion bugs may be possible. In particular, use one note type in a function that expected another in order to force unexpected functionality to occur. The types of notes are not explained well in the article but I'm doing my best to understand the flow of it. The function split() can be used to split a note into different positions. If this is called on a different asset, such as a fungible note, it will treat the second field as the amount even on a non-fungible note type. By using an NFT note for a function meant to be a fungible note, the liquidity from a previous transfer can be split. Now, when the attacker calls uniswapRemoveLiquidity, it will withdraw the funds from the other user. To fix the bug, a domain separator on the different note types to prevent the type confusion. Type confusion bugs are all over the place but don't always work out because things need to line up. This was a good bug and an interesting read!
Analysis Summary
# Vulnerability: Type Confusion in Dark Pool Asset Notes Leading to Liquidity Theft
## CVE Details
- CVE ID: N/A (Audit finding, not assigned public CVE)
- CVSS Score: N/A (Assessment by auditor)
- CWE: CWE-840: Improper Restriction of Object-related Type or Class (Type Confusion)
## Affected Systems
- Products: The private asset exchange component referred to as the "dark pool" within the audited protocol (identified as **Singularity** in the context).
- Versions: Not specified, as this was identified during an audit prior to public disclosure/patch release timeline reference.
- Configurations: Systems storing ETH, ERC-20, and ERC-712 notes within a single, undifferentiated Merkle tree.
## Vulnerability Description
The dark pool service maintained user asset positions (ETH, ERC-20, ERC-712 tokens) using internal "notes" stored in a single Merkle tree. These notes were composed of three fields: **asset type, amount/ID, and footer**.
The critical flaw was the **lack of domain separation** between fungible note types (like ETH/ERC-20) and non-fungible note types (ERC-712, used for Uniswap LP positions). Both note types were hashed similarly: Fungible notes used the `amount` field, while NFT notes used the token `id`.
An attacker could exploit this when calling the `split()` function—which expects and operates on fungible notes—using an ERC-712 (NFT) note as input. Because the fields were not domain-separated in the hash calculation, the `split()` function incorrectly interpreted the NFT's unique token ID field as a subtractable `amount` field. This allowed an attacker to artificially "split" an existing LP position note belonging to another user (B) into smaller value notes, effectively stealing the liquidity corresponding to the original NFT position when subsequently calling `uniswapRemoveLiquidity`.
## Exploitation
- Status: Theoretical/Demonstrated during audit; not reported as exploited in the wild based on context.
- Complexity: Medium (Requires specific interaction sequencing involving liquidity provision and note manipulation).
- Attack Vector: Network (Smart Contract Interaction)
## Impact
- Confidentiality: No direct impact.
- Integrity: **High**. Total loss of locked liquidity positions for affected users.
- Availability: Moderate. Disruption to the intended functionality of asset withdrawal for stolen positions.
## Remediation
### Patches
The recommended fix was to introduce domain separation into the note commitment structure:
1. **Fungible Notes:** Hashed using `hash(DOMAIN_SEPARATOR_FUNGIBLE, asset, amount, footer)`.
2. **Non-Fungible Notes (NFTs):** Hashed using `hash(DOMAIN_SEPARATOR_NON_FUNGIBLE, asset, id, footer)`.
Where `DOMAIN_SEPARATOR_FUNGIBLE` and `DOMAIN_SEPARATOR_NON_FUNGIBLE` are distinct, non-colliding constants. Logic enforcing these separators (e.g., the `split` circuit must fail if the input note does not contain the expected fungible domain separator) was also required.
### Workarounds
No explicit temporary workarounds were detailed, as the fix required code changes to the hashing and validation logic.
## Detection
- **Indicators of Compromise:** Unexplained loss of LP position NFTs or a sudden decrease in the amount associated with an existing NFT note balance, followed by an unexpected removal transaction.
- **Detection Methods and Tools:** Monitoring state transitions within the Merkle tree logic, specifically looking for functions designed for fungible assets (like `split`) being successfully executed with inputs whose underlying structure corresponds to non-fungible asset commitments.
## References
- Vendor Advisories: Singularity (The protocol audited by Zellic).
- Relevant Links:
- Zellic Security Roundup: July '24 (hxxps://substack.com/@zellic)
- Singularity Audit Report (hxxps://reports.zellic.io/publications/singularity)