Use Case
Check that the TWAP (Time-Weighted Average Price) reported by an oracle doesn’t deviate more than X% from the pre-state TWAP price. This is a critical security parameter for DeFi protocols that rely on TWAP oracles for pricing assets, as sudden price deviations could indicate manipulation or oracle failure.
This assertion is particularly important for:
- Preventing price manipulation through flash loan attacks
- Ensuring oracle reliability and accuracy
- Protecting against oracle manipulation in lending protocols
- Maintaining protocol stability during market volatility
- Detecting potential oracle failures or attacks
For example, if a protocol allows a 5% deviation, an attacker could potentially manipulate prices within this range to extract value, making this a critical security parameter that requires careful consideration.
Applicable Protocols
- AMMs and DEXs that use TWAP for price discovery
- Lending protocols that use TWAP for collateral valuation
- Yield aggregators that rely on TWAP for rebalancing
- Options protocols that use TWAP for settlement prices
- Cross-chain bridges that use TWAP for asset pricing
Each protocol type needs this assertion because:
- AMMs: Prevents price manipulation during large trades
- Lending: Ensures accurate collateral valuation for liquidations
- Yield: Maintains fair rebalancing prices
- Options: Guarantees fair settlement prices
- Bridges: Prevents cross-chain arbitrage attacks
Explanation
The assertion monitors changes to the current price by comparing it against the TWAP price from before the transaction. It uses a two-stage approach to ensure price stability:
The assertion uses the following cheatcodes:
ph.forkPreState()
: Creates a fork of the state before the transaction to capture the TWAP price as our reference point
ph.forkPostState()
: Creates a fork of the state after the transaction to get the final price
getStateChangesUint()
: Gets all state changes for the current price to detect manipulation throughout the callstack
registerStorageChangeTrigger()
: Triggers the assertion when the price storage slot changes
The implementation performs two checks:
- A comparison between the post-transaction price and the pre-transaction TWAP
- Verification of all price changes during the transaction’s execution
This multi-layered approach ensures that:
- The current price remains within acceptable bounds of the pre-transaction TWAP
- Any attempts to manipulate the price during the transaction are detected
- The protocol’s pricing mechanism cannot be exploited through flash loans or other attacks
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";
interface IPool {
function price() external view returns (uint256);
function twap() external view returns (uint256);
}
contract TwapDeviationAssertion is Assertion {
IPool public pool;
constructor(address _pool) {
pool = IPool(_pool);
}
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 TWAP price before the transaction (our reference point)
ph.forkPreState();
uint256 preTwapPrice = pool.twap();
// Get price after the transaction
ph.forkPostState();
uint256 postPrice = pool.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(pool),
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;
}
}
Note: This code example is maintained in the Phylax Assertion Examples Repository. For a full examples with mock protocol code and tests please refer to the repository.
Testing
To test this assertion:
- Deploy a mock pool contract with TWAP functionality
- Set up test scenarios with various price movements
- Verify the assertion catches deviations above the threshold
- Test edge cases like zero prices and extreme values
Assertion Best Practices
- Consider combining this assertion with other price-related assertions like Intra-tx Oracle Deviation for comprehensive security
- Use the
getStateChangesUint
cheatcode to detect price manipulation throughout the callstack
- Consider implementing different deviation thresholds for different market conditions (eg. stablecoin pools vs volatile assets)
Responses are generated using AI and may contain mistakes.