new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Smart Contract Auditor · all subjects

Oracle manipulation and MEV

7 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

What checks does a safe Chainlink price feed need?

Raw `latestAnswer()` is deprecated and unchecked `latestRoundData()` is a top-5 real-world oracle bug. Minimum: `(uint80 roundId, int256 answer, , uint256 updatedAt, uint80 answeredInRound) = feed.latestRoundData(); require(answer > 0); require(updatedAt != 0 && block.timestamp - updatedAt <= heartbeat); require(answeredInRound >= roundId);` — plus a sequencer-uptime check on L2s (if the Arbitrum/Optimism sequencer just came back, `updatedAt` can be fresh but the price stale relative to L1; use the official SequencerUptimeFeed with a grace period). Also: hardcode the feed address per asset, verify the feed's `decimals()` instead of assuming 8, and never use Chainlink for low-liquidity long-tail assets it doesn't robustly cover. Detection signal: `latestRoundData` return values partially destructured with `updatedAt`/`answeredInRound` ignored — grep the destructure pattern.

How was the Euler flash-loan exploit executed (March 2023)?

~$197M, and the root cause was a missing health check, not the flash loan itself. Euler's `donateToReserves` let users burn their eTokens without the protocol checking the donor's collateralization. Attacker: (1) flash-borrowed DAI, deposited to get eDAI; (2) leveraged up via `mint()` (self-collateralized borrow, Euler-specific); (3) donated a huge eToken balance, pushing their own account into liquidation territory; (4) self-liquidated from a second account at a discount; (5) repaid the flash loan and kept the difference. Lesson: ANY function that reduces a user's collateral or debt balance — donate, gift, socialize-loss — must re-run the account liquidity check. Detection signal: functions that decrease balances without calling the protocol's `checkLiquidity`/health-factor equivalent. The flash loan only provided capital; the bug would work for any whale. Don't report "uses flash loans" as a vulnerability — report the invariant the flash loan broke.

Can an attacker manipulate Uniswap V2 spot reserves as a price oracle?

Yes, trivially, and it has burned dozens of protocols. If a lending/market contract prices collateral with `pair.getReserves()` or computes `amountOut = reserveOut * amountIn / reserveIn` from the live pair, one flash-loaned swap skews reserves for the rest of the transaction: the attacker pumps the pool, borrows against the inflated "price", unwinds the swap, repays the loan. Detection signal: any pricing path that reads `getReserves()`, `balanceOf(pool)`, or a single-block `slot0`/spot tick without time weighting. Fix: Uniswap V2 cumulative-price TWAP (`price0CumulativeLast` sampled across blocks), Uniswap V3 `observe()` with a window (commonly 30 min), or Chainlink. Also check TWAP window length vs manipulation cost — a 1-block V3 TWAP is still manipulable by a whale willing to hold the skew for one block. Rule of thumb: if price can be moved within one transaction, it's not an oracle, it's an invitation.

How does exchange-rate manipulation attack ERC-4626 vaults?

Two flavors. (1) Inflation/donation attack: first depositor mints 1 wei of shares, then donates a large amount of underlying directly to the vault; the share price jumps, so the next depositor's deposit rounds DOWN to 0 shares — attacker redeems their 1 share for ~half the victim's deposit. OpenZeppelin's fix: virtual shares/decimals offset (`_decimalsOffset`), effectively starting the vault at a non-trivial exchange rate; alternatively require a minimum first deposit minted to a dead address. (2) Reward-streaming manipulation: if `totalAssets()` counts vested rewards, an attacker sandwiches the vesting boundary. Detection signals: `totalAssets()` implemented as `asset.balanceOf(address(this))` (donation-susceptible), `convertToShares` rounding against the depositor, no decimals offset. Check that `deposit`/`mint`/`withdraw`/`redeem` round in the direction that favors the VAULT, and verify with a foundry invariant.

Should my protocol use DEX spot price for liquidation thresholds?

No. Liquidation is the worst place for manipulable prices because the profit is automatic: attacker moves the price, triggers liquidations, captures the liquidation bonus, restores the price. Mango Markets (October 2022, ~$114M) is the canonical case — the attacker pumped the MNGO perpetual price across venues with their own capital (no flash loan needed), then borrowed against the inflated collateral and walked away; "oracle" here included the venue's own mark price. Detection signals: liquidation logic reading any same-transaction-derived price (spot AMM, order book mid, single-oracle with thin liquidity); missing bounds between two price sources (e.g., revert if internal price deviates >x% from Chainlink); liquidation bonus high enough to fund the manipulation cost. Defenses: robust oracle + deviation circuit breakers, delayed liquidation triggers, per-block borrow caps.

Does this swap function need a slippage parameter?

If `swap` is called with `amountOutMin = 0` (or a deadline of `block.timestamp`), yes — that's a finding. Without a min-out bound, any searcher sandwiches the transaction: front-run buy pushes the price up, victim buys at the top, back-run sell profits the difference, victim eats the loss. Detection signals: hardcoded `0` or `1` as `amountOutMin`/`sqrtPriceLimitX96`; `block.timestamp` passed as deadline (means "whenever a builder includes it" — a stale tx can execute minutes later at a worse price); slippage computed from SPOT price instead of quoted-with-tolerance. Fixes: require the caller to pass a real `amountOutMin` and a short absolute deadline; for protocol-internal swaps use an oracle check (`require(out >= expected * (10000 - maxSlippageBps) / 10000)`). The bug is usually in contracts that swap on behalf of pooled funds and skip computing a bound.

Is block.timestamp dependence a vulnerability?

Depends what it drives. ~15 seconds of validator influence (post-Merge, proposers can nudge timestamps within protocol bounds) doesn't matter for a 7-day timelock but matters for: randomness, short auction endings, interest accrual rounding tricks, and "first tx after timestamp X wins" mechanics. Report severity by consequence: lottery payout seeded by `block.timestamp` = high; `require(block.timestamp >= lockEnd)` with a 30-day lock = informational at best, often a false positive to NOT report. Detection signals worth grepping: `block.timestamp %`, `keccak256(abi.encode(block.timestamp`, comparisons in tight windows (< a few minutes) that gate value transfer. Also remember `block.timestamp` is the sequencer's claim, not wall clock — on some L2s it updates in coarse steps. For on-chain randomness, use Chainlink VRF or commit-reveal; `block.prevrandao` is still biasable by the proposer within a block.

Give your agent this brain