---
title: The 3 Most Subtle Solidity Bugs We Found in Audits (And How We Found Them)
tags: [Article, Audit, Security, EVM, Solidity, Case Study]
---
# The 3 Most Subtle Solidity Bugs We Found in Audits (And How We Found Them)
*(This is the first article in our three-part series on protocol security.)*
## Introduction: Why Manual Review Will Always Be Non-Negotiable
In smart contract auditing, automated tools like Slither or Aderyn are an essential first line of defence. They are excellent at finding known anti-patterns: re-entrancy, incorrect visibility, or known unsafe operations.
However, the most catastrophic vulnerabilities—the ones that automated tools cannot find—are almost always flaws in the protocol's *unique business logic*. These are bugs that arise not from a single bad line of code, but from a "correct" implementation of a flawed assumption.
Finding these requires an expert, adversarial, and creative manual review process. You must understand what the code *intends* to do, and then find a way to break that intention.
This article shares three real, subtle, and high-impact vulnerabilities our team discovered during recent EVM audits. They are case studies in why expert, manual review is the most critical part of any serious audit.
---
## Bug #1: The "Poisoned" Invariant - An Accounting System Broken by 1 Gwei
**Protocol Type:** Liquid Staking Protocol
**Vulnerability:** [HIGH] Code Invariant Can Be Broken
### The Context
We were auditing a liquid staking protocol where users would deposit `TKN` into a `Vault` contract and receive a liquid-staked token, `sTKN`, in return. The protocol's entire accounting system was built on one fundamental, "golden" invariant:
> The total `sTKN` tokens minted (the `totalSupply`) MUST always equal the total `shares` held by the `sTKN` token contract inside the `Vault`.
This invariant was used in core functions to calculate the value of `sTKN` and ensure every user could redeem their tokens for the correct, proportional amount of the underlying `TKN`.
### The Bug: A Subtle Logic Flaw
Our team found that this core invariant could be permanently broken. The attack vector was not a complex exploit, but a little-known property of the EVM: force-sending Ether via `selfdestruct`.
A malicious actor could deploy a simple contract, fund it with 1 gwei, and then call `selfdestruct`, specifying the `Vault` contract as the recipient.
Because this is a "force-send," it bypasses the `Vault`'s normal `receive()` or `deposit()` functions. The `Vault`'s `address(this).balance` simply increases by 1 gwei, with no corresponding update to its internal share-tracking logic.
### The Chain Reaction
This 1 gwei of "dust" would sit in the contract until the next time an oracle called the `rebalance()` function. This function was responsible for calculating protocol rewards. It would see this extra 1 gwei, which was not associated with any deposit, and (incorrectly) account for it as a "reward".
This small "reward" poisoned the internal accounting. The next time a legitimate user deposited `TKN`, the internal `_toShares()` calculation would be based on a now-tainted pool value. This would result in the user receiving a slightly *incorrect* number of shares for their deposit.
The result: the "golden" invariant was broken. The `sTKN.totalSupply()` was no longer equal to `stakingVault.sharesOf(address(sTKNToken))`.
### Why Automated Tools Fail (And Why Experts Succeed)
An automated tool would not find this. It would see a `rebalance()` function and `_toShares()` calculations that are mathematically correct. It cannot "understand" the high-level *business assumption* that `address(this).balance` should only ever increase via a `deposit()` call.
This is a classic example of where expert manual review is critical. Our auditors had to:
1. **Identify** the core, un-written assumption (the invariant).
2. **Theorise** ways to break it.
3. **Identify** the non-obvious `selfdestruct` vector as a method to "poison" the contract's state.
4. **Write** a specific Proof-of-Concept (PoC) test to prove the invariant could be broken.
**The Fix:** The client acknowledged the finding and updated their logic to use the `Vault`'s internal `totalShares()` for calculations, which could not be poisoned by external, force-sent funds.
---
## Bug #2: The "Perpetual Interest-Free Loan" - A Business Logic Flaw
**Protocol Type:** P2P Lending Protocol
**Vulnerability:** [LOW] Borrower can avoid paying loan interests if ‘loan.fixedInterestAmount‘ is zero
### The Context
We were auditing a P2P lending contract where borrowers could take out loans with two types of interest: a `fixedInterestAmount` set at the start, and a variable `accruingInterestAPR` that grew over time. A function called `AccrueInterest` was responsible for calculating the total interest due when a loan was repaid or refinanced.
### The Bug: An Integer Division Exploit
The logic appeared sound: the function calculated the `accruedInterest` based on the time elapsed (in minutes) and added it to the `fixedInterestAmount`.
However, the variable interest calculation used `Math.mulDiv` with a very large denominator (`5.256e12`) to normalise the annual rate to a per-minute rate.
Our team spotted a subtle but critical business logic flaw:
1. A borrower could create a loan with `fixedInterestAmount = 0`.
2. Because of the large denominator, the `accruedInterest` (the numerator) would be smaller than the denominator for a significant period (e.g., the first 50 minutes of the loan).
3. Due to integer division, any calculation where the numerator is smaller than the denominator **rounds down to 0**.
This created a 50-minute window where the `AccrueInterest` function would legally and "correctly" return `0 + 0 = 0` interest.
### The Attack Vector
This wasn't just a minor rounding error; it was a blueprint for an attack. A malicious bot could be built to:
1. Take out a large loan with `fixedInterestAmount = 0`.
2. Wait 49 minutes.
3. Call the `refLoan()` function to refinance the loan.
4. The protocol would calculate the interest due (`0`), add it, and start a new loan.
5. Repeat this process indefinitely.
The bot could hold a loan for months, **paying zero interest**, simply by refinancing it every 49 minutes. This was a direct, permanent, and free-money exploit against the protocol's lenders.
### Why Automated Tools Fail (And Why Experts Succeed)
An automated tool is excellent at checking math, but it cannot understand *finance*. It would see the `Math.mulDiv` calculation and confirm it is "safe" (i.e., it doesn't overflow or underflow).
It cannot, however, understand the *business context* that a "0 interest" result for 50 minutes is a critical failure of the protocol's entire economic model.
This is a classic **business logic flaw** found only through expert manual review. Our process is not just "is the code safe?" but "can the code's *intent* be abused?" This mindset is what allowed us to find a vulnerability that was hiding in plain sight.
---
## Bug #3: The "Gas-Limit DoS" - Using a `view` Function as a Weapon
**Protocol Type:** DAO Voting / Staking Protocol
**Vulnerability:** [MEDIUM] Staker Power Calculation Denial Of Service Attack
### The Context
We were auditing a DAO's governance system where users staked tokens to get voting power. To make the system work, a `view` function called `stakerPowerAt()` was used to calculate a user's voting power at a specific time. This function worked by looping through all of a user's individual "stakes" to add them up. Since it was a `view` function, it was "free" to call from off-chain services (like a UI) and was considered "safe."
### The Bug: A Flaw in Economic Assumptions
The protocol's logic assumed that a user would have a reasonable number of stakes. Our team identified that this assumption could be weaponised.
A malicious user could intentionally create *thousands* of tiny, individual stakes for a single address. The attack isn't complex:
1. **The Attacker:** Spends gas to create thousands of small stakes for a target address.
2. **The `view` Function:** The `stakerPowerAt()` function's `for` loop now has to iterate thousands of times.
3. **The DoS:** While a `view` function is free for the *caller*, it is not free for the *node* that has to execute it. Every node (like Infura or Alchemy) has a hard-coded gas limit for `eth_call` requests. The attacker's "dust" stakes would force the loop to consume so much gas that it *always* exceeded this RPC-node gas limit.
The result: the `stakerPowerAt()` function—which was essential for the voting system to work—would become permanently unusable for that staker, causing a Denial of Service for critical protocol functions.
### Why Automated Tools Fail (And Why Experts Succeed)
An automated tool cannot understand this *economic* context. It doesn't know *why* this loop is a DoS vector. It took manual, expert review to:
1. **Identify** that this specific loop was not just "inefficient" but *mission-critical*.
2. **Analyse** the "attacker's cost" (gas to create the stakes) versus the "damage" (breaking the voting system).
3. **Recognise** that `view` functions are a common blind spot for developers who only think about *transaction* gas, not *RPC node* gas limits.
This highlights our holistic approach: we don't just audit for theft; we audit for availability, economic incentives, and gas-limit exploits.
---
## Conclusion: You Don't Find What You Don't Look For
Automated tools are a commodity. They check for what's *known*.
Our audit process is designed to find what is *novel*. We find the subtle flaws in your unique business logic, the financial loopholes in your economic model, and the adversarial assumptions that haven't been tested.
This combination of deep, manual review, a clear understanding of financial incentives, and proprietary tooling forms the core of our audit process. We find what standard tools miss.
***In our next article, ["Beyond the Code,"](./beyond-the-code.md) we explore the expert, human-led methodology that allows our team to find these vulnerabilities.***
---
If your protocol is preparing for launch, don't just check for known bugs. Contact us for an audit that uncovers the unknown.
[Request an Audit Consultation](mailto:audits@extropy.io?subject=Audit%20Inquiry%20from%20Article)
or visit
[Extropy Audits](https://security.extropy.io/audits)