# KiloEx Exploit (April 2025, multi-chain perps DEX)

Sources are cited inline as [S1]..[S7], keyed to the **Sources** list at the end. Where sources disagree, each figure is given.

## 1. What happened

- **Date.** KiloEx's own post-mortem and SlowMist place the attack transactions on **Apr-14-2025, 18:52-19:40 UTC**, after funding on Apr-13-2025 23:31 UTC [S2][S3]. SlowMist's recap and Blockscope both headline the incident as "**April 15, 2025**" [S2][S5] (a source disagreement on the day label; the on-chain execution timestamps are Apr-14 UTC [S2][S3]).
- **Chains.** KiloEx's and SlowMist's timelines list deployment/attacks across **Base, BNB Chain (BSC), opBNB, Taiko, Manta and B2** [S2][S3][S5]. rekt.news also names opBNB and Manta [S1].
- **Amount lost (sources disagree).** rekt.news totals **~$7,491,500** [S1]; SlowMist and Blockscope report **~$8.44M / ~$8.4M** [S2][S5]; SolidityScan reports **~$7.4M** (approx. $3.3M Base, ~$1M BSC, ~$3.1M opBNB) [S4]; crypto.news headlines **~$7M** [S6]. rekt's per-chain breakdown: Base ~$3.13M+$187k+$11k, BNB ~$893k+$10k, opBNB ~$2.9M+$205.5k+$14k, Taiko ~$41k, Manta ~$100k [S1]. Treat ~$7.4M-$8.4M as the reported range.
- **What was taken.** User vault funds including **USDT and USDC**, plus **synthetic assets (VUSD/vUSDC)** unwound to real value; profit was realized in **USDC** and later held as converted **ETH, BNB, WBTC and DAI** [S2][S3][S5]. Funds were seeded from **Tornado Cash (1 ETH)** and bridged out via zkBridge/deBridge/Meson/DLNBridge [S1][S2][S5].
- **Outcome.** All stolen funds were recovered after ~3.5 days under a **10% white-hat bounty** agreement; assets returned to KiloEx Safe multisigs [S2][S3][S6]. The protocol was paused and positions closed on pre-hack price snapshots [S6].

## 2. Root cause

This was a **smart-contract access-control flaw, not a stolen key, tricked signer or oracle feed compromise** (the external Pyth feed was not at fault [S1]). KiloEx's `TrustedForwarder` inherited OpenZeppelin's `MinimalForwarderUpgradeable` **but never overrode its `execute()` method**, leaving that meta-transaction entry point publicly callable [S2][S3][S5]. Downstream, `PositionKeeper.delegateExecutePositions()` only checked `msg.sender == trustedForwarder` and **did not verify that the request's original signer/sender was an authorized Keeper** [S2][S3][S5]. In OpenZeppelin's forwarder, `execute()` verifies only that the signature recovers to `req.from` (any address) and then calls `req.to` with `abi.encodePacked(req.data, req.from)` — it performs no caller authorization at all [S7]. So the attacker could craft a self-signed, self-authorized request and drive the privileged call chain.

## 3. Pattern

**Missing access control / unvalidated input and calldata in a meta-transaction (forwarder) flow** — OWASP SC01 Access Control [S4]. The vulnerable shape:

```
# TrustedForwarder inherits the OZ base and does NOT override or guard execute()
execute(req, sig):                 # permissionless
    verify(req, sig)               # only proves req.from signed it (req.from is attacker)
    req.to.call(req.data + req.from)   # arbitrary target + arbitrary calldata

# PositionKeeper: trusts the forwarder's identity, not the real origin
delegateExecutePositions(req):
    require(msg.sender == trustedForwarder)   # passes for anyone via execute()
    # no check that the recovered signer is a whitelisted Keeper

# Keeper -> KiloPriceFeed.setPrices(...)  # reachable via the chain above
```

Reachable chain: `TrustedForwarder -> PositionKeeper -> Keeper -> KiloPriceFeed` [S2][S4][S5]. The attacker set the price low, opened a leveraged long, set it high, and closed it, all in one transaction, for risk-free profit [S2][S3][S4]. The flaw is the missing authorization on the relayed call, not the price-feed logic.

## 4. The check that would have caught it

- **Invariant/fuzz test (the specific one):** assert that **no state-changing path can reach `KiloPriceFeed.setPrices()` (or `PositionKeeper`/`Keeper` privileged setters) unless `msg.sender`/recovered signer is a whitelisted Keeper**. Concretely, fuzz `execute(ForwardRequest, signature)` from a non-keeper `req.from`/`msg.sender` and require revert. Invariant phrased as: *"for all callers and all calldata, the oracle price can only change when the on-chain origin is an authorized keeper."* [S4]
- **Review rule:** when a contract inherits a meta-tx base (`MinimalForwarderUpgradeable`, ERC-2771 `_msgSender()`), require an explicit **override or `onlyOwner`/`AccessControl` guard on `execute()`**, and never let `msg.sender == forwarder` alone stand in for checking the recovered signer against an allow-list; keep `setPrices`/liquidation only on role-gated paths, with nonces and deadlines [S4][S7].

## 5. Sources

- [S1] https://rekt.news/kiloex-rekt
- [S2] https://slowmist.medium.com/slowmist-assists-kiloex-in-recovering-all-stolen-funds-incident-recap-273d10f6d466
- [S3] https://medium.com/@KiloEx/kiloex-security-incident-root-cause-analysis-post-mortem-3d899caac08c
- [S4] https://blog.solidityscan.com/kiloex-vault-hack-analysis-123a086ccae3/
- [S5] https://research.blockscope.co/kiloex-exploit-analysis
- [S6] https://crypto.news/kiloex-reveals-7m-smart-contract-exploit-in-post-mortem-report/
- [S7] https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts-upgradeable/v4.8.3/contracts/metatx/MinimalForwarderUpgradeable.sol

Notes on coverage: I did **not** find a KiloEx-specific public analysis from BlockSec, Halborn, Verichains or CertiK. A QuillAudits breakdown exists (https://quillaudits.medium.com/kiloex-exploit-breakdown-7-4m-drained-across-chains-ff6e2293d5cb) but its body was bot-blocked and could not be read, so it is not relied on. The KiloEx/SlowMist timelines cite primary X posts (@KiloEx_perp, @SlowMist_Team, @CyversAlerts, @peckshield) that I did not open directly.
