Use Case & Applications

Prevents TWAP (Time-Weighted Average Price) price manipulation by ensuring oracle prices don’t deviate more than X% from pre-state TWAP prices. Critical for AMMs and DEXs (price discovery), lending protocols (collateral valuation), yield aggregators (rebalancing), options protocols (settlement), and cross-chain bridges (asset pricing). Sudden price deviations could indicate manipulation through flash loan attacks or oracle failures, potentially leading to protocol insolvency or fund extraction.

Explanation

Monitors TWAP price changes using a two-stage approach to ensure price stability:
  • ph.forkPreState() / ph.forkPostState(): Compare post-transaction price against pre-transaction TWAP
  • getStateChangesUint(): Track all price changes during transaction execution
  • registerStorageChangeTrigger(): Trigger when price storage slot changes
The assertion verifies both the final price and any intermediate price updates against the initial TWAP to detect flash loan attacks and price manipulation throughout the transaction. For more information about cheatcodes, see the Cheatcodes Documentation.

Code Example

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

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

contract TwapDeviationAssertion is Assertion {
    function triggers() external view override {
        // Register trigger for changes to the current price
        // We assume that the price is stored in storage slot 0
        registerStorageChangeTrigger(this.assertionTwapDeviation.selector, bytes32(uint256(0)));
    }

    // Assert that the current price doesn't deviate more than 5% from the TWAP price
    function assertionTwapDeviation() external {
        // Get the assertion adopter address
        IPool adopter = IPool(ph.getAssertionAdopter());

        // Get TWAP price before the transaction (our reference point)
        ph.forkPreTx();
        uint256 preTwapPrice = adopter.twap();

        // Get price after the transaction
        ph.forkPostTx();
        uint256 postPrice = adopter.price();

        uint256 maxDeviation = 5;

        // First check: Compare post-transaction price against pre-transaction TWAP
        uint256 deviation = calculateDeviation(preTwapPrice, postPrice);
        require(deviation <= maxDeviation, "Price deviation from TWAP exceeds maximum allowed");

        // Second check: If the simple check passes, inspect all price changes in the callstack
        // This is more expensive but catches manipulation attempts within the transaction
        uint256[] memory priceChanges = getStateChangesUint(
            address(adopter),
            bytes32(uint256(0)) // Current price storage slot
        );

        // Check each price change against the pre-transaction TWAP
        for (uint256 i = 0; i < priceChanges.length; i++) {
            deviation = calculateDeviation(preTwapPrice, priceChanges[i]);
            require(deviation <= maxDeviation, "Price deviation from TWAP exceeds maximum allowed");
        }
    }

    // Helper function to calculate percentage deviation
    function calculateDeviation(uint256 referencePrice, uint256 currentPrice) internal pure returns (uint256) {
        return (((currentPrice > referencePrice) ? currentPrice - referencePrice : referencePrice - currentPrice) * 100)
            / referencePrice;
    }
}

interface IPool {
    function price() external view returns (uint256);
    function twap() external view returns (uint256);
}
Note: Full examples with tests available in the Phylax Assertion Examples Repository.