You're 30 days from mainnet. You've had one internal review. The contract handles real money. The three things most likely to cost you everything are probably already in the code.
Not because your engineers are careless. Because the failure modes in smart contract development are genuinely non-obvious, the tooling catches a narrow slice of them, and the adversarial pressure on deployed contracts is immediate and relentless. A critical vulnerability in a live contract on mainnet doesn't get a patch window. It gets exploited.
What the stakes actually are
The numbers are public. Reentrancy: The DAO, $60M, 2016. Still happening. Ronin Bridge: $625M, 2022. Access control failure. Euler Finance: $197M, 2023. Flash loan + logic flaw. These aren't ancient history — the category of vulnerability that drained The DAO has appeared in contracts deployed in the last 12 months.
The relevant number for your situation is smaller but no less fatal to the project: a contract handling $500k in user funds, exploited before you've reached the user base you needed to raise your next round, is a company-ending event. The reputational damage in Web3 communities propagates fast and doesn't recover.
We've built production contracts for on-chain systems where asset provenance and access control were non-negotiable from day one. See how we approached Sigil →
The three categories of production smart contract failures
Reentrancy
The mechanism: a function sends ETH (or calls an external contract) before updating its own state. The recipient contract's receive() or fallback() function calls back into your contract before the first execution completes. Your state hasn't been updated yet, so the condition check passes again. The loop continues until the contract is drained.
// Vulnerable: state update happens after the external call
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
(bool success,) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount; // Too late — attacker has already re-entered
}
// Correct: update state before external interaction
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount; // State updated first
(bool success,) = msg.sender.call{value: amount}("");
require(success);
}
The nonReentrant modifier from OpenZeppelin's ReentrancyGuard is the standard mitigation. It's one import and one modifier. The failure mode is not using it, or using it inconsistently (applying it to some functions but not others in the same contract, when cross-function reentrancy is possible).
Cross-function reentrancy is the subtler variant: function A calls an external contract, which re-enters function B (not A). If A and B share state, B's state reads reflect pre-A values. This variant is more common now because developers have internalized the single-function reentrancy pattern but not the cross-contract state consistency requirement.
Access control failures
The mechanism: a privileged function — minting tokens, pausing the contract, upgrading the implementation, withdrawing fees — is callable by addresses that should not have that capability. Either the access check is missing entirely, implemented with a logic error, or the privileged role was misconfigured at deployment.
The Ronin Bridge exploit was not a code bug in the traditional sense. The multisig required 5-of-9 signatures to authorize withdrawals. The attacker compromised five validator private keys. The access control model itself was the vulnerability — too few signers, too much key concentration, no hardware security for validator keys.
Production access control requires:
onlyOwneror role-based access (OpenZeppelin AccessControl) on every function that modifies critical state- Multi-sig or timelock on privileged operations (minting supply, upgrading contracts, withdrawing protocol fees)
- Events emitted for every role grant and revoke, so the access history is auditable on-chain
- Explicit review of the deployment script: what roles get assigned to what addresses at deployment, and what's the process for rotating them?
The failure mode is often in the deployment configuration rather than the contract logic. A contract with correct onlyOwner checks but the wrong address set as owner at deployment is a live vulnerability.
Oracle and price manipulation
The mechanism: a contract takes an action based on the price of an asset, and an attacker manipulates that price (via a flash loan that moves a liquidity pool) within the same transaction. The contract sees the manipulated price as real, executes at the wrong ratio, and the attacker walks away with the difference.
This is specific to contracts that read price data from an on-chain source — an AMM pool's spot price, a liquidity pool ratio. The fix is time-weighted average prices (TWAP) from an oracle like Chainlink or a TWAP calculation over the last N blocks, which require multiple transactions to manipulate (and thus are economically unviable for flash loan attacks).
If your contract doesn't touch prices or external asset valuations, this category doesn't apply. If it does, using a spot price from a liquidity pool as if it were a real market price is a critical vulnerability.
What a proper pre-mainnet review looks like
A tool scan (Slither, Mythril) catches a defined class of known patterns — integer overflow risks, some reentrancy patterns, common access control issues in standard patterns. It does not catch logic errors, economic attack surfaces, or the interaction effects between your contract and the specific ecosystem it's deploying into.
An adversarial read is different. It's a reviewer who reads the contract with the question "how would I steal from this?" not "does this match known vulnerability patterns?" The goal is to find the attack surface that is specific to this contract's logic, not generic to all contracts.
Specifically, a proper review covers:
State transition analysis. Map every state a contract can be in, every function that changes state, and whether any state transition is reachable that the design didn't intend. Draw the state machine. Attack the unexpected transitions.
Privileged operation inventory. Every function that requires elevated access, every address that holds elevated access at deployment, every mechanism by which access can be transferred or revoked. Is there a path where the owner key is lost and the contract is bricked? Is there a path where access is transferred to an unexpected address due to a logic error?
External call map. Every call to an external contract or address. What can the external contract do? What state is your contract in when the call is made? What happens if the external call reverts? What happens if the external contract is malicious?
Economic model analysis. If your contract has economic incentives — token distribution, fee mechanisms, staking — model the adversarial case. Is there a sequence of valid transactions that drains the contract? Is there a profitable MEV (miner extractable value) opportunity that the contract inadvertently creates?
Gas limit analysis. Loops that iterate over unbounded arrays will hit the block gas limit at scale. A function that works with 10 elements in testing fails with 10,000 in production.
The upgradability decision
Immutable contracts are simpler and more trustworthy — users can verify exactly what code governs their funds, permanently. But they require that the code is correct before deployment. A critical bug means deploying a new contract and migrating users, which is operationally complex and trust-damaging.
Upgradeable contracts (typically via the proxy pattern — UUPS or Transparent Proxy) allow post-deployment fixes. The tradeoff is complexity: the proxy adds attack surface (storage layout collisions between proxy and implementation, function selector clashes), and the upgrade mechanism itself becomes a critical security boundary (who can upgrade, with what process, with what timelock?).
The transparent proxy pattern separates admin and user function namespaces by the caller's address — admin calls go to the proxy, user calls are forwarded to the implementation. This is secure if implemented correctly and dangerous if the proxy address management is misconfigured.
For most production contracts, the right answer depends on value at risk and maturity of the codebase. A contract in early production with moderate value at risk: upgradeable with a 48-hour timelock on upgrades, 3-of-5 multisig with hardware keys. A contract with large value at risk and a formally verified codebase: immutable.
The default "make it upgradeable so we can fix bugs" framing underestimates the attack surface that upgradeability adds. Upgrades are admin operations — they're the highest-privilege operation in an upgradeable system, and the security of the entire contract reduces to the security of the upgrade key.
What production Polygon/EVM deployment actually requires beyond the contract
Deployment automation. A deployment script that is version-controlled, reviewed, and executed deterministically. Not a sequence of Hardhat console commands run by hand. The deployment script is part of the security surface — it sets the initial state, assigns roles, and configures the contract. A manual deployment step is a point of human error.
Verification. Every deployed contract verified on the block explorer. Not optional. Unverified contracts signal distrust and make security review by your users impossible.
Event architecture. Every significant state change emits an event. Token transfers, role changes, pauses, ownership transfers. Events are the contract's audit trail and the basis for your off-chain indexing.
Frontend integration security. The contract may be correct and the frontend may introduce vulnerabilities: CORS-misconfigured RPC endpoints that allow unauthorized reads, insecure wallet connection flows, client-side state that doesn't match on-chain state after a reverted transaction. The attack surface extends to the full stack.
Incident response plan. What's the procedure if a vulnerability is discovered post-deployment? Who makes the decision to pause? How fast can the pause execute? What's the communication plan to users? This is a pre-deployment conversation, not a post-incident one.
What fixed looks like
A contract that reaches mainnet has had an adversarial read by someone who was not involved in writing it. Every privileged function has explicit access control. State updates precede external calls everywhere. The deployment script is version-controlled and has been executed against a testnet at least three times. The block explorer shows verified source code the moment the contract is live.
The first 30 days on mainnet are quiet. Not because nothing happened — because the attack surface was analyzed before deployment, not after.
This is for you if
You're deploying a smart contract to mainnet on Polygon or another EVM-compatible chain with real user funds or real asset value at stake. You've built the contract and you need an adversarial review before go-live. The engineering scope for a proper pre-mainnet review — adversarial read, state transition analysis, deployment infrastructure — runs $50k–$200k depending on contract complexity and whether you need deployment infrastructure built alongside the review.
This is not for you if you're deploying a toy contract or a testnet experiment. Spend your security budget where the value at risk warrants it.