TWAP (Time-Weighted Average Price) is a price calculation that averages the price of an asset over a specific time period, making it resistant to manipulation.
Aerodrome pools store price observations at regular intervals (every 30 minutes by default):
- Each observation records: timestamp, cumulative price
- TWAP calculation: (Price_end - Price_start) / (Time_end - Time_start)
- Longer time windows = more manipulation resistance
Spot Price: Current instantaneous price in the AMM
- Can be manipulated with a single large trade
- Reflects immediate supply/demand
TWAP Price: Average price over time window (e.g., 4 hours)
- Requires sustained capital to manipulate
- Lags behind spot price during volatility
TWAP requires historical data to calculate:
- New Pool = No History: When a pool launches, there are zero observations
- Minimum Observations: Need at least 2 observations to calculate TWAP
- Reliable TWAP: Requires ~8-12 observations (4-6 hours) for manipulation resistance
// Example: Pool with 0 observations
pool.observe([uint32(3600)]) // Reverts - no data for 1 hour ago
// After 4 hours (8 observations)
pool.observe([uint32(3600)]) // Returns valid TWAP for last hourObjective: Launch bonding with predictable pricing while accumulating TWAP data
// In BondingCurve.sol
function getCurrentPrice() public view returns (uint256) {
if (isBootstrapPhase) {
return FIXED_BOOTSTRAP_PRICE; // e.g., 1 TAIKO = 1 TD
}
// ... TWAP logic
}Actions:
- Deploy TAIKO/TD pool via Aerodrome PoolFactory
- Seed initial liquidity using POL
- Enable bonding at fixed rate
- Pool accumulates observations every 30 minutes
Objective: Ensure sufficient price history exists
Required Aerodrome Integration:
// Read from Aerodrome Pool contract
interface IAerodromePool {
function observe(uint32[] calldata secondsAgos)
external view
returns (
int56[] memory tickCumulatives,
uint160[] memory secondsPerLiquidityCumulativeX128s
);
function slot0() external view returns (
uint160 sqrtPriceX96,
int24 tick,
uint16 observationIndex,
uint16 observationCardinality,
uint16 observationCardinalityNext,
uint8 feeProtocol,
bool unlocked
);
}
// In BondingCurve.sol
function canUseTWAP() public view returns (bool) {
(, , , uint16 cardinality, , , ) = aerodromePool.slot0();
return cardinality >= MIN_OBSERVATIONS; // e.g., 8
}Objective: Switch bonding curve to use market-based pricing
// Admin function to end bootstrap
function endBootstrapPhase() external onlyAdmin {
require(canUseTWAP(), "Insufficient TWAP data");
isBootstrapPhase = false;
emit BootstrapEnded(block.timestamp);
}
// Price calculation after bootstrap
function getCurrentPrice() public view returns (uint256) {
if (isBootstrapPhase) {
return FIXED_BOOTSTRAP_PRICE;
}
// Use Aerodrome's built-in TWAP
uint32 twapWindow = 14400; // 4 hours
(int56[] memory tickCumulatives, ) = aerodromePool.observe(
[twapWindow, 0] // Compare now vs 4 hours ago
);
int56 tickDelta = tickCumulatives[1] - tickCumulatives[0];
int24 arithmeticMeanTick = int24(tickDelta / int56(uint56(twapWindow)));
return OracleLibrary.getQuoteAtTick(
arithmeticMeanTick,
uint128(1e18), // 1 TD
address(tdToken),
address(taikoToken)
);
}If TD and TAIKO permanently diverge in value (e.g., TD becomes worth 5x TAIKO):
- Fixed bonding price becomes extremely attractive
- TWAP-based bonding might become unattractive even with discounts
- Protocol objectives compromised
contract BondingCurve {
uint256 public constant MAX_PRICE_RATIO = 300; // TD can't exceed 3x TAIKO
uint256 public constant MIN_PRICE_RATIO = 33; // TD can't go below 0.33x TAIKO
uint256 public priceAdjustmentFactor = 100; // 100 = no adjustment
function getCurrentPrice() public view returns (uint256) {
uint256 basePrice = isBootstrapPhase ?
FIXED_BOOTSTRAP_PRICE :
getTWAPPrice();
// Apply adjustment factor
uint256 adjustedPrice = (basePrice * priceAdjustmentFactor) / 100;
// Enforce circuit breakers
if (adjustedPrice > MAX_PRICE_RATIO * 1e16) {
return MAX_PRICE_RATIO * 1e16; // Cap at 3:1
}
if (adjustedPrice < MIN_PRICE_RATIO * 1e16) {
return MIN_PRICE_RATIO * 1e16; // Floor at 0.33:1
}
return adjustedPrice;
}
// Admin can adjust pricing in extreme scenarios
function setPriceAdjustment(uint256 newFactor) external onlyAdmin {
require(newFactor >= 50 && newFactor <= 200, "Adjustment too extreme");
priceAdjustmentFactor = newFactor;
emit PriceAdjustmentSet(newFactor);
}
}The protocol can actively manage liquidity depth using bonded TAIKO:
contract BondingCurve {
uint256 public polDeploymentRatio = 50; // Deploy 50% of TAIKO as POL
function deployPOL() external onlyAdmin {
uint256 taikoBalance = IERC20(TAIKO).balanceOf(address(this));
uint256 taikoForPOL = (taikoBalance * polDeploymentRatio) / 100;
// Mint matching TD at current market ratio
uint256 currentPrice = getCurrentPrice(); // TAIKO per TD
uint256 tdToMint = (taikoForPOL * 1e18) / currentPrice;
// Deploy to Aerodrome pool
TD.mint(address(this), tdToMint);
TD.approve(address(aerodromeRouter), tdToMint);
TAIKO.approve(address(aerodromeRouter), taikoForPOL);
aerodromeRouter.addLiquidity(
address(TAIKO),
address(TD),
stable, // false for volatile pair
taikoForPOL,
tdToMint,
0, // Accept any amounts
0,
address(this), // POL owned by protocol
block.timestamp
);
}
// Adjust how much TAIKO goes to POL vs treasury
function setPOLRatio(uint256 newRatio) external onlyAdmin {
require(newRatio <= 100, "Invalid ratio");
polDeploymentRatio = newRatio;
}
}- Start Fixed: Launch with predictable pricing (e.g., 1:1)
- Accumulate Data: Let Aerodrome pool build observation history
- Transition Carefully: Switch to TWAP once sufficient data exists
- Manage Risks: Implement circuit breakers and adjustment mechanisms
- Control Liquidity: Use POL deployment to manage market depth
This approach balances simplicity, security, and flexibility while leveraging Aerodrome's existing TWAP infrastructure.