Before Deployment, Solidity Security Is About Containing Power, Not Polishing Syntax
Most pre-deployment checklists obsess over syntax. Real failures happen when invariants, privileges, and upgrade paths were never challenged together.
Establish the problem with technical depth
The usual Solidity best-practices article is too small for the problem it claims to solve. It tells developers to clean up visibility, avoid unnecessary storage reads, and keep functions readable. Fine. None of that is what turns a live protocol into a nine-figure loss. The expensive failures show up when a protocol ships code that is locally tidy but globally unsafe: invariants are underspecified, authority is over-concentrated, and upgrade logic is treated like deployment plumbing instead of security-critical code.
LI.FI's own July 18, 2024 incident report is a sharp example. Shortly after a new facet was deployed, the protocol says attackers exploited missing validation in that facet and stole about $11.6 million across 153 wallets. The issue was not obscure compiler trivia. The contract path allowed arbitrary external calls, and the whitelist validation present in other facets was missing in this one. That is what a pre-deployment failure looks like in production: a release path that widened authority faster than review caught it.
Euler is the harder lesson because it punishes comfortable teams, not obviously careless ones. Euler's official retrospective says the protocol was exploited for about $197 million in March 2023. The root cause was not a giant pile of bad code. It was a missing health check in donateToReserves, a function introduced as part of a fix for an earlier issue. A patch that looked reasonable in isolation changed a core accounting boundary. Deployment turned that gap into a liquidation machine.
Even the old examples still matter because the mechanism has not changed. Ethereum.org's smart contract security guide still points to the DAO exploit as one of the defining incidents in the category: 3.6 million ETH was drained because untrusted external code could re-enter before the contract restored its internal truth. Ten years of better tooling did not repeal that lesson. Solidity is still capital software running in public against adversaries who get the same bytecode and much more time to study it than your users do.
Founders and investors should care because these are not "engineering hygiene" failures in the ordinary startup sense. They are balance-sheet failures. A protocol launch can instantly reprice trust, insurance, exchange support, integration willingness, and the team's next financing conversation. CTOs and Solidity engineers should care because the market does not distinguish between a bug in core business logic and a bug in the release path around it. If either one can move funds, both are product-critical.
The mechanism, the mistake, the misunderstanding
The mechanism behind most costly Solidity failures is simple: the contract reaches a dangerous state more easily than the team realized. Sometimes that means external code gets control while balances or debt are temporarily false. Sometimes it means one role can do too much. Sometimes it means an upgrade path can change production behavior without the same scrutiny the original deployment received.
The Solidity security documentation remains blunt on the first category. Reentrancy is not only about sending Ether. Any external call can create a path for hostile code or hostile sequencing, and the recommended checks-effects-interactions pattern exists to keep a contract internally true before control leaves the system. If a function updates user debt after a transfer, finalizes a share burn after a callback, or depends on another contract that can observe stale state, the bug is already larger than one function. It is an invariant bug.
The second category is authority. OpenZeppelin's access-control guidance frames the question correctly: who is allowed to do this thing? That sounds basic until you enumerate the answers that actually matter before deployment. Who can upgrade logic? Who can pause transfers? Who can mint, whitelist, seize, reroute, or swap an oracle? If the same key or role can do all of it, the contract may be syntactically clean and still be one compromise away from total loss. Security starts degrading long before an attacker touches the EVM. It starts when too much trust is collapsed into one actor.
The third category is upgrade semantics, which many teams still treat like framework detail. OpenZeppelin's upgradeable-contract documentation explains why that is wrong. Constructors disappear behind proxies. Initialization becomes a callable function. Storage layout compatibility becomes part of correctness. A contract can be perfectly acceptable as a one-shot deployment and dangerously wrong as an upgradeable system.
contract Vault is Initializable, AccessControlUpgradeable, UUPSUpgradeable {
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
function initialize(address admin, address pauser, address upgrader)
public
initializer
{
__AccessControl_init();
__UUPSUpgradeable_init();
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(PAUSER_ROLE, pauser);
_grantRole(UPGRADER_ROLE, upgrader);
}
function _authorizeUpgrade(address)
internal
override
onlyRole(UPGRADER_ROLE)
{}
}
That snippet is not "secure" because it compiles. It is better because it makes the dangerous questions explicit. Initialization is one-time. Upgrade power is separate from pause power. The authorization path is named, reviewable, and testable. That is what best practice should mean in Solidity: not prettier code, but fewer ambiguous ways for reality to diverge from the team's safety story.
The misunderstanding underneath all of this is that many teams still treat best practices as a checklist of lintable patterns. Best practices are not style advice. They are design decisions that make bad states unreachable, obvious, or expensive to reach. If a practice does not narrow privilege, harden invariants, or reduce upgrade risk, it is probably useful engineering, but it is not one of the reasons a protocol survives mainnet.
What good looks like
Good pre-deployment Solidity work starts before the final audit window. First, define the invariants in plain language and make them testable. Solvency should remain true across arbitrary call sequences. Mint authority should stay bounded. Reward accounting should conserve value. Upgrade execution should not silently change the role graph or storage assumptions. If the team cannot state those properties in one sentence each, it will not defend them reliably in code review.
Second, compress privilege surfaces aggressively. Ethereum.org's security guide explicitly recommends proper access controls and warns that single-owner designs create centralization and single-point-of-failure risk. In practice, that means separating upgrader, pauser, treasury, and parameter-management powers wherever possible, and putting meaningful authority behind multisig control instead of a lone EOA. The point is not bureaucracy. It is blast-radius reduction.
Third, treat every external call and every configurable target as a trust boundary. Solidity's own docs recommend checks-effects-interactions because state must be finalized before interaction. LI.FI's incident report shows the adjacent lesson: if your architecture permits arbitrary calls through adapters, routers, or facets, validation is not a nice-to-have. It is the difference between orchestration and theft.
Fourth, review upgradeability as if it were a separate subsystem, because it is. OpenZeppelin's upgrades docs are clear that initializer logic and storage layout rules are fundamental, not optional nuance. Its @openzeppelin/upgrades-core tooling goes further and provides a validate command specifically to check upgrade safety and storage-layout compatibility. If a team is shipping proxies and not validating storage compatibility in CI, it is choosing to learn upgrade risk in production.
Fifth, test on hostile state, not only on happy-path mocks. Foundry's invariant-testing guide defines invariants as properties that should always hold regardless of action sequence. Its fork-testing guide explains why testing against real chain state matters for integrations and upgrades. Those are not luxury steps for mature teams. They are what pre-deployment seriousness looks like when contracts depend on live tokens, live oracles, or pre-existing positions.
Finally, freeze the exact commit that is being reviewed and refuse to blur the line between "audited" and "currently deployable." Euler's story is the warning. A fix, an audit, and a later deployment can still leave the dangerous delta in the live system. If the commit changes, the security claim changes with it.
ChainShield's angle
ChainShield's view is that the word "best" has drifted in smart contract security. Too often it means the codebase looks disciplined to other developers. That is not enough. The relevant standard is whether the live release can defend its invariants, explain its authority graph, and survive the exact upgrade path it is about to take.
That is why ChainShield cares less about ceremonial checklist compliance and more about change-surface proof. Show the invariant tests. Show the role inventory. Show the upgrade validation. Show that the audited commit and the deployable commit are the same thing, or explain why they are not. If the team cannot do that, then "best practices" is still just a style guide taped over capital risk.
Before deployment, secure Solidity is mostly about containing power: power to re-enter, power to mutate state unexpectedly, power to upgrade, and power to operate with stale assumptions. Teams that constrain that power before mainnet have a chance to earn trust. Teams that polish syntax while leaving authority and invariants vague are just shipping a cleaner path to the same old incident report.
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