Article by @Zer0day_sec | www.0daysec.xyz
Target: STON.fi DEX v2 - contracts/pool.fc
Researcher: Zeroday Sec
Disclosure URL: https://www.0daysec.xyz/research/ston-fi-protocol-fee
Full POC: https://gist.github.com/Zee99y/7143ff27791222d45f3de8486f3f49dc
Submitted Severity: Major (ZerodaySec) - Classified Minor (CertiK SkyShield)
Status: Disputed, Unpatched as of disclosure date
Disclosure Authorization Public disclosure of this finding was expressly authorized by STON.fi via CertiK SkyShield in written email correspondence dated April 17, 2026. STON.fi requested that their official response accompany the disclosure. That response is included in full under Official Response & Rebuttal below.

Executive Summary
A logical flaw in STON.fi DEX v2’s protocol fee collection mechanism permanently blocks fee withdrawal in any pool experiencing unidirectional swap flow. The AND-gate condition in protocolfee.fc requires both token fee buckets to be nonzero simultaneously before any collection is permitted, a condition that is never satisfied in pools where swaps flow predominantly or exclusively in one direction.
The vulnerability affects all pool types sharing the protocolfee.fc module, constant product, stableswap, and weighted variants. At $25.5M TVL, sustained fee trapping represents direct, ongoing financial loss to the protocol with no recovery path available to smart contract fee receivers.
This finding was submitted to CertiK SkyShield on March 18, 2026. STON.fi characterized the behavior as an intentional design choice and the report was classified Minor by CertiK. Zeroday Sec disputes both assessments on three independent technical grounds detailed below.
Significantly, CertiK formally acknowledged in writing that “the payout path is per-token while the unlock condition is global, and that a more permissive implementation could have been used.” This concession validates the architectural argument at the heart of this report. CertiK additionally withdrew a recovery path claim cited in their original Minor rationale after being unable to identify the referenced function in the in-scope codebase. These acknowledgments are documented and quoted in context below.
This disclosure is published in the interest of protocol security, DeFi ecosystem integrity, and community transparency.
Technical Finding
Root Cause
The collect_fees handler in protocolfee.fc line 5 uses a logical AND to gate all fee collection:
;; protocolfee.fc:5, compiled into contracts/pool.fc
throw_unless(error::zero_output,
(storage::collected_token0_protocol_fee > 0) &
(storage::collected_token1_protocol_fee > 0)
);
This requires both fee buckets to be nonzero before any collection proceeds. The swap handler, however, increments only one bucket per swap direction:
;; pool.fc, swap handler
if amount0 {
;; token0 → token1
storage::collected_token1_protocol_fee += protocol_fee_out; ;; line 97
} else {
;; token1 → token0
storage::collected_token0_protocol_fee += protocol_fee_out; ;; line 104
}
Any pool processing predominantly or exclusively one-directional swaps, a standard real-world condition, will permanently have one bucket at zero. The AND-gate then causes every collect_fees call to revert with error::zero_output (exit code 81). No override, emergency path, or administrative bypass exists anywhere in the in-scope contracts.
Architectural Dissonance
The “design choice” characterization cannot be sustained against the contract’s own architecture. The collect_fees function already sends fees via two independent msgs::send_simple calls, one per token bucket:
;; First send, token0 fees
msgs::send_simple(
gas,
storage::router_address,
router::pay_to(
...
storage::collected_token0_protocol_fee,
storage::token0_address,
0,
storage::token1_address
),
NORMAL
);
;; Second send, token1 fees
msgs::send_simple(
0,
storage::router_address,
router::pay_to(
...
0,
storage::token0_address,
storage::collected_token1_protocol_fee,
storage::token1_address
),
CARRY_ALL_BALANCE
);
The execution is already modular. The guard is global. These are architecturally incompatible. A genuine design choice for atomicity would use atomic execution. A genuine design choice for gas optimization would use per-bucket conditional guards. The current implementation achieves neither coherently.
CertiK acknowledged this directly in their final written response:
“We acknowledge that the payout path is per-token while the unlock condition is global, and that a more permissive implementation could have been used.”
- CertiK Bug Bounty Team, written correspondence, April 2026
This is a formal concession of the core architectural argument. Zeroday Sec considers this acknowledgment significant and notes it for the record.
The Withdrawn Recovery Claim
CertiK’s original Minor rationale stated the condition was “operationally recoverable by owner-controlled fee configuration changes.” Zeroday Sec formally challenged CertiK to identify the specific function or mechanism. CertiK subsequently confirmed in writing:
“We are not identifying any specific in-scope function, opcode, or mechanism that bypasses the collect_fees condition.”
- CertiK Bug Bounty Team, written correspondence, April 2026
No such function exists in the in-scope codebase. The recovery path claim was withdrawn. Zeroday Sec maintains that a Minor classification whose sole recovery rationale has been formally retracted has no remaining technical foundation.
Proof of Concept
PASS - collect_fees blocked after unidirectional swaps (2689ms)
Exit code: 81 (error::zero_output)
The POC was run against the live repository. After one-directional swaps populate only one fee bucket, every collect_fees call reverts with exit code 81. Post-state verification confirms fees remain unchanged, permanently trapped. The test is fully reproducible.
Full POC with screenshots: https://gist.github.com/Zee99y/7143ff27791222d45f3de8486f3f49dc
Severity Assessment
Zeroday Sec submitted this finding at Major severity. CertiK SkyShield classified it as Minor. Zeroday Sec disputes the Minor classification on the following basis:
A Minor classification implies limited, recoverable, low-impact behavior. This finding produces:
- Permanent, irrecoverable fee loss in dead-leg and depeg scenarios, not delayed
- Complete functional failure for smart contract fee receivers with no human override
- Protocol-wide scope, shared
protocolfee.fcaffects all pool types simultaneously - No documented recovery path, the only recovery mechanism cited in triage was formally withdrawn
Zeroday Sec maintains Major as the appropriate classification and notes that the finding maps directly to STON.fi’s bounty program categories of “Permanent freezing of funds” and “Theft of unclaimed yield”, both listed as Critical impacts under their published scope. The security community is invited to assess the severity independently against the technical record.
Impact Analysis
Why “Wait for Arbitrage” Fails
STON.fi’s defense rests on the implicit assumption that market activity will eventually populate both fee buckets. Zeroday Sec identifies three standard DeFi conditions where this assumption fails permanently:
Dead-Leg Pools
A reward or incentive token that is only ever distributed (swapped out) and never purchased is permanently unidirectional by construction. No subsequent reverse swap will ever populate the zero bucket. This describes entire categories of incentive tokens, airdrop distribution pools, and protocol-owned liquidity positions.
Depeg Scenarios
A stablecoin or asset losing its peg experiences sustained one-directional outflow. Rational market participants do not swap into a failing asset. The surviving token’s protocol fees accumulate while the dead bucket remains at zero, indefinitely. At $25.5M TVL, STON.fi hosts stablecoin pairs where this scenario is not theoretical.
Smart Contract Fee Receivers
If protocol_fee_address is a smart contract, say a DAO treasury, automated fee distributor, or buy-and-burn contract, it is permanently and irreparably broken. An autonomous smart contract cannot execute a dust swap to unblock its own revenue stream. It will revert on every collect_fees call, forever, with no human override possible. This breaks the fundamental DeFi composability assumption for protocol fee architecture.
Financial Loss Estimate
| Scenario | Daily One-Dir. Volume | Monthly Loss | Annual Loss |
|---|---|---|---|
| Mid-size pool (10bps) | $500,000 | ~$12,000 | ~$144,000 |
| High-volume pool (10bps) | $5,000,000 | ~$120,000 | ~$1,440,000 |
| Protocol-wide (multiple pools) | Variable | Compounding | Irrecoverable |
Fees trapped under this condition are not delayed, they are permanently inaccessible under the current implementation.
Official Response & Rebuttal
STON.fi Official Statement
STON.fi reviewed the submission and provided the following official response, which they requested be included alongside this disclosure:
“We do not consider this report valid. The title refers to a check implemented in the contract that is, again, an intentional design choice, given that no pool on STON.fi DEX is one-sided.”
- STON.fi, via CertiK SkyShield, April 2026
Zeroday Sec rebuttal: This is a market conditions observation, not a technical argument. “No pool is currently one-sided” does not address dead-leg pools, depeg scenarios, or smart contract receivers, all three of which produce permanent failure independent of current market conditions. A vulnerability does not require active exploitation to be valid. No documentation has been produced explicitly stating the AND-gate behavior is intentional, and CertiK has itself acknowledged a more permissive implementation could have been used.
Why “Design Choice” Is Not Technically Sustainable
Zeroday Sec presents three independent grounds. Defeating one does not defeat the others.
Ground 1. Internal Architectural Contradiction
The function’s own send logic is already structured to handle each token independently. A global gate on modular logic is not a design pattern, it is an inconsistency. CertiK conceded this in writing.
Ground 2. Three Permanent Failure Scenarios
The “wait for arbitrage” assumption fails categorically in dead-leg pools, depeg conditions, and for smart contract receivers. All three are standard, foreseeable DeFi conditions. A design choice that produces permanent irrecoverable failure under standard foreseeable conditions is a defect.
Ground 3. Absence of Documentation
No line in the repository’s technical documentation, developer README, protocol specification, or inline comments explicitly states that protocol fees are intended to be locked until both token buckets are nonzero simultaneously. The absence of documentation for behavior that causes revenue loss is itself significant.
Recommended Fix
Immediate: Change AND to OR
;; Current, blocks collection until both nonzero
throw_unless(error::zero_output,
(storage::collected_token0_protocol_fee > 0) &
(storage::collected_token1_protocol_fee > 0)
);
;; Fix, allows collection when either is nonzero
throw_unless(error::zero_output,
(storage::collected_token0_protocol_fee > 0) |
(storage::collected_token1_protocol_fee > 0)
);
Complete, Per-Bucket Conditional Guards
;; Guard each send independently
if (storage::collected_token0_protocol_fee > 0) {
msgs::send_simple(
gas,
storage::router_address,
router::pay_to(..., storage::collected_token0_protocol_fee, ...),
NORMAL
);
}
if (storage::collected_token1_protocol_fee > 0) {
msgs::send_simple(
0,
storage::router_address,
router::pay_to(..., storage::collected_token1_protocol_fee, ...),
CARRY_ALL_BALANCE
);
}
This achieves zero-value transfer avoidance on each leg independently, collection whenever either bucket is nonzero, and full architectural consistency with the existing modular send design.
Gas Estimation Note
The current gas check assumes both sends will execute. With per-bucket guards, the gas formula should be updated to reflect that only one send may execute in unidirectional cases — reducing the minimum required MSG_VALUE for single-bucket collections.
Zeroday Sec Recommendations
For STON.fi
1. Apply the complete fix immediately. The AND->OR change is a one-character patch deployable via the existing pool upgrade mechanism. The per-bucket guard implementation is the architecturally complete solution.
2. Audit all live pools for trapped fees. Identify pools that have accumulated nonzero fees in one bucket with zero in the other. These pools have already experienced fee trapping. Quantify and address the trapped revenue.
3. Document protocol_fee_address constraints. If the protocol intends protocol_fee_address to always be an EOA rather than a smart contract, this constraint should be explicitly documented and enforced. If smart contract receivers are ever intended to be supported, the fix is mandatory.
4. Add collection test coverage for unidirectional pools. A test that performs one-directional swaps and then calls collect_fees would have caught this during development. Add it to the test suite regardless of fix status.
5. Commission a targeted review of protocolfee.fc. Given that this module is shared across all pool types, a focused audit of the complete fee collection path is warranted following fix deployment.
For the Security Community
1. When auditing shared fee modules in DEX contracts, verify that collection guards match the execution architecture. A global gate on per-token logic is a pattern worth checking explicitly.
2. The “currently unexploited” argument is not a validity argument. Dead-leg pools and depeg conditions are foreseeable and standard. Design choice defenses require documentation, not market observations.
3. For researchers on mediated bug bounty platforms, challenge recovery path claims for technical specificity. Any claimed recovery mechanism should be identifiable by function name in the in-scope codebase. If it cannot be named, it does not exist.
For Bug Bounty Programs
1. Recovery path claims in triage rationale must be verifiable by function identification. Unverifiable claims are not adequate technical closure.
2. Mediated programs should be mindful of how supplementary triage signals influence project review. Researcher-submitted severity and mediator severity delivered simultaneously to a project create a structural dynamic that may disadvantage technically valid findings before the project engages with the substance.
3. A concession of the core architectural argument while maintaining an adverse severity classification warrants re-triage, not maintenance of the original outcome.
Conclusion
The AND-gate in STON.Fi’s protocolfee.fc is a logical flaw, not a design choice. It is architecturally inconsistent with the function’s own implementation, produces permanent irrecoverable failure under standard foreseeable DeFi conditions, and has no documented specification supporting intentionality. CertiK formally acknowledged the architectural argument. The only recovery path cited in triage was formally withdrawn.
The fix is straightforward. The risk is real. The recommendation is unambiguous: patch, audit trapped fees, and document constraints.
Zeroday Sec publishes this disclosure in the interest of protocol security, DeFi ecosystem integrity, and the advancement of responsible security research standards.
Zeroday Research - Web3 Security Intelligence X: @Zer0day_sec Website: 0daysec.xyz Disclosure: www.0daysec.xyz/research/ston-fi-protocol-fee