Description

Background: https://x.com/SlowMist_Team/status/1911991384254402737

The vulnerability stemmed from a critical access control flaw in KiloEx’s price oracle implementation:

  • The protocol used a chain of contracts for price updates:

    • KiloPriceFeed: Main oracle contract
    • Keeper: Contract responsible for price updates
    • PositionKeeper: Contract managing positions
    • MinimalForwarder: Contract handling transaction forwarding
  • The access control chain was implemented as follows:

    • KiloPriceFeed trusted Keeper
    • Keeper trusted PositionKeeper
    • PositionKeeper trusted MinimalForwarder
    • MinimalForwarder had no access controls

Attack Mechanism:

  1. The attacker exploited the MinimalForwarder’s lack of access controls
  2. They were able to forge signatures and bypass the entire access control chain
  3. This allowed direct manipulation of the KiloPriceFeed contract

Exploitation Steps:

  1. Attacker funded their wallet through Tornado Cash
  2. Used the MinimalForwarder to bypass access controls
  3. Manipulated ETH price down to $100
  4. Opened leveraged long positions
  5. Manipulated price up to $10,000
  6. Closed positions for profit
  7. Repeated across multiple chains (Base, BNB Chain, Taiko)

Vulnerability Details:

  • The MinimalForwarder contract lacked signature validation
  • No checks were performed on the caller’s identity
  • The entire access control chain relied on each component trusting the next
  • The protocol had been audited 5 times since June 2023, but the vulnerability remained undetected

Prevention Assertions

Price Deviation Assertion

With an assertion very similar to the Price Deviation Assertion, this attack could have been prevented. The example assertion below could with very few changes have caught the large price deviations in the attack and prevented the transaction.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Assertion} from "credible-std/Assertion.sol";
import {PhEvm} from "credible-std/PhEvm.sol";

interface IOracle {
    function updatePrice(uint256 price) external;
    function price() external view returns (uint256);
}

contract IntraTxOracleDeviationAssertion is Assertion {
    // The oracle contract to monitor
    IOracle public oracle;

    // Maximum allowed price deviation (10% by default)
    uint256 public constant MAX_DEVIATION_PERCENTAGE = 10;

    constructor(address oracle_) {
        oracle = IOracle(oracle_);
    }

    function triggers() external view override {
        // Register trigger for oracle price update calls
        registerCallTrigger(this.assertOracleDeviation.selector, oracle.updatePrice.selector);
    }

    // Check that price updates don't deviate more than the allowed percentage
    // from the initial price at any point during the transaction
    function assertOracleDeviation() external {
        // Start with a simple check comparing pre and post state
        ph.forkPreState();
        uint256 prePrice = oracle.price();

        // Calculate allowed deviation thresholds (10% by default)
        uint256 maxAllowedPrice = (prePrice * (100 + MAX_DEVIATION_PERCENTAGE)) / 100;
        uint256 minAllowedPrice = (prePrice * (100 - MAX_DEVIATION_PERCENTAGE)) / 100;

        // First check post-state price
        ph.forkPostState();
        uint256 postPrice = oracle.price();

        // Verify post-state price is within allowed deviation from initial price
        require(
            postPrice >= minAllowedPrice && postPrice <= maxAllowedPrice,
            "Oracle post-state price deviation exceeds threshold"
        );

        // Get all price update calls in this transaction
        // Since this assertion is triggered by updatePrice calls, we know there's at least one
        PhEvm.CallInputs[] memory priceUpdates = ph.getCallInputs(address(oracle), oracle.updatePrice.selector);

        // Check each price update to ensure none deviate more than allowed from initial price
        for (uint256 i = 0; i < priceUpdates.length; i++) {
            // Decode the price from the function call data
            uint256 updatedPrice = abi.decode(priceUpdates[i].input, (uint256));

            // Verify each update is within allowed deviation from initial pre-state price
            require(
                updatedPrice >= minAllowedPrice && updatedPrice <= maxAllowedPrice,
                "Oracle intra-tx price deviation exceeds threshold"
            );
        }
    }
}

Specifically, the assertion could have been configured to:

  1. Trigger on all calls to the setPrices function in the KiloPriceFeed contract
  2. Enforcing a maximum price deviation of 5% per update
  3. Reverting if the price deviation exceeds this threshold

This would have prevented the attack in two ways:

  1. When the attacker tried to set ETH price to 100(fromthethen 100 (from the then ~2000), the assertion would have detected this ~95% deviation and reverted the transaction
  2. When they later tried to set the price to $10,000, the assertion would have again detected this extreme deviation and reverted
  3. If this was all done in a single transaction, the assertion would have caught the large deviation and reverted the entire transaction

Note: In a real implementation, this assertion should be:

  • Run for each token in the protocol
  • Consider token-specific deviation thresholds
  • Handle multiple price updates in a single transaction

Access Control Assertion

The root cause of the vulnerability was the MinimalForwarder’s lack of access controls. A simple assertion could have prevented this by ensuring the MinimalForwarder is only called by authorized addresses in the expected call chain (Keeper → PositionKeeper → MinimalForwarder):

  • Trigger: Register for all calls to MinimalForwarder.execute()
  • Assertion:
    • Maintain a whitelist of authorized callers (specifically the PositionKeeper)
    • Verify the caller is in the whitelist
    • Revert if unauthorized

This simple assertion would have prevented the attack by ensuring that only the PositionKeeper could trigger price updates through the MinimalForwarder. While fixing the access control in the contract itself would be the better solution, this assertion could have served as a safety net to catch unauthorized access attempts.