- Published on
Balancer V3 Geomean Oracle
- Authors
- Name
- Beirao
- @0xBeirao
Github: BalancerV3-Price-Oracle
Production links: Coming...
Abstract
The Balancer V3 Geomean Oracle is a robust price oracle solution designed specifically for Balancer V3 pools. It provides reliable, manipulation-resistant price data for assets by calculating geometric mean prices over customizable time periods. Implemented as hook contracts for both Weighted and Stable pools, the oracle updates on every swap and features block-level granularity with built-in safeguards against price manipulation. The system requires zero maintenance after deployment and supports seamless integration with the broader DeFi ecosystem through an optional Chainlink-compatible adaptor.
Specs
- Compatibility: Works with Weighted pools (any number of assets, any weight) and Stable pools (any number of assets).
- Update Frequency: On every swap.
- Granularity: One price update per block maximum.
- Manipulation Resistance:
- Inherently protected against single-block manipulation via geometric mean calculation.
- Manipulation Safe Guard: Implements a maximum price change limit of 10% per block. This relies on the assumption that larger changes are likely manipulation or highly inefficient swaps.
- Common manipulation scenario: With control over 6 consecutive blocks on Ethereum mainnet, an attacker can only achieve a maximum price deviation of 0.46% over a 1-hour observation period, significantly limiting manipulation potential.
- Worst-Case Scenario: Even if an attacker controls 16 consecutive blocks on Ethereum mainnet (an extremely unlikely event), the maximum price deviation is calculated to be 3.10% over a 1-hour observation period.
- Flexibility:
- Fully customizable observation period (up to 30 days).
- Ability to create multiple Chainlink adaptors from a single oracle hook contract.
- Maintenance: Zero maintenance required after initial deployment.
- Price Transformation: Chainlink adaptors can be configured with a
_chainlinkAggregator
to convert the price output from the reference token denomination to another (e.g., from USDC to USD), enhancing interoperability. - Security Profile: Depending on the underlying blockchain's security and block times, this oracle can be considered safer than many second-class oracles.
The maths
Geometric mean
The geometric mean price provides a time-weighted average that naturally resists short-term price manipulation. It is calculated as:
Where:
- is the spot price during the -th interval.
- is the duration of the -th interval.
- is the total observation period.
For computational efficiency and to avoid precision issues with products of many numbers, the oracle uses the logarithmic form:
The term represents the accumulated log-price over the period .
Weighted Pool
In Balancer V3 Weighted Pools, the spot price between two tokens (A and B) is determined by their balances (, ) and their normalized weights (, ):
Where:
- is the balance of token A
- is the balance of token B
- is the weight of token A
- is the weight of token B
This price reflects the marginal rate of substitution between the assets in the pool and is used as the input for the geometric mean calculation.
Stable Pool
For Balancer V3 Stable Pools, the price calculation involves the pool's invariant and requires calculating partial derivatives with respect to the balances of the involved tokens ( for the token, for the reference token):
The specific form of the partial derivative for a token in a Stable Pool is:
Where:
- is the sum of all token balances (scaled).
- is the pool's invariant.
- is the amplification parameter (related to the
amp
factor). - is the product of all token balances (scaled).
- is the number of tokens in the pool.
This spot price is then used as the input for the geometric mean calculation.
BPT price calculation
The Balancer Pool Token (BPT) price represents the value of one BPT share in terms of the reference token. It is calculated by summing the value of all underlying tokens in the pool and dividing by the total supply of BPT:
Where:
- is the balance of token in the pool (in WAD)
- is the price of token in terms of the reference token
- is the total supply of BPT tokens
This calculation is performed in the _getLastPrice
function when the input token is the pool's BPT address. The function:
- Retrieves all pool tokens and their balances
- For each token, calculates its value in terms of the reference token
- Sums up all token values to get the total pool value
- Divides the total value by the BPT total supply to get the price per BPT
The resulting BPT price is then used as input for the geometric mean calculation, just like any other token price in the pool.
How does it work
Observation
The oracle stores price observations for each non-reference token within the pool. Each observation captures the state at a specific point in time:
struct Observation {
uint40 timestamp; // Timestamp of the observation
uint216 scaled18Price; // Spot price at timestamp (scaled to 18 decimals)
int256 accumulatedPrice; // Accumulated log-price (ln(price) * time) up to timestamp
}
Observations are recorded with a maximum frequency of one per block. If multiple swaps occur within the same block, only the price resulting from the last swap in that block is recorded or used to update the existing observation for that block. This prevents intra-block manipulation from affecting the oracle state more than once per block.
Price updates
Price updates are triggered by the onAfterSwap
hook, which is called by the Balancer Vault after each swap involving the pool. The update logic is as follows:
- Calculate Current Spot Price: Determine the spot price based on the pool's balances after the swap (using the formulas described in "The maths" section).
- Check Block Number: Compare the current
block.number
with thelastBlockNumber
stored for the token. - New Block:
- Retrieve the
scaled18Price
from the most recent stored observation. - Apply the
_manipulationSafeGuard
to the newly calculated spot price, comparing it against the retrieved last price. This clamps the change to +/- 10%. - Calculate the
accumulatedPrice
up to the currentblock.timestamp
based on the previous observation's state. - Store a new
Observation
struct with the currentblock.timestamp
, the manipulation-checkedscaled18Price
, and the calculatedaccumulatedPrice
. - Update the token's
lastBlockNumber
to the currentblock.number
.
- Retrieve the
- Same Block:
- Retrieve the
scaled18Price
from the second-to-last stored observation (the price before the first swap in this block). - Apply the
_manipulationSafeGuard
to the newly calculated spot price, comparing it against the retrieved second-to-last price. - Recalculate the
accumulatedPrice
for the current observation based on the state of the second-to-last observation and the currentblock.timestamp
. - Update the existing
Observation
for the current block with the new manipulation-checkedscaled18Price
and the recalculatedaccumulatedPrice
. (Timestamp remains unchanged).
- Retrieve the
The _manipulationSafeGuard
function limits price volatility between consecutive blocks:
function _manipulationSafeGuard(uint256 _currentPrice, uint256 _lastPrice) internal pure returns (uint256) {
uint256 minPrice_ = _lastPrice.mulWadDown(WAD - MAX_INTER_OBSERVATION_PRICE_CHANGE); // 90% of last price
uint256 maxPrice_ = _lastPrice.mulWadDown(WAD + MAX_INTER_OBSERVATION_PRICE_CHANGE); // 110% of last price
if (_currentPrice > maxPrice_) {
return maxPrice_;
} else if (_currentPrice < minPrice_) {
return minPrice_;
}
return _currentPrice; // Price change is within +/- 10%
}
Geometric mean price calculation
To get the geometric mean price over a specific _observationPeriod
, the getGeomeanPrice
function performs these steps:
- Identify Boundaries:
- Find the most recent observation (
observationsNow
). - Determine the target timestamp for the start of the period:
startPeriodTimestamp_ = block.timestamp - _observationPeriod
. - Use binary search (
_binarySearch
) to find the latest observation whose timestamp is less than or equal tostartPeriodTimestamp_
(observationsPeriodStart
). A hint (_hintLow
) can optimize this search.
- Find the most recent observation (
- Calculate Accumulated Prices:
- Calculate the interpolated
accumulatedPrice
at the exact currentblock.timestamp
based onobservationsNow
. - Calculate the interpolated
accumulatedPrice
at the exactstartPeriodTimestamp_
based onobservationsPeriodStart
.
- Calculate the interpolated
- Compute Average Log-Price: Calculate the difference between the two accumulated prices and divide by the
_observationPeriod
:averageLogPrice = (accumulatedNow - accumulatedStart) / _observationPeriod
- Exponentiate: Calculate the final geometric mean price by taking the exponent of the average log-price:
geomeanPrice = exp(averageLogPrice)
(usingwadExp
for fixed-point math). - Unscale: Adjust the 18-decimal result to match the
referenceToken
's decimals using_unscalePrice
.
function getGeomeanPrice(address _token, uint256 _observationPeriod, uint256 _hintLow) public view returns (uint256) {
// ... find observationsNow and observationsPeriodStart ...
int256 averageLogPrice_ = _calculateAccumulatedPrice(observationsNow, block.timestamp)
- _calculateAccumulatedPrice(observationsPeriodStart, startPeriodTimestamp_);
return _unscalePrice(uint256(wadExp(averageLogPrice_ / int256(_observationPeriod))));
}
function _calculateAccumulatedPrice(Observation storage observation, uint256 _timestamp) internal view returns (int256) {
// Interpolates accumulated price to _timestamp based on the observation's state
return observation.accumulatedPrice + (int256(_timestamp - observation.timestamp)) * wadLn(int216(observation.scaled18Price));
}
Chainlink adaptor
The ChainlinkPriceFeedAdaptor
contract provides an interface compatible with Chainlink's AggregatorV2V3Interface
. This allows seamless integration with protocols and systems that expect Chainlink price feeds.
Key Features:
- Implements standard Chainlink methods like
latestAnswer()
,latestTimestamp()
,latestRoundData()
,decimals()
,description()
, etc. - Fetches the geometric mean price from the underlying
GeomeanOracleHookContract
for the specifiedobservationPeriod
. - Optional Price Conversion: If a
_chainlinkAggregator
address (pointing to an existing Chainlink feed, e.g., USDC/USD) is provided during adaptor creation, the adaptor will automatically convert the oracle's output price.- Example: If the oracle provides ETH/USDC and the
_chainlinkAggregator
is USDC/USD, the adaptor'slatestAnswer()
will return the price in ETH/USD. - Conversion logic:
finalPrice = (oraclePrice_ETH_USDC * chainlinkPrice_USDC_USD) / 10^referenceTokenDecimals
- Example: If the oracle provides ETH/USDC and the
- Simplified Round IDs: Since the oracle updates continuously, the adaptor often uses
block.timestamp
as a proxy for round IDs where applicable, unless a real_chainlinkAggregator
is used, in which case it delegates round information.
Creation: Adaptors are deployed via the hook contract:
function createChainlinkPriceFeedAdaptor(
address _token, // Token to get price for
uint256 _observationPeriod, // TWAP period for this adaptor
address _chainlinkAggregator // Optional: Address of Chainlink feed for price conversion (e.g., USDC/USD)
) external returns (address); // Returns address of the new adaptor
This allows creating multiple adaptors with different observation periods or target denominations from the same core oracle hook.
Conclusion
The Balancer V3 Geomean Oracle offers a highly robust, flexible, and low-maintenance on-chain price feed solution. Its use of geometric means and per-block manipulation safeguards provides strong resistance against common oracle attacks. Compatibility with both Weighted and Stable pools, coupled with the Chainlink adaptor for enhanced interoperability, makes it a versatile tool for DeFi applications built on Balancer V3. The zero-maintenance design further reduces operational overhead for developers. Its security properties, particularly the quantifiable resistance to multi-block manipulation, position it as a potentially safer alternative to simpler TWAP oracles or less established price sources, especially on blockchains with reliable block production.