DeFi & Prediction Market Smart Contracts: Security, Risk & Protocol Design
"DeFi smart contracts" covers a lot of ground: AMMs, lending markets, perps, prediction markets, launchpads, staking, casino engines. The implementation details differ - but the failure modes are remarkably similar. Almost every high-profile hack I've read has root causes that fit in one of five buckets.
This guide is how I approach DeFi and prediction market contracts specifically: the design decisions I revisit on every project, and the concrete patterns that stop each class of bug before audit.
1. Access control is a design problem, not a modifier
The most expensive bugs I see are authorization mistakes. Usually they happen because "who can call this?" was answered in a comment instead of in the type system.
Bad (EVM):
function setFeeRecipient(address r) external {
require(msg.sender == owner, "not owner");
feeRecipient = r;
}This works until you introduce a second admin role, at which point the
require soup grows and somebody forgets a check.
Better:
using AccessControl for AccessControl.Role;
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN");
bytes32 public constant TREASURY_ROLE = keccak256("TREASURY");
function setFeeRecipient(address r) external onlyRole(TREASURY_ROLE) {
feeRecipient = r;
}Pick a role model on day one. Ownable is fine for a toy; for anything with a
treasury, use AccessControl or its Solana equivalent (a PDA whose authority is
a multisig).
On Solana:
#[derive(Accounts)]
pub struct UpdateFeeRecipient<'info> {
#[account(mut, has_one = treasury_authority @ Err::Unauthorized)]
pub market: Account<'info, Market>,
pub treasury_authority: Signer<'info>,
}has_one is a habit worth forming. The validator runs on every call; you can't
forget it.
2. Reentrancy and its cousins
EVM reentrancy (and its Solana-flavored cousins involving CPI-then-read-state) is still one of the top causes of fund loss. The rule is simple, the discipline is hard: update state before external calls, and never trust a value you read after a call.
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount, "balance");
balances[msg.sender] -= amount; // state update FIRST
(bool ok, ) = msg.sender.call{value: amount}(""); // then external
require(ok, "transfer failed");
}nonReentrant is not enough by itself if the contract also has cross-function
reentrancy (different entry points sharing the same storage). Audit for every
pair of functions that touch the same slot.
On Solana: reentrancy is less likely but state-update-after-CPI bugs are real. The fix is identical: write the new state, then CPI, then return.
3. Arithmetic is where the decimals live
DeFi lives on fixed-point math. The two classic bugs:
- Wrong rounding direction. Rounding user-favorable on deposits and protocol-favorable on withdrawals is how insolvencies start.
- Precision loss on intermediate multiplications. Always multiply before dividing, and know your units.
// shares = amount * totalShares / totalAssets
// When totalShares == 0, mint 1:1 and seed a tiny "dead shares" deposit so
// the first depositor cannot be front-run into owning 100% of the vault.On Solana, checked_mul / checked_div are mandatory on user-controlled paths.
So is the seeded-shares pattern - the Jupiter and Marinade codebases are good
references.
4. Oracles: assume they lie, plan for it
Every protocol that touches price data has an oracle failure mode. Things I always check:
- Staleness. Reject feeds older than N seconds.
- Deviation bands. Reject a price that moved more than X% from the last accepted price within Y seconds.
- Circuit breakers. If the feed is wrong, the protocol pauses, it does not keep trading.
(, int256 answer, , uint256 updatedAt, ) = oracle.latestRoundData();
require(answer > 0, "bad price");
require(block.timestamp - updatedAt <= MAX_STALE, "stale price");The same story with Chainlink VRF in casino engines: assume the callback might be delayed; design the state machine so a pending request can always be resolved later without losing funds.
5. Upgradeability without footguns
Upgradeable proxies are how you ship DeFi in 2026. They are also how you ship exploits. The three rules:
- Every upgrade goes through a timelock. Users must be able to exit before any change touches their money.
- Storage layouts are append-only and documented. A migration that reorders slots is not an upgrade, it's a destruction.
initialize()isinitializer-protected. Forgetting this is still a top-ten bug.
Solana's equivalent is "admin upgrade authority". Same idea: multisig, timelock, explicit migration path.
Prediction market contracts, specifically
Prediction markets (Polymarket, on-chain CLOB markets, parimutuel pools) combine most of the risks above with a few of their own.
Market resolution
The resolution path is the single most sensitive part of the protocol. Design questions I ask up-front:
- Who is the canonical oracle for "did this thing happen"? UMA, Chainlink, a multisig, a committee?
- What happens if the oracle returns invalid (the canonical "we can't tell" state)? Users must be able to cancel, not have funds stuck.
- Is there a dispute window? Who can dispute, what's the bond, what happens to the bond on a correct vs incorrect dispute?
A good resolution state machine looks roughly like:
┌──────────── Proposed ────────────┐
│ │
Open ──► Closed ──► AwaitingResolution ──► Resolved(outcome) ──► Claimable
│
└──► Disputed ──► Re-resolved ──► Claimable
│
└──► Invalid ──► Refundable
Every transition is a function. Every transition has explicit caller restrictions and explicit state preconditions. No path moves funds without the state being exactly the right enum variant.
CLOB vs AMM trade-offs
Two common designs for prediction market liquidity:
CLOB (orderbook): precise pricing, familiar to traders, harder to seed liquidity. Polymarket-style. The contract (or off-chain matching + on-chain settlement) has to handle order placement, cancellation, matching, and fills without double-spending shares.
AMM (automated market maker): LMSR or constant-product over outcome tokens. Easier to bootstrap, worse pricing for informed flow, tighter exposure on the pool.
I usually recommend CLOB for serious prediction markets with real volume, and an AMM only where the market is narrow and episodic (e.g., single-event settlement with no need for continuous order flow).
Risk controls the contract enforces
Off-chain risk controls are table stakes (see my trading infrastructure guide). But the contract itself should also enforce:
- Per-outcome supply cap to avoid runaway minting via a bug in the mint path.
- Fee cap - no "admin can set fee to 99%".
- Max position per address if the product needs it (regulated venues sometimes do).
- Global pause - behind the same timelock as upgrades, so a live exploit can be contained.
Treasury and fee flows
Money that flows through the protocol is almost always the attack target. Two patterns that have saved me:
- Separate the vault from the logic. A minimal vault contract with a tiny attack surface holds funds. Logic contracts have transfer permission, not storage.
- Pull, don't push. Users call
claim()to withdraw rewards / settlements. Avoid for-loops that push to many recipients - they are a denial-of-service waiting to happen.
Testing: unit, integration, fuzz, invariant
For anything I'd ship to mainnet:
- Unit tests for every instruction / external function, happy + revert.
- Integration tests that walk the full state machine (market open → trades → close → resolve → claim).
- Fuzz tests (Foundry's
forge test --fuzzor Solana'ssolana-program-test+proptest) on arithmetic-heavy paths. - Invariant tests that assert "sum of all user balances == vault balance" across random action sequences. This single invariant has caught more bugs for me than all unit tests combined.
The five-bullet pre-deploy checklist
Before mainnet, on every DeFi or prediction market contract:
- Access control is role-based and role assignments are documented.
- All external calls come after state updates; no cross-function reentrancy.
- Arithmetic is checked, rounding direction is correct, first-depositor attack is mitigated.
- Oracle staleness + deviation checks + a global pause, with a timelock on every admin action.
- Invariant tests pass on at least 10k randomized action sequences.
If any of those is "we'll get to it", I don't ship.
Conclusion
Secure DeFi is less about cleverness than about discipline: role-based access, state-before-CPI, checked math, oracle hygiene, timelocked upgrades, and invariant testing. Prediction markets inherit all of that plus the resolution state machine - which deserves more of your attention than any single part of the code, because it is where the funds finally move.
Design those right and the rest of the protocol has room to be interesting.