Description
Background: https://x.com/SlowMist_Team/status/1911991384254402737
The vulnerability stemmed from a critical access control flaw in KiloEx’s price oracle implementation:
Attack Mechanism:
- The attacker exploited the MinimalForwarder’s lack of access controls
- They were able to forge signatures and bypass the entire access control chain
- This allowed direct manipulation of the KiloPriceFeed contract
Exploitation Steps:
- Attacker funded their wallet through Tornado Cash
- Used the MinimalForwarder to bypass access controls
- Manipulated ETH price down to $100
- Opened leveraged long positions
- Manipulated price up to $10,000
- Closed positions for profit
- 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:
- Trigger on all calls to the
setPrices
function in the KiloPriceFeed contract
- Enforcing a maximum price deviation of 5% per update
- Reverting if the price deviation exceeds this threshold
This would have prevented the attack in two ways:
- When the attacker tried to set ETH price to 100(fromthethen 2000), the assertion would have detected this ~95% deviation and reverted the transaction
- When they later tried to set the price to $10,000, the assertion would have again detected this extreme deviation and reverted
- 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.
Responses are generated using AI and may contain mistakes.