Most Solidity Exploits Are Shipped as Legitimate Features First
Most Solidity exploits do not begin with exotic bytecode tricks. They begin when teams ship a permission, integration, or upgrade they never modeled as an attack surface.
Establish the problem with technical depth
That is the uncomfortable truth behind most "best practices" conversations in Solidity. Teams still talk as if smart contract security is mainly about avoiding obvious language mistakes inside individual functions. In production, the expensive failures usually happen one layer up. They happen when a team adds a new facet, wires in a new market, widens a role, or pushes an upgrade path that looks operationally normal right until funds leave the system.
LI.FI's July 16, 2024 incident is a clean example. In its own incident report, LI.FI said the breach happened shortly after adding a new smart contract facet. A validation check that existed elsewhere in the system was missing in the new facet, which allowed arbitrary calls without the expected whitelist enforcement. The result was unauthorized access to wallets that had granted infinite approvals, with an estimated loss of about $11.6 million across 153 wallets.
Penpie's September 3, 2024 exploit tells the same story from a different angle. Penpie's own post-mortem says the attacker stole 11,113.6 ETH, roughly $27.3 million. The root cause was a reentrancy vulnerability in reward harvesting logic, but the exploit became practical because the protocol's permissionless market registration let the attacker introduce a fake Pendle market and weaponize that reentrancy path.
This matters to founders and investors because the losses are not abstract engineering defects. They are capital events. The bug changes treasury durability, counterparty confidence, insurance posture, and the team's ability to ship under pressure after the incident. It matters even more to CTOs and Solidity engineers because secure code is not just code that passes review. It is code whose privileges, upgrade paths, external calls, and invariants still make sense after the next feature lands.
Solidity's own security guidance points in that direction. The official docs emphasize keeping contracts small and modular, using the checks-effects-interactions pattern, including fail-safe modes, and taking peer review seriously. None of that advice is really about style. It is about reducing the number of places where a valid-looking product change can quietly become a valid-looking exploit path.
The mechanism, the mistake, the misunderstanding
The mechanism behind these failures is straightforward: legitimate features create legitimate authority, and legitimate authority becomes dangerous when it is broader than the team thinks.
In LI.FI's case, the dangerous surface was an upgrade that introduced a new code path without the same call validation the rest of the system relied on. In Penpie's case, the dangerous surface was the interaction between reward accounting, reentrancy, and permissionless market registration. In both cases, the exploit did not require breaking Solidity itself. It required finding a production path that the protocol considered normal and then driving that path into a state the designers had not constrained.
That is why developers get misled by shallow best-practice lists. "Use the latest compiler." Yes. "Add a reentrancy guard." Sometimes. "Get an audit." Obviously. But none of those by themselves answer the real pre-deployment questions:
What new authority does this release create?
What assumptions does this integration make about the contracts on the other side?
Which state transitions would be catastrophic if repeated, interleaved, front-run, or executed by the wrong role?
What changes can happen after deployment without going back through the same security bar as the original launch?
The biggest mistake is treating the function body as the unit of security review. The unit of security review is the trust surface. That includes privileged roles, upgrade executors, initializer paths, external call targets, paused-state escape hatches, and the business invariants that must remain true no matter how users and adversaries sequence calls.
OpenZeppelin's access-control guidance is blunt on the key point: least privilege is a security practice, not an implementation detail. If one operator key can pause, upgrade, reroute funds, list new markets, and rescue assets, then the protocol has created a single point where operational convenience and exploit blast radius collapse into each other. The problem is not only key compromise. The problem is that the code has already encoded too much trust into one surface.
Upgradeable systems make this worse when teams treat deployment as the end of the hard part. OpenZeppelin's upgradeability docs emphasize correct initializer design, manually calling parent initializers where needed, and locking implementation contracts so they cannot be taken over. That is not housekeeping. It is the difference between a proxy system whose control plane is explicit and one whose most important assumptions only exist in the deployer's memory.
The misunderstanding underneath all of this is that "secure Solidity" is often framed as a code cleanliness exercise. It is not. Secure Solidity is a state-machine design exercise about whether the protocol can still only reach acceptable states when real users, real keepers, real admins, and hostile contracts all interact with it in ugly orders.
What good looks like
Good Solidity practice before deployment starts with privilege mapping, not linting. Write down every actor that can change production truth: multisigs, operators, keepers, upgrade executors, emergency admins, allowlist managers, market listers, oracle updaters. Then narrow each one until it can do exactly one class of job. If a role does not need to move funds or swap implementations, do not let it.
Next, treat every upgrade or feature add as new attack surface, not as maintenance. LI.FI's incident is the reminder here: one new facet with one missing check can bypass the habits that made the rest of the system look mature. A serious pre-deployment review asks not only whether the new code works, but whether it preserves the same validation, authorization, and accounting guarantees as the old code paths.
After that, encode invariants before mainnet. Foundry's invariant testing exists for exactly this reason: you can define properties that should remain true regardless of call sequence and then force the system through random action chains until those properties break or hold.
function invariant_assetsCoverLiabilities() public view {
assertGe(vault.totalAssets(), vault.totalSupply());
}
That tiny test does more real security work than a dozen checklist items if the protocol's core failure mode is insolvency. The point is not the exact invariant above. The point is that every serious protocol should know which truths are worth making executable.
Then run fork tests for anything that touches external integrations. Foundry's fork-testing model is valuable because integration risk is often invisible in isolated unit tests. If the protocol depends on live token behavior, real router assumptions, or existing liquidity conditions, the team should test against pinned chain state before pretending the integration is understood.
Finally, make the deployment packet part of the security review. That means compiler version, constructor or initializer arguments, role assignments, proxy admin ownership, emergency controls, approved external targets, and any operational scripts used to push the release. A PoC is a proof of concept: enough evidence to show a bug is real. Before deployment, your job is to make sure an attacker cannot build that proof from your release process itself.
ChainShield's angle
ChainShield's view is that the best Solidity teams do not defend source code in isolation. They defend change.
That means the interesting review target is rarely just src/. It is the diff plus the privileges plus the deployment payload plus the assumptions about the contracts around you. It is the live control plane, not just the compiled artifact.
That perspective changes how security work gets prioritized. Instead of asking whether the protocol has already "done security," the better question is whether the next legitimate change can introduce an unsafe state faster than the team can detect it. If the answer is yes, the protocol is not under-secured because engineers are sloppy. It is under-secured because the review process is pointed at the wrong object.
So the Solidity best practices that matter before deployment are not the ones that make engineers feel tidy. They are the ones that make dangerous states harder to express, privileged actions harder to misuse, integrations harder to over-trust, and post-launch changes harder to slip through under routine operational cover.
Most exploits are not born looking malicious. They are born looking shippable. The teams that survive are the ones that review legitimate features with the same suspicion attackers will.
ChainShield Discovery Runs are designed to identify high-risk issues quickly, validate what matters, and give engineering teams a faster path to remediation.
Request Security Quote