DeFi & Prediction Market Smart Contracts: Security, Risk & Protocol Design

·6 min read

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:

// 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:

(, 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:

  1. Every upgrade goes through a timelock. Users must be able to exit before any change touches their money.
  2. Storage layouts are append-only and documented. A migration that reorders slots is not an upgrade, it's a destruction.
  3. initialize() is initializer-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:

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:

Treasury and fee flows

Money that flows through the protocol is almost always the attack target. Two patterns that have saved me:

  1. Separate the vault from the logic. A minimal vault contract with a tiny attack surface holds funds. Logic contracts have transfer permission, not storage.
  2. 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:

The five-bullet pre-deploy checklist

Before mainnet, on every DeFi or prediction market contract:

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.

Related posts