Full Report
The Graph is a decentralized indexing protocol. Developers can access and query data across different blockchain using web2 APIs. Many projects, use this for UIs but also for backend services. It falls into the blockchain infrastructure category. To pay for using the service, there is a token. This is where the TWO bugs are at. A subgraph API is the curator of creator of the content. To have this be created, a user needs to stake tokens. Alongside this, they pay a 1% curation tax in GRT tokens to the platform. When calling mint() the amount of tokens paid is rounded down. If the tax was 1%, then sending in 99 tokens would result in 0 tokens, instead of 1, being sent in. To me, this feels fairly insignificant because the cost of gas would be much higher. However, the article claims that since this was deployed on Avalanche (low gas cost EVM chain). To be honest, this felt sorta hand-wavy but I'll live with it. By batching 99 tokens per call in a contract, the cost is cheaper than the cost of gas. This steals revenue from the protocol, which is bad. When a node operator for the Graph wants to provide services to earn rewards, they stake their GRT tokens for some period of time. When unstaking, there is a thawing period in order to ensure that bad indexers are penalized for their actions. When calling unstake() there's a calculation error that allows a user to bypass the lock duration. There's a weighted average function for unstaking. It takes into consideration the total unstaked tokens and the amount of tokens newly being unstaked. Given this information, it will return a time where the tokens can be unstaked. This function is vulnerable to a rounding issue when the currentUnstakedTokens is 201600 larger than newUnstakedTokens. When this happens, the newLockedUntil function will return the previous time! Using the same strategy as before, an attacker can unstake small batches of tokens at a time to avoid the locking period. Rounding bugs are very unintuitive issues for me. The first one makes sense but required a specific blockchain to make viable. The second one I stared at the function for a while and played with some numbers until I understood it. It seems that with small numbers, the rounding is bad! I had two big takeaways. First, if the supported chain for the protocol is on a cheap fee blockchain, then the small rounding errors become a bigger deal. Second, is a heuristic for checking these in my head. Initially, checking if the rounding direction if good or bad for the protocol. If it's bad, then review the impact then play with the numbers some to understand the impact. Good review!
Analysis Summary
# Vulnerability: The Graph Protocol Rounding Errors in Curation and Staking
## CVE Details
- **CVE ID:** Not assigned (Identified via Immunefi Bug Bounty Program)
- **CVSS Score:** Critical/High (Internal program rating)
- **CWE:** CWE-682: Incorrect Calculation (Rounding Error)
## Affected Systems
- **Products:** The Graph Protocol Smart Contracts
- **Versions:** Vulnerable versions deployed prior to January 2024
- **Configurations:**
- `Curation.sol` and `L2Curation.sol` (specifically the `mint` function)
- `Staking.sol` (specifically the `unstake` and `newLockedUntil` functions)
- Specifically impactful on Layer 2 (L2) deployments like Arbitrum/Avalanche due to low gas costs.
## Vulnerability Description
The protocol suffered from two distinct rounding errors:
1. **Curation Tax Evasion:** When users mint signals for subgraphs, a 1% curation tax is applied. The math was implemented such that the tax was calculated and rounded down *before* being applied. If the input amount was less than 100 units (e.g., 99 GRT), the 1% tax (0.99) rounded down to zero. On low-fee networks, an attacker can batch many small transactions to execute mints without paying the protocol tax.
2. **Thawing Period Bypass:** The `unstake` function uses a weighted average to calculate the "thawing period" (a mandatory lock duration for withdrawn funds). Due to a precision flaw in the `newLockedUntil` calculation, when the current amount of already-unstaking tokens is significantly larger (approx. 201,600x) than a new unstaking request, the function rounds down and returns the *existing* unlock time rather than extending it.
## Exploitation
- **Status:** PoC available (Authored by GregadETH); reported via whitehat disclosure.
- **Complexity:** Medium (Requires batching logic for tax evasion and specific ratio calculations for staking bypass).
- **Attack Vector:** Network (Smart Contract Interaction).
## Impact
- **Confidentiality:** None
- **Integrity:** High (Manipulation of protocol state and lock durations)
- **Availability:** None (Protocol remains active, but financial logic is compromised)
- **Financial:** Loss of protocol revenue (tax evasion) and circumvention of security-critical slashing windows (staking bypass).
## Remediation
### Patches
The Graph development team has deployed fixes to the following contracts:
- **Curation Fix:** Updated the calculation to determine tokens *after* tax first, then subtracting that from the total to ensure the tax amount favors the protocol.
- **Staking Fix:** Corrected the weighted average formula to prevent precision loss during the `newLockedUntil` calculation.
- **Reference Commit:** `6a87193f809d403a835ab188bdd90beeb77d6c2b`
### Workarounds
- No immediate workarounds were provided for users; the protocol addressed the issue via smart contract upgrades.
## Detection
- **Indicators of Compromise:** High volumes of transactions interacting with the `mint()` function with amounts exactly at 99 units of GRT.
- **Detection Methods:** Monitor for many small, batched calls to Curation contracts that result in zero tax accumulation, or indexer accounts that successfully withdraw funds earlier than the standard thawing period would allow.
## References
- **Immunefi Report:** hxxps://medium[.]com/immunefi/the-graph-rounding-error-bugfix-review-c946ff470f65
- **The Graph Repository:** hxxps://github[.]com/graphprotocol/contracts
- **Immunefi Top 10 (Rounding Errors):** hxxps://immunefi[.]com/immunefi-top-10/#v062023-rounding-error