From 473c6f39cb859f2f81049307cef9105809e11ef2 Mon Sep 17 00:00:00 2001 From: TRiLeZ Date: Sun, 25 Dec 2022 09:54:55 +0000 Subject: [PATCH 01/32] Add granular observation support --- contracts/interfaces/IPeriodic.sol | 4 + contracts/oracles/AggregatedOracle.sol | 3 +- .../oracles/PeriodicAccumulationOracle.sol | 219 +++++++++--------- contracts/oracles/PeriodicOracle.sol | 12 +- .../test/oracles/AggregatedOracleStub.sol | 2 + .../PeriodicAccumulationOracleStub.sol | 17 +- scripts/consult-aggregate.js | 23 +- scripts/consult-curve-steth.js | 15 +- scripts/consult-curve-tricrypto2.js | 15 +- scripts/consult-uniswap-v2.js | 53 +++-- scripts/consult-uniswap-v3.js | 7 +- 11 files changed, 214 insertions(+), 156 deletions(-) diff --git a/contracts/interfaces/IPeriodic.sol b/contracts/interfaces/IPeriodic.sol index ed25a26..15b3f73 100644 --- a/contracts/interfaces/IPeriodic.sol +++ b/contracts/interfaces/IPeriodic.sol @@ -10,4 +10,8 @@ interface IPeriodic { /// @notice Gets the period, in seconds. /// @return periodSeconds The period, in seconds. function period() external view returns (uint256 periodSeconds); + + // @notice Gets the number of observations made every period. + // @return granularity The number of observations made every period. + function granularity() external view returns (uint256 granularity); } diff --git a/contracts/oracles/AggregatedOracle.sol b/contracts/oracles/AggregatedOracle.sol index 42bbc75..6cd3e6e 100644 --- a/contracts/oracles/AggregatedOracle.sol +++ b/contracts/oracles/AggregatedOracle.sol @@ -72,10 +72,11 @@ contract AggregatedOracle is IAggregatedOracle, PeriodicOracle, ExplicitQuotatio address[] memory oracles_, TokenSpecificOracle[] memory tokenSpecificOracles_, uint256 period_, + uint256 granularity_, uint256 minimumTokenLiquidityValue_, uint256 minimumQuoteTokenLiquidity_ ) - PeriodicOracle(quoteTokenAddress_, period_) + PeriodicOracle(quoteTokenAddress_, period_, granularity_) ExplicitQuotationMetadata(quoteTokenName_, quoteTokenAddress_, quoteTokenSymbol_, quoteTokenDecimals_) { require(oracles_.length > 0 || tokenSpecificOracles_.length > 0, "AggregatedOracle: MISSING_ORACLES"); diff --git a/contracts/oracles/PeriodicAccumulationOracle.sol b/contracts/oracles/PeriodicAccumulationOracle.sol index ef1caec..f93db7e 100644 --- a/contracts/oracles/PeriodicAccumulationOracle.sol +++ b/contracts/oracles/PeriodicAccumulationOracle.sol @@ -16,18 +16,27 @@ import "../libraries/ObservationLibrary.sol"; contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, IHasPriceAccumulator { using SafeCast for uint256; + struct BufferMetadata { + uint8 start; + uint8 end; + uint8 size; + } + address public immutable override liquidityAccumulator; address public immutable override priceAccumulator; - mapping(address => AccumulationLibrary.PriceAccumulator) public priceAccumulations; - mapping(address => AccumulationLibrary.LiquidityAccumulator) public liquidityAccumulations; + mapping(address => BufferMetadata) public accumulationBufferMetadata; + + mapping(address => AccumulationLibrary.PriceAccumulator[]) public priceAccumulationBuffers; + mapping(address => AccumulationLibrary.LiquidityAccumulator[]) public liquidityAccumulationBuffers; constructor( address liquidityAccumulator_, address priceAccumulator_, address quoteToken_, - uint256 period_ - ) PeriodicOracle(quoteToken_, period_) { + uint256 period_, + uint256 granularity_ + ) PeriodicOracle(quoteToken_, period_, granularity_) { liquidityAccumulator = liquidityAccumulator_; priceAccumulator = priceAccumulator_; } @@ -53,10 +62,15 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, function lastUpdateTime(bytes memory data) public view virtual override returns (uint256) { address token = abi.decode(data, (address)); + BufferMetadata storage meta = accumulationBufferMetadata[token]; + + // Return 0 if there are no observations (never updated) + if (meta.size == 0) return 0; + // Note: We ignore the last observation timestamp because it always updates when the accumulation timestamps // update. - uint256 lastPriceAccumulationTimestamp = priceAccumulations[token].timestamp; - uint256 lastLiquidityAccumulationTimestamp = liquidityAccumulations[token].timestamp; + uint256 lastPriceAccumulationTimestamp = priceAccumulationBuffers[token][meta.end].timestamp; + uint256 lastLiquidityAccumulationTimestamp = liquidityAccumulationBuffers[token][meta.end].timestamp; return Math.max(lastPriceAccumulationTimestamp, lastLiquidityAccumulationTimestamp); } @@ -97,6 +111,86 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, return (period * 2) + 5 minutes; } + function initializeBuffers(address token) internal virtual { + require( + priceAccumulationBuffers[token].length == 0 && liquidityAccumulationBuffers[token].length == 0, + "PeriodicAccumulationOracle: ALREADY_INITIALIZED" + ); + + BufferMetadata storage meta = accumulationBufferMetadata[token]; + + // Initialize the buffers + AccumulationLibrary.PriceAccumulator[] storage priceAccumulationBuffer = priceAccumulationBuffers[token]; + AccumulationLibrary.LiquidityAccumulator[] storage liquidityAccumulationBuffer = liquidityAccumulationBuffers[ + token + ]; + + for (uint256 i = 0; i < granularity; ++i) { + priceAccumulationBuffer.push(); + liquidityAccumulationBuffer.push(); + } + + // Initialize the metadata + meta.start = 0; + meta.end = 0; + meta.size = 0; + } + + function push( + address token, + AccumulationLibrary.PriceAccumulator memory priceAccumulation, + AccumulationLibrary.LiquidityAccumulator memory liquidityAccumulation + ) internal virtual { + BufferMetadata storage meta = accumulationBufferMetadata[token]; + + if (meta.size == 0) { + // Initialize the buffers + initializeBuffers(token); + } else { + // We have multiple accumulations now + + uint256 firstAccumulationTime = priceAccumulationBuffers[token][meta.start].timestamp; + uint256 periodTimeElapsed = priceAccumulation.timestamp - firstAccumulationTime; + + if ( + periodTimeElapsed <= period + updateDelayTolerance() && + meta.size == granularity && + periodTimeElapsed >= period + ) { + ObservationLibrary.Observation storage observation = observations[token]; + + observation.price = IPriceAccumulator(priceAccumulator).calculatePrice( + priceAccumulationBuffers[token][meta.start], + priceAccumulation + ); + (observation.tokenLiquidity, observation.quoteTokenLiquidity) = ILiquidityAccumulator( + liquidityAccumulator + ).calculateLiquidity(liquidityAccumulationBuffers[token][meta.start], liquidityAccumulation); + observation.timestamp = block.timestamp.toUint32(); + + emit Updated( + token, + observation.price, + observation.tokenLiquidity, + observation.quoteTokenLiquidity, + observation.timestamp + ); + } + + meta.end = (meta.end + 1) % uint8(granularity); + } + + priceAccumulationBuffers[token][meta.end] = priceAccumulation; + liquidityAccumulationBuffers[token][meta.end] = liquidityAccumulation; + + if (meta.size < granularity) { + meta.size++; + } else { + // start was just overwritten + meta.start = (meta.start + 1) % uint8(granularity); + } + } + function performUpdate(bytes memory data) internal virtual override returns (bool) { // We require that the accumulators have a heartbeat update that is within the grace period (i.e. they are // up-to-date). @@ -120,114 +214,19 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, address token = abi.decode(data, (address)); - ObservationLibrary.Observation memory observation = observations[token]; - - bool updatedObservation; - bool missingPrice; - bool anythingUpdated; - bool observationExpired; - - /* - * 1. Update price - */ - { - AccumulationLibrary.PriceAccumulator memory freshAccumulation = IPriceAccumulator(priceAccumulator) - .getCurrentAccumulation(token); - - AccumulationLibrary.PriceAccumulator storage lastAccumulation = priceAccumulations[token]; - - uint256 lastAccumulationTime = priceAccumulations[token].timestamp; - - if (freshAccumulation.timestamp > lastAccumulationTime) { - // Accumulator updated, so we update our observation - - if (lastAccumulationTime != 0) { - if (freshAccumulation.timestamp - lastAccumulationTime > period + updateDelayTolerance()) { - // The accumulator is too far behind, so we discard the last accumulation and wait for the - // next update. - observationExpired = true; - } else { - // We have two accumulations -> calculate price from them - observation.price = IPriceAccumulator(priceAccumulator).calculatePrice( - lastAccumulation, - freshAccumulation - ); - - updatedObservation = true; - } - } else { - // This is our first update (or rather, we have our first accumulation) - // Record that we're missing the price to later prevent the observation timestamp and event - // from being emitted (no timestamp = missing observation and consult reverts). - missingPrice = true; - } - - lastAccumulation.cumulativePrice = freshAccumulation.cumulativePrice; - lastAccumulation.timestamp = freshAccumulation.timestamp; - - anythingUpdated = true; - } - } - - /* - * 2. Update liquidity - */ - { - AccumulationLibrary.LiquidityAccumulator memory freshAccumulation = ILiquidityAccumulator( - liquidityAccumulator - ).getCurrentAccumulation(token); - - AccumulationLibrary.LiquidityAccumulator storage lastAccumulation = liquidityAccumulations[token]; - - uint256 lastAccumulationTime = liquidityAccumulations[token].timestamp; - - if (freshAccumulation.timestamp > lastAccumulationTime) { - // Accumulator updated, so we update our observation - - if (lastAccumulationTime != 0) { - if (freshAccumulation.timestamp - lastAccumulationTime > period + updateDelayTolerance()) { - // The accumulator is too far behind, so we discard the last accumulation and wait for the - // next update. - observationExpired = true; - } else { - // We have two accumulations -> calculate liquidity from them - (observation.tokenLiquidity, observation.quoteTokenLiquidity) = ILiquidityAccumulator( - liquidityAccumulator - ).calculateLiquidity(lastAccumulation, freshAccumulation); - - updatedObservation = true; - } - } + AccumulationLibrary.PriceAccumulator memory priceAccumulation = IPriceAccumulator(priceAccumulator) + .getCurrentAccumulation(token); + AccumulationLibrary.LiquidityAccumulator memory liquidityAccumulation = ILiquidityAccumulator( + liquidityAccumulator + ).getCurrentAccumulation(token); - lastAccumulation.cumulativeTokenLiquidity = freshAccumulation.cumulativeTokenLiquidity; - lastAccumulation.cumulativeQuoteTokenLiquidity = freshAccumulation.cumulativeQuoteTokenLiquidity; - lastAccumulation.timestamp = freshAccumulation.timestamp; + if (priceAccumulation.timestamp != 0 && liquidityAccumulation.timestamp != 0) { + push(token, priceAccumulation, liquidityAccumulation); - anythingUpdated = true; - } - } - - // We only want to update the timestamp and emit an event when both the observation has been updated and we - // have a price (even if the accumulator calculates a price of 0). - // Note: We rely on consult reverting when the observation timestamp is 0. - if (updatedObservation && !missingPrice && !observationExpired) { - ObservationLibrary.Observation storage storageObservation = observations[token]; - - storageObservation.price = observation.price; - storageObservation.tokenLiquidity = observation.tokenLiquidity; - storageObservation.quoteTokenLiquidity = observation.quoteTokenLiquidity; - storageObservation.timestamp = block.timestamp.toUint32(); - - emit Updated( - token, - observation.price, - observation.tokenLiquidity, - observation.quoteTokenLiquidity, - block.timestamp - ); + return true; } - return anythingUpdated; + return false; } /// @inheritdoc AbstractOracle diff --git a/contracts/oracles/PeriodicOracle.sol b/contracts/oracles/PeriodicOracle.sol index 25ff94b..3572376 100644 --- a/contracts/oracles/PeriodicOracle.sol +++ b/contracts/oracles/PeriodicOracle.sol @@ -7,11 +7,19 @@ import "./AbstractOracle.sol"; abstract contract PeriodicOracle is IPeriodic, AbstractOracle { uint256 public immutable override period; + uint256 public immutable override granularity; - constructor(address quoteToken_, uint256 period_) AbstractOracle(quoteToken_) { + uint internal immutable _updateEvery; + + constructor(address quoteToken_, uint256 period_, uint256 granularity_) AbstractOracle(quoteToken_) { require(period_ > 0, "PeriodicOracle: INVALID_PERIOD"); + require(granularity_ > 0, "PeriodicOracle: INVALID_GRANULARITY"); + require(period_ % granularity_ == 0, "PeriodicOracle: INVALID_PERIOD_GRANULARITY"); period = period_; + granularity = granularity_; + + _updateEvery = period_ / granularity_; } /// @inheritdoc AbstractOracle @@ -23,7 +31,7 @@ abstract contract PeriodicOracle is IPeriodic, AbstractOracle { /// @inheritdoc AbstractOracle function needsUpdate(bytes memory data) public view virtual override returns (bool) { - return timeSinceLastUpdate(data) >= period; + return timeSinceLastUpdate(data) >= _updateEvery; } /// @inheritdoc AbstractOracle diff --git a/contracts/test/oracles/AggregatedOracleStub.sol b/contracts/test/oracles/AggregatedOracleStub.sol index c4fe3db..314b207 100644 --- a/contracts/test/oracles/AggregatedOracleStub.sol +++ b/contracts/test/oracles/AggregatedOracleStub.sol @@ -32,6 +32,7 @@ contract AggregatedOracleStub is AggregatedOracle { address[] memory oracles_, AggregatedOracle.TokenSpecificOracle[] memory _tokenSpecificOracles, uint256 period_, + uint256 granularity_, uint256 minimumTokenLiquidityValue_, uint256 minimumQuoteTokenLiquidity_ ) @@ -44,6 +45,7 @@ contract AggregatedOracleStub is AggregatedOracle { oracles_, _tokenSpecificOracles, period_, + granularity_, minimumTokenLiquidityValue_, minimumQuoteTokenLiquidity_ ) diff --git a/contracts/test/oracles/PeriodicAccumulationOracleStub.sol b/contracts/test/oracles/PeriodicAccumulationOracleStub.sol index 38a8204..746d481 100644 --- a/contracts/test/oracles/PeriodicAccumulationOracleStub.sol +++ b/contracts/test/oracles/PeriodicAccumulationOracleStub.sol @@ -15,8 +15,9 @@ contract PeriodicAccumulationOracleStub is PeriodicAccumulationOracle { address liquidityAccumulator_, address priceAccumulator_, address quoteToken_, - uint256 period_ - ) PeriodicAccumulationOracle(liquidityAccumulator_, priceAccumulator_, quoteToken_, period_) {} + uint256 period_, + uint256 granularity_ + ) PeriodicAccumulationOracle(liquidityAccumulator_, priceAccumulator_, quoteToken_, period_, granularity_) {} function stubSetObservation( address token, @@ -40,7 +41,7 @@ contract PeriodicAccumulationOracleStub is PeriodicAccumulationOracle { uint112 cumulativeQuoteTokenLiquidity, uint32 timestamp ) public { - AccumulationLibrary.LiquidityAccumulator storage liquidityAccumulation = liquidityAccumulations[token]; + /*AccumulationLibrary.LiquidityAccumulator storage liquidityAccumulation = liquidityAccumulations[token]; AccumulationLibrary.PriceAccumulator storage priceAccumulation = priceAccumulations[token]; priceAccumulation.cumulativePrice = cumulativePrice; @@ -48,14 +49,14 @@ contract PeriodicAccumulationOracleStub is PeriodicAccumulationOracle { liquidityAccumulation.cumulativeTokenLiquidity = cumulativeTokenLiquidity; liquidityAccumulation.cumulativeQuoteTokenLiquidity = cumulativeQuoteTokenLiquidity; - liquidityAccumulation.timestamp = timestamp; + liquidityAccumulation.timestamp = timestamp;*/ } function stubSetPriceAccumulation(address token, uint112 cumulativePrice, uint32 timestamp) public { - AccumulationLibrary.PriceAccumulator storage priceAccumulation = priceAccumulations[token]; + /*AccumulationLibrary.PriceAccumulator storage priceAccumulation = priceAccumulations[token]; priceAccumulation.cumulativePrice = cumulativePrice; - priceAccumulation.timestamp = timestamp; + priceAccumulation.timestamp = timestamp;*/ } function stubSetLiquidityAccumulation( @@ -64,11 +65,11 @@ contract PeriodicAccumulationOracleStub is PeriodicAccumulationOracle { uint112 cumulativeQuoteTokenLiquidity, uint32 timestamp ) public { - AccumulationLibrary.LiquidityAccumulator storage liquidityAccumulation = liquidityAccumulations[token]; + /*AccumulationLibrary.LiquidityAccumulator storage liquidityAccumulation = liquidityAccumulations[token]; liquidityAccumulation.cumulativeTokenLiquidity = cumulativeTokenLiquidity; liquidityAccumulation.cumulativeQuoteTokenLiquidity = cumulativeQuoteTokenLiquidity; - liquidityAccumulation.timestamp = timestamp; + liquidityAccumulation.timestamp = timestamp;*/ } function overrideNeedsUpdate(bool overridden, bool needsUpdate_) public { diff --git a/scripts/consult-aggregate.js b/scripts/consult-aggregate.js index 8ff3f2c..20b118b 100644 --- a/scripts/consult-aggregate.js +++ b/scripts/consult-aggregate.js @@ -39,7 +39,7 @@ async function createContract(name, ...deploymentArgs) { return contract; } -async function createUniswapV2Oracle(factory, initCodeHash, quoteToken, liquidityDecimals, period) { +async function createUniswapV2Oracle(factory, initCodeHash, quoteToken, liquidityDecimals, period, granularity) { const updateTheshold = 2000000; // 2% change -> update const minUpdateDelay = 5; // At least 5 seconds between every update const maxUpdateDelay = 10; // At most (optimistically) 60 seconds between every update @@ -70,7 +70,8 @@ async function createUniswapV2Oracle(factory, initCodeHash, quoteToken, liquidit liquidityAccumulator.address, priceAccumulator.address, quoteToken, - period + period, + granularity ); return { @@ -80,7 +81,7 @@ async function createUniswapV2Oracle(factory, initCodeHash, quoteToken, liquidit }; } -async function createUniswapV3Oracle(factory, initCodeHash, quoteToken, liquidityDecimals, period) { +async function createUniswapV3Oracle(factory, initCodeHash, quoteToken, liquidityDecimals, period, granularity) { const poolFees = [/*500, */ 3000 /*, 10000*/]; const updateTheshold = 2000000; // 2% change -> update @@ -115,7 +116,8 @@ async function createUniswapV3Oracle(factory, initCodeHash, quoteToken, liquidit liquidityAccumulator.address, priceAccumulator.address, quoteToken, - period + period, + granularity ); return { @@ -132,6 +134,7 @@ async function createAggregatedOracle( quoteTokenDecimals, liquidityDecimals, period, + granularity, oracles, tokenSpecificOracles ) { @@ -145,6 +148,7 @@ async function createAggregatedOracle( oracles, tokenSpecificOracles, period, + granularity, 1, 10 ** quoteTokenDecimals // minimum is one whole token ); @@ -156,6 +160,7 @@ async function main() { const underlyingPeriodSeconds = 10; const periodSeconds = 10; + const granularity = 1; const liquidityDecimals = 4; @@ -164,21 +169,24 @@ async function main() { uniswapV2InitCodeHash, quoteToken, liquidityDecimals, - underlyingPeriodSeconds + underlyingPeriodSeconds, + granularity ); const sushiswap = await createUniswapV2Oracle( sushiswapFactoryAddress, sushiswapInitCodeHash, quoteToken, liquidityDecimals, - underlyingPeriodSeconds + underlyingPeriodSeconds, + granularity ); const uniswapV3 = await createUniswapV3Oracle( uniswapV3FactoryAddress, uniswapV3InitCodeHash, quoteToken, liquidityDecimals, - underlyingPeriodSeconds + underlyingPeriodSeconds, + granularity ); const oracles = [uniswapV2.oracle.address, sushiswap.oracle.address, uniswapV3.oracle.address]; @@ -192,6 +200,7 @@ async function main() { 6, liquidityDecimals, periodSeconds, + granularity, oracles, tokenSpecificOracles ); diff --git a/scripts/consult-curve-steth.js b/scripts/consult-curve-steth.js index 81e0aa0..df04189 100644 --- a/scripts/consult-curve-steth.js +++ b/scripts/consult-curve-steth.js @@ -27,7 +27,7 @@ async function createContract(name, ...deploymentArgs) { return contract; } -async function createCurveOracle(pool, poolQuoteToken, ourQuoteToken, period, liquidityDecimals) { +async function createCurveOracle(pool, poolQuoteToken, ourQuoteToken, period, granularity, liquidityDecimals) { const updateTheshold = 2000000; // 2% change -> update const minUpdateDelay = 5; // At least 5 seconds between every update const maxUpdateDelay = 60; // At most (optimistically) 60 seconds between every update @@ -60,7 +60,8 @@ async function createCurveOracle(pool, poolQuoteToken, ourQuoteToken, period, li liquidityAccumulator.address, priceAccumulator.address, ourQuoteToken, - period + period, + granularity ); return { @@ -79,10 +80,18 @@ async function main() { const ourQuoteToken = wethAddress; const period = 10; // 10 seconds + const granularity = 1; const liquidityDecimals = 4; - const curve = await createCurveOracle(poolAddress, poolQuoteToken, ourQuoteToken, period, liquidityDecimals); + const curve = await createCurveOracle( + poolAddress, + poolQuoteToken, + ourQuoteToken, + period, + granularity, + liquidityDecimals + ); const tokenContract = await ethers.getContractAt("ERC20", token); const quoteTokenContract = await ethers.getContractAt("ERC20", ourQuoteToken); diff --git a/scripts/consult-curve-tricrypto2.js b/scripts/consult-curve-tricrypto2.js index fa89808..2d14287 100644 --- a/scripts/consult-curve-tricrypto2.js +++ b/scripts/consult-curve-tricrypto2.js @@ -28,7 +28,7 @@ async function createContract(name, ...deploymentArgs) { return contract; } -async function createCurveOracle(pool, poolQuoteToken, ourQuoteToken, period, liquidityDecimals) { +async function createCurveOracle(pool, poolQuoteToken, ourQuoteToken, period, granularity, liquidityDecimals) { const updateTheshold = 2000000; // 2% change -> update const minUpdateDelay = 5; // At least 5 seconds between every update const maxUpdateDelay = 60; // At most (optimistically) 60 seconds between every update @@ -61,7 +61,8 @@ async function createCurveOracle(pool, poolQuoteToken, ourQuoteToken, period, li liquidityAccumulator.address, priceAccumulator.address, ourQuoteToken, - period + period, + granularity ); return { @@ -80,10 +81,18 @@ async function main() { const ourQuoteToken = usdtAddress; const period = 10; // 10 seconds + const granularity = 1; const liquidityDecimals = 4; - const curve = await createCurveOracle(poolAddress, poolQuoteToken, ourQuoteToken, period, liquidityDecimals); + const curve = await createCurveOracle( + poolAddress, + poolQuoteToken, + ourQuoteToken, + period, + granularity, + liquidityDecimals + ); const tokenContract = await ethers.getContractAt("ERC20", token); const quoteTokenContract = await ethers.getContractAt("ERC20", ourQuoteToken); diff --git a/scripts/consult-uniswap-v2.js b/scripts/consult-uniswap-v2.js index d6063ab..1f07623 100644 --- a/scripts/consult-uniswap-v2.js +++ b/scripts/consult-uniswap-v2.js @@ -32,7 +32,7 @@ async function createContract(name, ...deploymentArgs) { return contract; } -async function createUniswapV2Oracle(factory, initCodeHash, quoteToken, period, liquidityDecimals) { +async function createUniswapV2Oracle(factory, initCodeHash, quoteToken, period, granularity, liquidityDecimals) { const updateTheshold = 2000000; // 2% change -> update const minUpdateDelay = 5; // At least 5 seconds between every update const maxUpdateDelay = 60; // At most (optimistically) 60 seconds between every update @@ -63,7 +63,8 @@ async function createUniswapV2Oracle(factory, initCodeHash, quoteToken, period, liquidityAccumulator.address, priceAccumulator.address, quoteToken, - period + period, + granularity ); return { @@ -81,10 +82,18 @@ async function main() { const quoteToken = usdcAddress; const period = 10; // 10 seconds + const granularity = 1; const liquidityDecimals = 4; - const uniswapV2 = await createUniswapV2Oracle(factoryAddress, initCodeHash, quoteToken, period, liquidityDecimals); + const uniswapV2 = await createUniswapV2Oracle( + factoryAddress, + initCodeHash, + quoteToken, + period, + granularity, + liquidityDecimals + ); const tokenContract = await ethers.getContractAt("ERC20", token); const quoteTokenContract = await ethers.getContractAt("ERC20", quoteToken); @@ -149,28 +158,32 @@ async function main() { ); } - const consultation = await uniswapV2.oracle["consult(address)"](token); + try { + const consultation = await uniswapV2.oracle["consult(address)"](token); - const priceStr = ethers.utils.commify(ethers.utils.formatUnits(consultation["price"], quoteTokenDecimals)); + const priceStr = ethers.utils.commify( + ethers.utils.formatUnits(consultation["price"], quoteTokenDecimals) + ); - console.log( - "\u001b[" + 32 + "m" + "Price(%s) = %s %s" + "\u001b[0m", - tokenSymbol, - priceStr, - quoteTokenSymbol - ); + console.log( + "\u001b[" + 32 + "m" + "Price(%s) = %s %s" + "\u001b[0m", + tokenSymbol, + priceStr, + quoteTokenSymbol + ); - const tokenLiquidityStr = ethers.utils.commify(consultation["tokenLiquidity"]); + const tokenLiquidityStr = ethers.utils.commify(consultation["tokenLiquidity"]); - const quoteTokenLiquidityStr = ethers.utils.commify(consultation["quoteTokenLiquidity"]); + const quoteTokenLiquidityStr = ethers.utils.commify(consultation["quoteTokenLiquidity"]); - console.log( - "\u001b[" + 31 + "m" + "Liquidity(%s) = %s, Liquidity(%s) = %s" + "\u001b[0m", - tokenSymbol, - tokenLiquidityStr, - quoteTokenSymbol, - quoteTokenLiquidityStr - ); + console.log( + "\u001b[" + 31 + "m" + "Liquidity(%s) = %s, Liquidity(%s) = %s" + "\u001b[0m", + tokenSymbol, + tokenLiquidityStr, + quoteTokenSymbol, + quoteTokenLiquidityStr + ); + } catch (e) {} } catch (e) { console.log(e); } diff --git a/scripts/consult-uniswap-v3.js b/scripts/consult-uniswap-v3.js index f247f89..4f9f003 100644 --- a/scripts/consult-uniswap-v3.js +++ b/scripts/consult-uniswap-v3.js @@ -27,7 +27,7 @@ async function createContract(name, ...deploymentArgs) { return contract; } -async function createUniswapV3Oracle(factory, initCodeHash, quoteToken, period, liquidityDecimals) { +async function createUniswapV3Oracle(factory, initCodeHash, quoteToken, period, granularity, liquidityDecimals) { const poolFees = [/*500, */ 3000 /*, 10000*/]; const updateTheshold = 2000000; // 2% change -> update @@ -62,7 +62,8 @@ async function createUniswapV3Oracle(factory, initCodeHash, quoteToken, period, liquidityAccumulator.address, priceAccumulator.address, quoteToken, - period + period, + granularity ); return { @@ -77,6 +78,7 @@ async function main() { const quoteToken = usdcAddress; const underlyingPeriodSeconds = 5; + const granularity = 1; const liquidityDecimals = 4; @@ -85,6 +87,7 @@ async function main() { uniswapV3InitCodeHash, quoteToken, underlyingPeriodSeconds, + granularity, liquidityDecimals ); From 8d18203b1cc1adca10811a08aea87991dab12fb9 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Wed, 4 Jan 2023 14:50:56 -0800 Subject: [PATCH 02/32] Make updateDelayTolerance use granularity --- contracts/oracles/PeriodicAccumulationOracle.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/oracles/PeriodicAccumulationOracle.sol b/contracts/oracles/PeriodicAccumulationOracle.sol index f93db7e..c7d5d5d 100644 --- a/contracts/oracles/PeriodicAccumulationOracle.sol +++ b/contracts/oracles/PeriodicAccumulationOracle.sol @@ -108,7 +108,7 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, // We tolerate two missed periods plus 5 minutes (to allow for some time to update the oracles). // We trade off some freshness for greater reliability. Using too low of a tolerance reduces the cost of DoS // attacks. - return (period * 2) + 5 minutes; + return (_updateEvery * 2) + 5 minutes; } function initializeBuffers(address token) internal virtual { From c88fa9ed66e698ed353eae5fdba31336d31ecfe0 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Mon, 9 Jan 2023 15:36:10 -0800 Subject: [PATCH 03/32] Add AbstractOracle#getLatestObservation --- contracts/oracles/AbstractOracle.sol | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/contracts/oracles/AbstractOracle.sol b/contracts/oracles/AbstractOracle.sol index f408bc7..86cfd8e 100644 --- a/contracts/oracles/AbstractOracle.sol +++ b/contracts/oracles/AbstractOracle.sol @@ -25,12 +25,18 @@ abstract contract AbstractOracle is IERC165, IOracle, SimpleQuotationMetadata { /// @inheritdoc IUpdateable function canUpdate(bytes memory data) public view virtual override returns (bool); + function getLatestObservation( + address token + ) public view virtual returns (ObservationLibrary.Observation memory observation) { + return observations[token]; + } + /// @param data The encoded address of the token for which the update relates to. /// @inheritdoc IUpdateable function lastUpdateTime(bytes memory data) public view virtual override returns (uint256) { address token = abi.decode(data, (address)); - return observations[token].timestamp; + return getLatestObservation(token).timestamp; } /// @param data The encoded address of the token for which the update relates to. @@ -42,7 +48,7 @@ abstract contract AbstractOracle is IERC165, IOracle, SimpleQuotationMetadata { function consultPrice(address token) public view virtual override returns (uint112 price) { if (token == quoteTokenAddress()) return uint112(10 ** quoteTokenDecimals()); - ObservationLibrary.Observation storage observation = observations[token]; + ObservationLibrary.Observation memory observation = getLatestObservation(token); require(observation.timestamp != 0, "AbstractOracle: MISSING_OBSERVATION"); @@ -59,7 +65,7 @@ abstract contract AbstractOracle is IERC165, IOracle, SimpleQuotationMetadata { return price; } - ObservationLibrary.Observation storage observation = observations[token]; + ObservationLibrary.Observation memory observation = getLatestObservation(token); require(observation.timestamp != 0, "AbstractOracle: MISSING_OBSERVATION"); require(block.timestamp <= observation.timestamp + maxAge, "AbstractOracle: RATE_TOO_OLD"); @@ -73,7 +79,7 @@ abstract contract AbstractOracle is IERC165, IOracle, SimpleQuotationMetadata { ) public view virtual override returns (uint112 tokenLiquidity, uint112 quoteTokenLiquidity) { if (token == quoteTokenAddress()) return (0, 0); - ObservationLibrary.Observation storage observation = observations[token]; + ObservationLibrary.Observation memory observation = getLatestObservation(token); require(observation.timestamp != 0, "AbstractOracle: MISSING_OBSERVATION"); @@ -94,7 +100,7 @@ abstract contract AbstractOracle is IERC165, IOracle, SimpleQuotationMetadata { return (tokenLiquidity, quoteTokenLiquidity); } - ObservationLibrary.Observation storage observation = observations[token]; + ObservationLibrary.Observation memory observation = getLatestObservation(token); require(observation.timestamp != 0, "AbstractOracle: MISSING_OBSERVATION"); require(block.timestamp <= observation.timestamp + maxAge, "AbstractOracle: RATE_TOO_OLD"); @@ -109,7 +115,7 @@ abstract contract AbstractOracle is IERC165, IOracle, SimpleQuotationMetadata { ) public view virtual override returns (uint112 price, uint112 tokenLiquidity, uint112 quoteTokenLiquidity) { if (token == quoteTokenAddress()) return (uint112(10 ** quoteTokenDecimals()), 0, 0); - ObservationLibrary.Observation storage observation = observations[token]; + ObservationLibrary.Observation memory observation = getLatestObservation(token); require(observation.timestamp != 0, "AbstractOracle: MISSING_OBSERVATION"); @@ -127,7 +133,7 @@ abstract contract AbstractOracle is IERC165, IOracle, SimpleQuotationMetadata { if (maxAge == 0) return instantFetch(token); - ObservationLibrary.Observation storage observation = observations[token]; + ObservationLibrary.Observation memory observation = getLatestObservation(token); require(observation.timestamp != 0, "AbstractOracle: MISSING_OBSERVATION"); require(block.timestamp <= observation.timestamp + maxAge, "AbstractOracle: RATE_TOO_OLD"); From 31d6c25a6cb2af14d0888ad791bd0b1688fc3afd Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Mon, 9 Jan 2023 15:39:01 -0800 Subject: [PATCH 04/32] Add cardinality to AggregatedOracle - This allows the oracle to store many observations in a ring buffer --- contracts/oracles/AggregatedOracle.sol | 87 ++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 6 deletions(-) diff --git a/contracts/oracles/AggregatedOracle.sol b/contracts/oracles/AggregatedOracle.sol index 6cd3e6e..aea0bdc 100644 --- a/contracts/oracles/AggregatedOracle.sol +++ b/contracts/oracles/AggregatedOracle.sol @@ -10,8 +10,6 @@ import "../libraries/SafeCastExt.sol"; import "../libraries/uniswap-lib/FullMath.sol"; import "../utils/ExplicitQuotationMetadata.sol"; -import "hardhat/console.sol"; - /// @dev Limitation: Supports up to 16 underlying oracles. contract AggregatedOracle is IAggregatedOracle, PeriodicOracle, ExplicitQuotationMetadata { using SafeCast for uint256; @@ -32,6 +30,17 @@ contract AggregatedOracle is IAggregatedOracle, PeriodicOracle, ExplicitQuotatio uint8 liquidityDecimals; } + struct BufferMetadata { + uint8 start; + uint8 end; + uint8 size; + uint8 maxSize; + } + + mapping(address => BufferMetadata) public observationBufferMetadata; + + mapping(address => ObservationLibrary.Observation[]) public observationBuffers; + /// @notice The minimum quote token denominated value of the token liquidity, scaled by this oracle's liquidity /// decimals, required for all underlying oracles to be considered valid and thus included in the aggregation. uint256 public immutable minimumTokenLiquidityValue; @@ -45,6 +54,8 @@ contract AggregatedOracle is IAggregatedOracle, PeriodicOracle, ExplicitQuotatio uint8 internal immutable _liquidityDecimals; + uint8 internal immutable _initialCardinality; + /* * Internal variables */ @@ -120,6 +131,8 @@ contract AggregatedOracle is IAggregatedOracle, PeriodicOracle, ExplicitQuotatio }) ); } + + _initialCardinality = 1; } /* @@ -250,6 +263,70 @@ contract AggregatedOracle is IAggregatedOracle, PeriodicOracle, ExplicitQuotatio * Internal functions */ + function getLatestObservation( + address token + ) public view virtual override returns (ObservationLibrary.Observation memory observation) { + BufferMetadata storage meta = observationBufferMetadata[token]; + + if (meta.size == 0) { + // If the buffer is empty, return the default observation + return ObservationLibrary.Observation({price: 0, tokenLiquidity: 0, quoteTokenLiquidity: 0, timestamp: 0}); + } + + return observationBuffers[token][meta.end]; + } + + function initializeBuffers(address token) internal virtual { + require( + observationBuffers[token].length == 0 && observationBuffers[token].length == 0, + "AggregatedOracle: ALREADY_INITIALIZED" + ); + + BufferMetadata storage meta = observationBufferMetadata[token]; + + // Initialize the buffers + ObservationLibrary.Observation[] storage observationBuffer = observationBuffers[token]; + + for (uint256 i = 0; i < _initialCardinality; ++i) { + observationBuffer.push(); + } + + // Initialize the metadata + meta.start = 0; + meta.end = 0; + meta.size = 0; + meta.maxSize = _initialCardinality; + } + + function push(address token, ObservationLibrary.Observation memory observation) internal virtual { + BufferMetadata storage meta = observationBufferMetadata[token]; + + if (meta.size == 0) { + // Initialize the buffers + initializeBuffers(token); + } else { + meta.end = (meta.end + 1) % meta.maxSize; + } + + observationBuffers[token][meta.end] = observation; + + emit Updated( + token, + observation.price, + observation.tokenLiquidity, + observation.quoteTokenLiquidity, + block.timestamp + ); + + if (meta.size < meta.maxSize && meta.end == meta.size) { + // We are at the end of the array and we have not yet filled it + meta.size++; + } else { + // start was just overwritten + meta.start = (meta.start + 1) % meta.maxSize; + } + } + function performUpdate(bytes memory data) internal override returns (bool) { bool underlyingUpdated; address token = abi.decode(data, (address)); @@ -287,14 +364,14 @@ contract AggregatedOracle is IAggregatedOracle, PeriodicOracle, ExplicitQuotatio if (quoteTokenLiquidity > type(uint112).max) quoteTokenLiquidity = type(uint112).max; if (validResponses >= minimumResponses()) { - ObservationLibrary.Observation storage observation = observations[token]; + ObservationLibrary.Observation memory observation; observation.price = price.toUint112(); // Should never (realistically) overflow observation.tokenLiquidity = uint112(tokenLiquidity); // Will never overflow observation.quoteTokenLiquidity = uint112(quoteTokenLiquidity); // Will never overflow observation.timestamp = block.timestamp.toUint32(); - emit Updated(token, price, tokenLiquidity, quoteTokenLiquidity, block.timestamp); + push(token, observation); return true; } else emit UpdateErrorWithReason(address(this), token, "AggregatedOracle: INVALID_NUM_CONSULTATIONS"); @@ -487,8 +564,6 @@ contract AggregatedOracle is IAggregatedOracle, PeriodicOracle, ExplicitQuotatio // Reverts if none of the underlying oracles report anything require(validResponses > 0, "AggregatedOracle: INVALID_NUM_CONSULTATIONS"); - console.log("AggregatedOracle: instantFetch: bigPrice", bigPrice); - // This revert should realistically never occur, but we use it to prevent an invalid price from being returned require(bigPrice <= type(uint112).max, "AggregatedOracle: PRICE_TOO_HIGH"); From a789ef3ba2dd9b207c00f63f9cca6e09239c6c8a Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Wed, 11 Jan 2023 12:47:23 -0800 Subject: [PATCH 05/32] Add IHistoricalOracle --- contracts/interfaces/IHistoricalOracle.sol | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 contracts/interfaces/IHistoricalOracle.sol diff --git a/contracts/interfaces/IHistoricalOracle.sol b/contracts/interfaces/IHistoricalOracle.sol new file mode 100644 index 0000000..12eb446 --- /dev/null +++ b/contracts/interfaces/IHistoricalOracle.sol @@ -0,0 +1,44 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.5.0 <0.9.0; + +import "../libraries/ObservationLibrary.sol"; + +/** + * @title IHistoricalOracle + * @notice An interface that defines an oracle contract that stores historical observations. + */ +interface IHistoricalOracle { + /// @notice Gets an observation for a token at a specific index. + /// @param token The address of the token to get the observation for. + /// @param index The index of the observation to get, where index 0 contains the latest observation, and the last + /// index contains the oldest observation (uses reverse chronological ordering). + /// @return observation The observation for the token at the specified index. + function getObservationAt( + address token, + uint256 index + ) external view returns (ObservationLibrary.Observation memory); + + /// @notice Gets the latest observations for a token. + /// @param token The address of the token to get the observations for. + /// @param amount The number of observations to get. + /// @return observations The latest observations for the token, in reverse chronological order, from newest to oldest. + function getObservations( + address token, + uint256 amount + ) external view returns (ObservationLibrary.Observation[] memory); + + /// @notice Gets the number of observations for a token. + /// @param token The address of the token to get the number of observations for. + /// @return count The number of observations for the token. + function getObservationsCount(address token) external view returns (uint256); + + /// @notice Gets the capacity of observations for a token. + /// @param token The address of the token to get the capacity of observations for. + /// @return capacity The capacity of observations for the token. + function getObservationsCapacity(address token) external view returns (uint256); + + /// @notice Sets the capacity of observations for a token. + /// @param token The address of the token to set the capacity of observations for. + /// @param amount The new capacity of observations for the token. + function setObservationsCapacity(address token, uint256 amount) external; +} From db45b71ad4ac01a0963097ad0ae78bedcfe72927 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Wed, 11 Jan 2023 12:48:55 -0800 Subject: [PATCH 06/32] Implement IHistoricalOracle in AggregatedOracle --- contracts/oracles/AggregatedOracle.sol | 98 ++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 4 deletions(-) diff --git a/contracts/oracles/AggregatedOracle.sol b/contracts/oracles/AggregatedOracle.sol index aea0bdc..fc919e6 100644 --- a/contracts/oracles/AggregatedOracle.sol +++ b/contracts/oracles/AggregatedOracle.sol @@ -6,12 +6,13 @@ import "@openzeppelin-v4/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "./PeriodicOracle.sol"; import "../interfaces/IAggregatedOracle.sol"; +import "../interfaces/IHistoricalOracle.sol"; import "../libraries/SafeCastExt.sol"; import "../libraries/uniswap-lib/FullMath.sol"; import "../utils/ExplicitQuotationMetadata.sol"; /// @dev Limitation: Supports up to 16 underlying oracles. -contract AggregatedOracle is IAggregatedOracle, PeriodicOracle, ExplicitQuotationMetadata { +contract AggregatedOracle is IAggregatedOracle, IHistoricalOracle, PeriodicOracle, ExplicitQuotationMetadata { using SafeCast for uint256; using SafeCastExt for uint256; @@ -70,6 +71,13 @@ contract AggregatedOracle is IAggregatedOracle, PeriodicOracle, ExplicitQuotatio mapping(address => bool) private oracleExists; mapping(address => mapping(address => bool)) private oracleForExists; + /// @notice Event emitted when an observation buffer's capacity is increased past the initial capacity. + /// @dev Buffer initialization does not emit an event. + /// @param token The token for which the observation buffer's capacity was increased. + /// @param oldCapacity The previous capacity of the observation buffer. + /// @param newCapacity The new capacity of the observation buffer. + event ObservationCapacityIncreased(address indexed token, uint256 oldCapacity, uint256 newCapacity); + /* * Constructors */ @@ -168,6 +176,86 @@ contract AggregatedOracle is IAggregatedOracle, PeriodicOracle, ExplicitQuotatio return allOracles; } + /// @inheritdoc IHistoricalOracle + function getObservationAt( + address token, + uint256 index + ) external view virtual override returns (ObservationLibrary.Observation memory) { + BufferMetadata memory meta = observationBufferMetadata[token]; + + require(index < meta.size, "AggregatedOracle: INVALID_INDEX"); + + uint256 bufferIndex = uint256((int256(uint256(meta.end)) - int256(index)) % int256(uint256(meta.size))); + + return observationBuffers[token][bufferIndex]; + } + + /// @inheritdoc IHistoricalOracle + function getObservations( + address token, + uint256 amount + ) external view virtual override returns (ObservationLibrary.Observation[] memory) { + if (amount == 0) return new ObservationLibrary.Observation[](0); + + BufferMetadata memory meta = observationBufferMetadata[token]; + require(meta.size >= amount, "AggregatedOracle: INSUFFICIENT_DATA"); + + ObservationLibrary.Observation[] memory observations = new ObservationLibrary.Observation[](amount); + + uint256 count = 0; + + for (uint256 i = meta.end; count < amount; i = (i == 0) ? meta.size - 1 : i - 1) { + observations[count++] = observationBuffers[token][i]; + } + + return observations; + } + + /// @inheritdoc IHistoricalOracle + function getObservationsCount(address token) external view override returns (uint256) { + return observationBufferMetadata[token].size; + } + + /// @inheritdoc IHistoricalOracle + function getObservationsCapacity(address token) external view virtual override returns (uint256) { + uint256 maxSize = observationBufferMetadata[token].maxSize; + if (maxSize == 0) return _initialCardinality; + + return maxSize; + } + + /// @inheritdoc IHistoricalOracle + /// @param amount The new capacity of observations for the token. Must be greater than the current capacity, but + /// less than 256. + function setObservationsCapacity(address token, uint256 amount) external virtual override { + BufferMetadata storage meta = observationBufferMetadata[token]; + if (meta.maxSize == 0) { + // Buffer is not initialized yet + initializeBuffers(token); + } + + require(amount >= meta.maxSize, "AggregatedOracle: CAPACITY_CANNOT_BE_DECREASED"); + require(amount <= type(uint8).max, "AggregatedOracle: CAPACITY_TOO_LARGE"); + + ObservationLibrary.Observation[] storage observationBuffer = observationBuffers[token]; + + // Add new slots to the buffer + uint256 capacityToAdd = amount - meta.maxSize; + for (uint256 i = 0; i < capacityToAdd; ++i) { + // Push a dummy observation with non-zero values to put most of the gas cost on the caller + observationBuffer.push( + ObservationLibrary.Observation({price: 1, tokenLiquidity: 1, quoteTokenLiquidity: 1, timestamp: 1}) + ); + } + + if (meta.maxSize != amount) { + emit ObservationCapacityIncreased(token, meta.maxSize, amount); + + // Update the metadata + meta.maxSize = uint8(amount); + } + } + /* * Public functions */ @@ -302,8 +390,10 @@ contract AggregatedOracle is IAggregatedOracle, PeriodicOracle, ExplicitQuotatio BufferMetadata storage meta = observationBufferMetadata[token]; if (meta.size == 0) { - // Initialize the buffers - initializeBuffers(token); + if (meta.maxSize == 0) { + // Initialize the buffers + initializeBuffers(token); + } } else { meta.end = (meta.end + 1) % meta.maxSize; } @@ -323,7 +413,7 @@ contract AggregatedOracle is IAggregatedOracle, PeriodicOracle, ExplicitQuotatio meta.size++; } else { // start was just overwritten - meta.start = (meta.start + 1) % meta.maxSize; + meta.start = (meta.start + 1) % meta.size; } } From 720a58888a0ec6377276af986e6ca3ec4d99df2d Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Wed, 11 Jan 2023 16:01:44 -0800 Subject: [PATCH 07/32] Overload IHistoricalOracle#getObservations to add offset and increment --- contracts/interfaces/IHistoricalOracle.sol | 13 ++++++ contracts/oracles/AggregatedOracle.sol | 49 ++++++++++++++++------ 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/contracts/interfaces/IHistoricalOracle.sol b/contracts/interfaces/IHistoricalOracle.sol index 12eb446..442455a 100644 --- a/contracts/interfaces/IHistoricalOracle.sol +++ b/contracts/interfaces/IHistoricalOracle.sol @@ -27,6 +27,19 @@ interface IHistoricalOracle { uint256 amount ) external view returns (ObservationLibrary.Observation[] memory); + /// @notice Gets the latest observations for a token. + /// @param token The address of the token to get the observations for. + /// @param amount The number of observations to get. + /// @param offset The index of the first observation to get (default: 0). + /// @param increment The increment between observations to get (default: 1). + /// @return observations The latest observations for the token, in reverse chronological order, from newest to oldest. + function getObservations( + address token, + uint256 amount, + uint256 offset, + uint256 increment + ) external view returns (ObservationLibrary.Observation[] memory); + /// @notice Gets the number of observations for a token. /// @param token The address of the token to get the number of observations for. /// @return count The number of observations for the token. diff --git a/contracts/oracles/AggregatedOracle.sol b/contracts/oracles/AggregatedOracle.sol index fc919e6..3de934c 100644 --- a/contracts/oracles/AggregatedOracle.sol +++ b/contracts/oracles/AggregatedOracle.sol @@ -195,20 +195,17 @@ contract AggregatedOracle is IAggregatedOracle, IHistoricalOracle, PeriodicOracl address token, uint256 amount ) external view virtual override returns (ObservationLibrary.Observation[] memory) { - if (amount == 0) return new ObservationLibrary.Observation[](0); - - BufferMetadata memory meta = observationBufferMetadata[token]; - require(meta.size >= amount, "AggregatedOracle: INSUFFICIENT_DATA"); - - ObservationLibrary.Observation[] memory observations = new ObservationLibrary.Observation[](amount); - - uint256 count = 0; - - for (uint256 i = meta.end; count < amount; i = (i == 0) ? meta.size - 1 : i - 1) { - observations[count++] = observationBuffers[token][i]; - } + return getObservationsInternal(token, amount, 0, 1); + } - return observations; + /// @inheritdoc IHistoricalOracle + function getObservations( + address token, + uint256 amount, + uint256 offset, + uint256 increment + ) external view virtual returns (ObservationLibrary.Observation[] memory) { + return getObservationsInternal(token, amount, offset, increment); } /// @inheritdoc IHistoricalOracle @@ -364,6 +361,32 @@ contract AggregatedOracle is IAggregatedOracle, IHistoricalOracle, PeriodicOracl return observationBuffers[token][meta.end]; } + function getObservationsInternal( + address token, + uint256 amount, + uint256 offset, + uint256 increment + ) internal view virtual returns (ObservationLibrary.Observation[] memory) { + if (amount == 0) return new ObservationLibrary.Observation[](0); + + BufferMetadata memory meta = observationBufferMetadata[token]; + require(meta.size > (amount - 1) * increment + offset, "AggregatedOracle: INSUFFICIENT_DATA"); + + ObservationLibrary.Observation[] memory observations = new ObservationLibrary.Observation[](amount); + + uint256 count = 0; + + for ( + uint256 i = meta.end < offset ? meta.end + meta.size - offset : meta.end - offset; + count < amount; + i = (i < increment) ? (i + meta.size) - increment : i - increment + ) { + observations[count++] = observationBuffers[token][i]; + } + + return observations; + } + function initializeBuffers(address token) internal virtual { require( observationBuffers[token].length == 0 && observationBuffers[token].length == 0, From c96e0ca92ef7bb994d139472a3e320667f378b25 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Wed, 11 Jan 2023 16:26:42 -0800 Subject: [PATCH 08/32] Fix bug in AggregatedOracle#getObservationAt --- contracts/oracles/AggregatedOracle.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/oracles/AggregatedOracle.sol b/contracts/oracles/AggregatedOracle.sol index 3de934c..ac05f21 100644 --- a/contracts/oracles/AggregatedOracle.sol +++ b/contracts/oracles/AggregatedOracle.sol @@ -185,7 +185,7 @@ contract AggregatedOracle is IAggregatedOracle, IHistoricalOracle, PeriodicOracl require(index < meta.size, "AggregatedOracle: INVALID_INDEX"); - uint256 bufferIndex = uint256((int256(uint256(meta.end)) - int256(index)) % int256(uint256(meta.size))); + uint256 bufferIndex = meta.end < index ? meta.end + meta.size - index : meta.end - index; return observationBuffers[token][bufferIndex]; } From 75a852fa42a85167a94574f9273e4f5f32c69118 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Wed, 11 Jan 2023 16:34:25 -0800 Subject: [PATCH 09/32] Increase max cardinality to 65535 --- contracts/oracles/AggregatedOracle.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/oracles/AggregatedOracle.sol b/contracts/oracles/AggregatedOracle.sol index ac05f21..447bc7e 100644 --- a/contracts/oracles/AggregatedOracle.sol +++ b/contracts/oracles/AggregatedOracle.sol @@ -32,10 +32,10 @@ contract AggregatedOracle is IAggregatedOracle, IHistoricalOracle, PeriodicOracl } struct BufferMetadata { - uint8 start; - uint8 end; - uint8 size; - uint8 maxSize; + uint16 start; + uint16 end; + uint16 size; + uint16 maxSize; } mapping(address => BufferMetadata) public observationBufferMetadata; @@ -55,7 +55,7 @@ contract AggregatedOracle is IAggregatedOracle, IHistoricalOracle, PeriodicOracl uint8 internal immutable _liquidityDecimals; - uint8 internal immutable _initialCardinality; + uint16 internal immutable _initialCardinality; /* * Internal variables @@ -223,7 +223,7 @@ contract AggregatedOracle is IAggregatedOracle, IHistoricalOracle, PeriodicOracl /// @inheritdoc IHistoricalOracle /// @param amount The new capacity of observations for the token. Must be greater than the current capacity, but - /// less than 256. + /// less than 65536. function setObservationsCapacity(address token, uint256 amount) external virtual override { BufferMetadata storage meta = observationBufferMetadata[token]; if (meta.maxSize == 0) { @@ -232,7 +232,7 @@ contract AggregatedOracle is IAggregatedOracle, IHistoricalOracle, PeriodicOracl } require(amount >= meta.maxSize, "AggregatedOracle: CAPACITY_CANNOT_BE_DECREASED"); - require(amount <= type(uint8).max, "AggregatedOracle: CAPACITY_TOO_LARGE"); + require(amount <= type(uint16).max, "AggregatedOracle: CAPACITY_TOO_LARGE"); ObservationLibrary.Observation[] storage observationBuffer = observationBuffers[token]; @@ -249,7 +249,7 @@ contract AggregatedOracle is IAggregatedOracle, IHistoricalOracle, PeriodicOracl emit ObservationCapacityIncreased(token, meta.maxSize, amount); // Update the metadata - meta.maxSize = uint8(amount); + meta.maxSize = uint16(amount); } } From a3a30dcc9b2e0f558b2bf6100c012e8717d0047e Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Fri, 13 Jan 2023 13:23:07 -0800 Subject: [PATCH 10/32] Use historical oracle functions in consult aggregate script --- scripts/consult-aggregate.js | 117 +++++++++++++++++++++++++++-------- 1 file changed, 91 insertions(+), 26 deletions(-) diff --git a/scripts/consult-aggregate.js b/scripts/consult-aggregate.js index 20b118b..6d96bb3 100644 --- a/scripts/consult-aggregate.js +++ b/scripts/consult-aggregate.js @@ -160,10 +160,12 @@ async function main() { const underlyingPeriodSeconds = 10; const periodSeconds = 10; - const granularity = 1; + const granularity = 2; const liquidityDecimals = 4; + const increaseObservationsCapacityTo = 10; + const uniswapV2 = await createUniswapV2Oracle( uniswapV2FactoryAddress, uniswapV2InitCodeHash, @@ -358,40 +360,103 @@ async function main() { ); } - const consultGas = await oracle.estimateGas["consult(address)"](token); + const observationCount = await oracle["getObservationsCount(address)"](token); + const observationCapacity = await oracle["getObservationsCapacity(address)"](token); + + console.log("Observations count = %s, capacity = %s", observationCount, observationCapacity); - if (!consultGas.eq(lastConsultGas)) { - console.log("\u001b[" + 93 + "m" + "Consult gas used = " + consultGas + "\u001b[0m"); + if (observationCapacity < increaseObservationsCapacityTo) { + const capacityTx = await oracle["setObservationsCapacity(address,uint256)"]( + token, + increaseObservationsCapacityTo + ); + const capacityReceipt = await capacityTx.wait(); - lastConsultGas = consultGas; + console.log( + "\u001b[" + + 93 + + "m" + + "Oracle capacity updated. Gas used = " + + capacityReceipt["gasUsed"] + + "\u001b[0m" + ); } - const consultation = await oracle["consult(address)"](token); + if (observationCount > 0) { + const consultGas = await oracle.estimateGas["consult(address)"](token); + + if (!consultGas.eq(lastConsultGas)) { + console.log("\u001b[" + 93 + "m" + "Consult gas used = " + consultGas + "\u001b[0m"); + + lastConsultGas = consultGas; + } + + const consultation = await oracle["consult(address)"](token); + + const priceStr = ethers.utils.commify( + ethers.utils.formatUnits(consultation["price"], quoteTokenDecimals) + ); + + console.log( + "\u001b[" + 32 + "m" + "Price(%s) = %s %s" + "\u001b[0m", + tokenSymbol, + priceStr, + quoteTokenSymbol + ); + + const tokenLiquidityStr = ethers.utils.commify( + ethers.utils.formatUnits(consultation["tokenLiquidity"], liquidityDecimals) + ); - const priceStr = ethers.utils.commify(ethers.utils.formatUnits(consultation["price"], quoteTokenDecimals)); + const quoteTokenLiquidityStr = ethers.utils.commify( + ethers.utils.formatUnits(consultation["quoteTokenLiquidity"], liquidityDecimals) + ); - console.log( - "\u001b[" + 32 + "m" + "Price(%s) = %s %s" + "\u001b[0m", - tokenSymbol, - priceStr, - quoteTokenSymbol - ); + console.log( + "\u001b[" + 31 + "m" + "Liquidity(%s) = %s, Liquidity(%s) = %s" + "\u001b[0m", + tokenSymbol, + tokenLiquidityStr, + quoteTokenSymbol, + quoteTokenLiquidityStr + ); + + const observationAt0 = await oracle["getObservationAt(address,uint256)"](token, 0); + console.log("Observation at 0 = %s", observationAt0.toString()); - const tokenLiquidityStr = ethers.utils.commify( - ethers.utils.formatUnits(consultation["tokenLiquidity"], liquidityDecimals) - ); + if (observationCount > granularity) { + const observationAt2 = await oracle["getObservationAt(address,uint256)"](token, granularity); + console.log("Observation at %s = %s", granularity.toString(), observationAt2.toString()); + } - const quoteTokenLiquidityStr = ethers.utils.commify( - ethers.utils.formatUnits(consultation["quoteTokenLiquidity"], liquidityDecimals) - ); + const numObservationsToGet = ethers.BigNumber.from(observationCount).div(granularity); + const offset = 0; - console.log( - "\u001b[" + 31 + "m" + "Liquidity(%s) = %s, Liquidity(%s) = %s" + "\u001b[0m", - tokenSymbol, - tokenLiquidityStr, - quoteTokenSymbol, - quoteTokenLiquidityStr - ); + const observationsGas = await oracle.estimateGas["getObservations(address,uint256)"]( + token, + observationCount + ); + const observations = await oracle["getObservations(address,uint256,uint256,uint256)"]( + token, + numObservationsToGet, + offset, + granularity + ); + + console.log("Observations gas used = %s", observationsGas.toNumber()); + // Print the observations, expanding each observation into its own line + console.log( + "\u001b[" + + 93 + + "m" + + "Observations(%s, %s, %s, %s) = " + + observations.map((o) => o.toString()).join(", ") + + "\u001b[0m", + tokenSymbol, + numObservationsToGet.toString(), + offset.toString(), + granularity.toString() + ); + } } catch (e) { console.log(e); } From c37c4e1ababdd570df1f9080c0f960283f341413 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Tue, 31 Jan 2023 14:16:29 -0800 Subject: [PATCH 11/32] Move oracle observation storage out of AbstractOracle --- contracts/oracles/AbstractOracle.sol | 6 +----- contracts/oracles/PeriodicAccumulationOracle.sol | 8 ++++++++ contracts/test/oracles/AggregatedOracleStub.sol | 4 +++- contracts/test/oracles/MockOracle.sol | 8 ++++++++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/contracts/oracles/AbstractOracle.sol b/contracts/oracles/AbstractOracle.sol index 86cfd8e..93bce4b 100644 --- a/contracts/oracles/AbstractOracle.sol +++ b/contracts/oracles/AbstractOracle.sol @@ -9,8 +9,6 @@ import "../libraries/ObservationLibrary.sol"; import "../utils/SimpleQuotationMetadata.sol"; abstract contract AbstractOracle is IERC165, IOracle, SimpleQuotationMetadata { - mapping(address => ObservationLibrary.Observation) public observations; - constructor(address quoteToken_) SimpleQuotationMetadata(quoteToken_) {} /// @param data The encoded address of the token for which to perform the update. @@ -27,9 +25,7 @@ abstract contract AbstractOracle is IERC165, IOracle, SimpleQuotationMetadata { function getLatestObservation( address token - ) public view virtual returns (ObservationLibrary.Observation memory observation) { - return observations[token]; - } + ) public view virtual returns (ObservationLibrary.Observation memory observation); /// @param data The encoded address of the token for which the update relates to. /// @inheritdoc IUpdateable diff --git a/contracts/oracles/PeriodicAccumulationOracle.sol b/contracts/oracles/PeriodicAccumulationOracle.sol index c7d5d5d..3163621 100644 --- a/contracts/oracles/PeriodicAccumulationOracle.sol +++ b/contracts/oracles/PeriodicAccumulationOracle.sol @@ -30,6 +30,8 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, mapping(address => AccumulationLibrary.PriceAccumulator[]) public priceAccumulationBuffers; mapping(address => AccumulationLibrary.LiquidityAccumulator[]) public liquidityAccumulationBuffers; + mapping(address => ObservationLibrary.Observation) internal observations; + constructor( address liquidityAccumulator_, address priceAccumulator_, @@ -41,6 +43,12 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, priceAccumulator = priceAccumulator_; } + function getLatestObservation( + address token + ) public view virtual override returns (ObservationLibrary.Observation memory observation) { + return observations[token]; + } + /// @inheritdoc PeriodicOracle function canUpdate(bytes memory data) public view virtual override returns (bool) { uint256 gracePeriod = accumulatorUpdateDelayTolerance(); diff --git a/contracts/test/oracles/AggregatedOracleStub.sol b/contracts/test/oracles/AggregatedOracleStub.sol index 314b207..8a93adf 100644 --- a/contracts/test/oracles/AggregatedOracleStub.sol +++ b/contracts/test/oracles/AggregatedOracleStub.sol @@ -70,12 +70,14 @@ contract AggregatedOracleStub is AggregatedOracle { uint112 quoteTokenLiquidity, uint32 timestamp ) public { - ObservationLibrary.Observation storage observation = observations[token]; + ObservationLibrary.Observation memory observation; observation.price = price; observation.tokenLiquidity = tokenLiquidity; observation.quoteTokenLiquidity = quoteTokenLiquidity; observation.timestamp = timestamp; + + push(token, observation); } function overrideNeedsUpdate(bool overridden, bool needsUpdate_) public { diff --git a/contracts/test/oracles/MockOracle.sol b/contracts/test/oracles/MockOracle.sol index 436cd0a..2c8dda3 100644 --- a/contracts/test/oracles/MockOracle.sol +++ b/contracts/test/oracles/MockOracle.sol @@ -15,12 +15,20 @@ contract MockOracle is AbstractOracle { uint8 _liquidityDecimals; + mapping(address => ObservationLibrary.Observation) public observations; + mapping(address => ObservationLibrary.Observation) instantRates; constructor(address quoteToken_) AbstractOracle(quoteToken_) { _liquidityDecimals = 0; } + function getLatestObservation( + address token + ) public view virtual override returns (ObservationLibrary.Observation memory observation) { + return observations[token]; + } + function stubSetObservation( address token, uint112 price, From 072b00400806f91e73d728527dafb0eb0af50653 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Thu, 2 Feb 2023 12:03:42 -0800 Subject: [PATCH 12/32] Fix tests and correct problems --- contracts/oracles/AggregatedOracle.sol | 1 + .../oracles/PeriodicAccumulationOracle.sol | 31 ++-- contracts/test/InterfaceIds.sol | 5 + .../PeriodicAccumulationOracleStub.sol | 51 ++++++- test/oracles/aggregated-oracle.js | 132 +++++++++++++----- test/oracles/periodic-accumulation-oracle.js | 124 ++++++++++++---- 6 files changed, 259 insertions(+), 85 deletions(-) diff --git a/contracts/oracles/AggregatedOracle.sol b/contracts/oracles/AggregatedOracle.sol index 447bc7e..82a0c07 100644 --- a/contracts/oracles/AggregatedOracle.sol +++ b/contracts/oracles/AggregatedOracle.sol @@ -307,6 +307,7 @@ contract AggregatedOracle is IAggregatedOracle, IHistoricalOracle, PeriodicOracl ) public view virtual override(PeriodicOracle, ExplicitQuotationMetadata) returns (bool) { return interfaceId == type(IAggregatedOracle).interfaceId || + interfaceId == type(IHistoricalOracle).interfaceId || ExplicitQuotationMetadata.supportsInterface(interfaceId) || PeriodicOracle.supportsInterface(interfaceId); } diff --git a/contracts/oracles/PeriodicAccumulationOracle.sol b/contracts/oracles/PeriodicAccumulationOracle.sol index 3163621..0a9c79c 100644 --- a/contracts/oracles/PeriodicAccumulationOracle.sol +++ b/contracts/oracles/PeriodicAccumulationOracle.sol @@ -148,7 +148,7 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, address token, AccumulationLibrary.PriceAccumulator memory priceAccumulation, AccumulationLibrary.LiquidityAccumulator memory liquidityAccumulation - ) internal virtual { + ) internal virtual returns (bool) { BufferMetadata storage meta = accumulationBufferMetadata[token]; if (meta.size == 0) { @@ -157,13 +157,20 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, } else { // We have multiple accumulations now - uint256 firstAccumulationTime = priceAccumulationBuffers[token][meta.start].timestamp; - uint256 periodTimeElapsed = priceAccumulation.timestamp - firstAccumulationTime; + uint256 firstPriceAccumulationTime = priceAccumulationBuffers[token][meta.start].timestamp; + uint256 pricePeriodTimeElapsed = priceAccumulation.timestamp - firstPriceAccumulationTime; + + uint256 firstLiquidityAccumulationTime = liquidityAccumulationBuffers[token][meta.start].timestamp; + uint256 liquidityPeriodTimeElapsed = liquidityAccumulation.timestamp - firstLiquidityAccumulationTime; + + uint256 maxUpdateGap = period + updateDelayTolerance(); if ( - periodTimeElapsed <= period + updateDelayTolerance() && meta.size == granularity && - periodTimeElapsed >= period + pricePeriodTimeElapsed <= maxUpdateGap && + pricePeriodTimeElapsed >= period && + liquidityPeriodTimeElapsed <= maxUpdateGap && + liquidityPeriodTimeElapsed >= period ) { ObservationLibrary.Observation storage observation = observations[token]; @@ -183,6 +190,9 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, observation.quoteTokenLiquidity, observation.timestamp ); + } else if (pricePeriodTimeElapsed == 0 && liquidityPeriodTimeElapsed == 0) { + // Both accumulations haven't changed, so we don't need to update + return false; } meta.end = (meta.end + 1) % uint8(granularity); @@ -197,6 +207,8 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, // start was just overwritten meta.start = (meta.start + 1) % uint8(granularity); } + + return true; } function performUpdate(bytes memory data) internal virtual override returns (bool) { @@ -228,13 +240,10 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, liquidityAccumulator ).getCurrentAccumulation(token); - if (priceAccumulation.timestamp != 0 && liquidityAccumulation.timestamp != 0) { + return + priceAccumulation.timestamp != 0 && + liquidityAccumulation.timestamp != 0 && push(token, priceAccumulation, liquidityAccumulation); - - return true; - } - - return false; } /// @inheritdoc AbstractOracle diff --git a/contracts/test/InterfaceIds.sol b/contracts/test/InterfaceIds.sol index c2faabe..22653a4 100644 --- a/contracts/test/InterfaceIds.sol +++ b/contracts/test/InterfaceIds.sol @@ -13,6 +13,7 @@ import "../interfaces/IPriceOracle.sol"; import "../interfaces/IQuoteToken.sol"; import "../interfaces/IUpdateable.sol"; import "../interfaces/IAccumulator.sol"; +import "../interfaces/IHistoricalOracle.sol"; contract InterfaceIds { function iAggregatedOracle() external pure returns (bytes4) { @@ -62,4 +63,8 @@ contract InterfaceIds { function iAccumulator() external pure returns (bytes4) { return type(IAccumulator).interfaceId; } + + function iHistoricalOracle() external pure returns (bytes4) { + return type(IHistoricalOracle).interfaceId; + } } diff --git a/contracts/test/oracles/PeriodicAccumulationOracleStub.sol b/contracts/test/oracles/PeriodicAccumulationOracleStub.sol index 746d481..c7e3a01 100644 --- a/contracts/test/oracles/PeriodicAccumulationOracleStub.sol +++ b/contracts/test/oracles/PeriodicAccumulationOracleStub.sol @@ -19,6 +19,16 @@ contract PeriodicAccumulationOracleStub is PeriodicAccumulationOracle { uint256 granularity_ ) PeriodicAccumulationOracle(liquidityAccumulator_, priceAccumulator_, quoteToken_, period_, granularity_) {} + function priceAccumulations(address token) public view returns (AccumulationLibrary.PriceAccumulator memory) { + return priceAccumulationBuffers[token][accumulationBufferMetadata[token].end]; + } + + function liquidityAccumulations( + address token + ) public view returns (AccumulationLibrary.LiquidityAccumulator memory) { + return liquidityAccumulationBuffers[token][accumulationBufferMetadata[token].end]; + } + function stubSetObservation( address token, uint112 price, @@ -41,22 +51,32 @@ contract PeriodicAccumulationOracleStub is PeriodicAccumulationOracle { uint112 cumulativeQuoteTokenLiquidity, uint32 timestamp ) public { - /*AccumulationLibrary.LiquidityAccumulator storage liquidityAccumulation = liquidityAccumulations[token]; - AccumulationLibrary.PriceAccumulator storage priceAccumulation = priceAccumulations[token]; + ensureBuffersInitialized(token); + + BufferMetadata storage meta = accumulationBufferMetadata[token]; + + AccumulationLibrary.PriceAccumulator storage priceAccumulation = priceAccumulationBuffers[token][meta.end]; + AccumulationLibrary.LiquidityAccumulator storage liquidityAccumulation = liquidityAccumulationBuffers[token][ + meta.end + ]; priceAccumulation.cumulativePrice = cumulativePrice; priceAccumulation.timestamp = timestamp; liquidityAccumulation.cumulativeTokenLiquidity = cumulativeTokenLiquidity; liquidityAccumulation.cumulativeQuoteTokenLiquidity = cumulativeQuoteTokenLiquidity; - liquidityAccumulation.timestamp = timestamp;*/ + liquidityAccumulation.timestamp = timestamp; } function stubSetPriceAccumulation(address token, uint112 cumulativePrice, uint32 timestamp) public { - /*AccumulationLibrary.PriceAccumulator storage priceAccumulation = priceAccumulations[token]; + ensureBuffersInitialized(token); + + BufferMetadata storage meta = accumulationBufferMetadata[token]; + + AccumulationLibrary.PriceAccumulator storage priceAccumulation = priceAccumulationBuffers[token][meta.end]; priceAccumulation.cumulativePrice = cumulativePrice; - priceAccumulation.timestamp = timestamp;*/ + priceAccumulation.timestamp = timestamp; } function stubSetLiquidityAccumulation( @@ -65,11 +85,17 @@ contract PeriodicAccumulationOracleStub is PeriodicAccumulationOracle { uint112 cumulativeQuoteTokenLiquidity, uint32 timestamp ) public { - /*AccumulationLibrary.LiquidityAccumulator storage liquidityAccumulation = liquidityAccumulations[token]; + ensureBuffersInitialized(token); + + BufferMetadata storage meta = accumulationBufferMetadata[token]; + + AccumulationLibrary.LiquidityAccumulator storage liquidityAccumulation = liquidityAccumulationBuffers[token][ + meta.end + ]; liquidityAccumulation.cumulativeTokenLiquidity = cumulativeTokenLiquidity; liquidityAccumulation.cumulativeQuoteTokenLiquidity = cumulativeQuoteTokenLiquidity; - liquidityAccumulation.timestamp = timestamp;*/ + liquidityAccumulation.timestamp = timestamp; } function overrideNeedsUpdate(bool overridden, bool needsUpdate_) public { @@ -97,4 +123,15 @@ contract PeriodicAccumulationOracleStub is PeriodicAccumulationOracle { return super.performUpdate(data); } + + function ensureBuffersInitialized(address token) internal virtual { + BufferMetadata storage meta = accumulationBufferMetadata[token]; + + if (meta.size == 0) { + AccumulationLibrary.PriceAccumulator memory priceAccumulation; + AccumulationLibrary.LiquidityAccumulator memory liquidityAccumulation; + + push(token, priceAccumulation, liquidityAccumulation); + } + } } diff --git a/test/oracles/aggregated-oracle.js b/test/oracles/aggregated-oracle.js index ac93ef3..d397671 100644 --- a/test/oracles/aggregated-oracle.js +++ b/test/oracles/aggregated-oracle.js @@ -9,6 +9,7 @@ const GRT = "0xc944E90C64B2c07662A292be6244BDf05Cda44a7"; const BAT = "0x0D8775F648430679A709E98d2b0Cb6250d2887EF"; const PERIOD = 100; +const GRANULARITY = 1; const MINIMUM_TOKEN_LIQUIDITY_VALUE = BigNumber.from(0); const MINIMUM_QUOTE_TOKEN_LIQUIDITY = BigNumber.from(0); @@ -86,6 +87,7 @@ describe("AggregatedOracle#constructor", async function () { const oracles = [oracle1.address]; const tokenSpecificOracles = [grtOracle]; const period = 30; + const granularity = 5; const minimumTokenLiquidityValue = BigNumber.from(1); const minimumQuoteTokenLiquidity = BigNumber.from(2); @@ -98,6 +100,7 @@ describe("AggregatedOracle#constructor", async function () { oracles, tokenSpecificOracles, period, + granularity, minimumTokenLiquidityValue, minimumQuoteTokenLiquidity ); @@ -109,6 +112,7 @@ describe("AggregatedOracle#constructor", async function () { expect(await oracle.liquidityDecimals()).to.equal(liquidityDecimals); expect(await oracle.getOracles()).to.eql(oracles); // eql = deep equality expect(await oracle.period()).to.equal(period); + expect(await oracle.granularity()).to.equal(granularity); expect(await oracle.minimumTokenLiquidityValue()).to.equal(minimumTokenLiquidityValue); expect(await oracle.minimumQuoteTokenLiquidity()).to.equal(minimumQuoteTokenLiquidity); @@ -128,6 +132,7 @@ describe("AggregatedOracle#constructor", async function () { [], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ) @@ -148,6 +153,7 @@ describe("AggregatedOracle#constructor", async function () { [oracle1.address, oracle1.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ) @@ -173,6 +179,7 @@ describe("AggregatedOracle#constructor", async function () { [], [oracle1Config, oracle1Config], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ) @@ -198,6 +205,7 @@ describe("AggregatedOracle#constructor", async function () { [oracle1.address], [oracle1Config], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ) @@ -224,6 +232,7 @@ describe("AggregatedOracle#needsUpdate", function () { [underlyingOracle.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -306,6 +315,7 @@ describe("AggregatedOracle#canUpdate", function () { [underlyingOracle1.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -388,6 +398,7 @@ describe("AggregatedOracle#consultPrice(token)", function () { [underlyingOracle.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -442,6 +453,7 @@ describe("AggregatedOracle#consultPrice(token, maxAge = 0)", function () { [underlyingOracle.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -479,6 +491,7 @@ describe("AggregatedOracle#consultPrice(token, maxAge)", function () { [underlyingOracle.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -636,6 +649,7 @@ describe("AggregatedOracle#consultLiquidity(token)", function () { [underlyingOracle.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -694,6 +708,7 @@ describe("AggregatedOracle#consultLiquidity(token, maxAge = 0)", function () { [underlyingOracle.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -765,6 +780,7 @@ describe("AggregatedOracle#consultLiquidity(token, maxAge)", function () { [underlyingOracle.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -942,6 +958,7 @@ describe("AggregatedOracle#consult(token)", function () { [underlyingOracle.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -1004,6 +1021,7 @@ describe("AggregatedOracle#consult(token, maxAge = 0)", function () { [underlyingOracle.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -1042,6 +1060,7 @@ describe("AggregatedOracle#consult(token, maxAge = 0)", function () { [underlyingOracle.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -1076,6 +1095,7 @@ describe("AggregatedOracle#consult(token, maxAge = 0)", function () { [underlyingOracle.address, underlyingOracle2.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -1111,6 +1131,7 @@ describe("AggregatedOracle#consult(token, maxAge = 0)", function () { [underlyingOracle.address, underlyingOracle2.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -1146,6 +1167,7 @@ describe("AggregatedOracle#consult(token, maxAge = 0)", function () { [underlyingOracle.address, underlyingOracle2.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -1248,6 +1270,7 @@ describe("AggregatedOracle#consult(token, maxAge)", function () { [underlyingOracle.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -1377,6 +1400,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { [underlyingOracle.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -1406,7 +1430,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(price); expect(oTokenLiquidity).to.equal(tokenLiquidity); @@ -1440,7 +1464,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(price); expect(oTokenLiquidity).to.equal(tokenLiquidity); @@ -1472,7 +1496,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { .to.emit(oracle, "Updated") .withArgs(token, expectedPrice, tokenLiquidity, quoteTokenLiquidity, timestamp); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(expectedPrice); expect(oTokenLiquidity).to.equal(tokenLiquidity); @@ -1504,7 +1528,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { .to.emit(oracle, "Updated") .withArgs(token, expectedPrice, tokenLiquidity, quoteTokenLiquidity, timestamp); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(expectedPrice); expect(oTokenLiquidity).to.equal(tokenLiquidity); @@ -1537,7 +1561,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { .to.emit(oracle, "Updated") .withArgs(token, price, expectedTokenLiquidity, expectedQuoteTokenLiquidity, timestamp); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(price); expect(oTokenLiquidity).to.equal(expectedTokenLiquidity); @@ -1570,7 +1594,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { .to.emit(oracle, "Updated") .withArgs(token, price, expectedTokenLiquidity, expectedQuoteTokenLiquidity, timestamp); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(price); expect(oTokenLiquidity).to.equal(expectedTokenLiquidity); @@ -1584,7 +1608,9 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { const quoteTokenLiquidity = ethers.utils.parseUnits("1", 18); const timestamp = (await currentBlockTimestamp()) + PERIOD * 2 + 1; - const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.observations(token); + const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.getLatestObservation( + token + ); await underlyingOracle.stubSetObservation( token, @@ -1607,7 +1633,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(poPrice); expect(oTokenLiquidity).to.equal(poTokenLiquidity); @@ -1621,7 +1647,9 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { const quoteTokenLiquidity = ethers.utils.parseUnits("1", 18); const timestamp = (await currentBlockTimestamp()) + 10; - const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.observations(token); + const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.getLatestObservation( + token + ); await underlyingOracle.stubSetObservation( token, @@ -1656,7 +1684,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(poPrice); expect(oTokenLiquidity).to.equal(poTokenLiquidity); @@ -1690,7 +1718,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { "0x4e487b710000000000000000000000000000000000000000000000000000000000000011" ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(price); expect(oTokenLiquidity).to.equal(tokenLiquidity); @@ -1720,7 +1748,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { .to.emit(oracle, "UpdateErrorWithReason") .withArgs(underlyingOracle.address, token, "REASON"); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(price); expect(oTokenLiquidity).to.equal(tokenLiquidity); @@ -1731,7 +1759,9 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { it("Shouldn't update when there aren't any valid consultations", async () => { const timestamp = (await currentBlockTimestamp()) + 10; - const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.observations(token); + const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.getLatestObservation( + token + ); await hre.timeAndMine.setTimeNextBlock(timestamp); @@ -1743,7 +1773,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(poPrice); expect(oTokenLiquidity).to.equal(poTokenLiquidity); @@ -1765,7 +1795,9 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { await currentBlockTimestamp() ); - const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.observations(token); + const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.getLatestObservation( + token + ); await hre.timeAndMine.setTimeNextBlock(timestamp); @@ -1777,7 +1809,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(poPrice); expect(oTokenLiquidity).to.equal(poTokenLiquidity); @@ -1799,7 +1831,9 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { await currentBlockTimestamp() ); - const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.observations(token); + const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.getLatestObservation( + token + ); await hre.timeAndMine.setTimeNextBlock(timestamp); @@ -1811,7 +1845,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(poPrice); expect(oTokenLiquidity).to.equal(poTokenLiquidity); @@ -1833,7 +1867,9 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { await currentBlockTimestamp() ); - const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.observations(token); + const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.getLatestObservation( + token + ); await hre.timeAndMine.setTimeNextBlock(timestamp); @@ -1845,7 +1881,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle", function () { BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(poPrice); expect(oTokenLiquidity).to.equal(poTokenLiquidity); @@ -1885,6 +1921,7 @@ describe("AggregatedOracle#update w/ 2 underlying oracle", function () { [underlyingOracle1.address, underlyingOracle2.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -1930,7 +1967,7 @@ describe("AggregatedOracle#update w/ 2 underlying oracle", function () { BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice, "Observation price").to.equal(price); expect(oTokenLiquidity, "Observation token liquidity").to.equal(totalTokenLiquidity); @@ -1976,7 +2013,7 @@ describe("AggregatedOracle#update w/ 2 underlying oracle", function () { BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(price); expect(oTokenLiquidity).to.equal(totalTokenLiquidity); @@ -2030,7 +2067,7 @@ describe("AggregatedOracle#update w/ 2 underlying oracle", function () { BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(expectedPrice); expect(oTokenLiquidity).to.equal(totalTokenLiquidity); @@ -2075,6 +2112,7 @@ describe("AggregatedOracle#update w/ 1 general underlying oracle and one token s }, ], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -2118,7 +2156,7 @@ describe("AggregatedOracle#update w/ 1 general underlying oracle and one token s BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(price); expect(oTokenLiquidity).to.equal(totalTokenLiquidity); @@ -2163,7 +2201,7 @@ describe("AggregatedOracle#update w/ 1 general underlying oracle and one token s BigNumber.from(0) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(price); expect(oTokenLiquidity).to.equal(tokenLiquidity); @@ -2203,6 +2241,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle and a minimum token liq [underlyingOracle.address], [], PERIOD, + GRANULARITY, minimumTokenLiquidityValue, minimumQuoteTokenLiquidity ); @@ -2226,7 +2265,9 @@ describe("AggregatedOracle#update w/ 1 underlying oracle and a minimum token liq await currentBlockTimestamp() ); - const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.observations(token); + const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.getLatestObservation( + token + ); await hre.timeAndMine.setTimeNextBlock(timestamp); @@ -2238,7 +2279,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle and a minimum token liq BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(poPrice); expect(oTokenLiquidity).to.equal(poTokenLiquidity); @@ -2270,7 +2311,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle and a minimum token liq BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(price); expect(oTokenLiquidity).to.equal(tokenLiquidity); @@ -2310,6 +2351,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle and a minimum quote tok [underlyingOracle.address], [], PERIOD, + GRANULARITY, minimumTokenLiquidityValue, minimumQuoteTokenLiquidity ); @@ -2333,7 +2375,9 @@ describe("AggregatedOracle#update w/ 1 underlying oracle and a minimum quote tok await currentBlockTimestamp() ); - const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.observations(token); + const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.getLatestObservation( + token + ); await hre.timeAndMine.setTimeNextBlock(timestamp); @@ -2345,7 +2389,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle and a minimum quote tok BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(poPrice); expect(oTokenLiquidity).to.equal(poTokenLiquidity); @@ -2377,7 +2421,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle and a minimum quote tok BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(price); expect(oTokenLiquidity).to.equal(tokenLiquidity); @@ -2414,6 +2458,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle and an allowed TVL dist [underlyingOracle.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -2438,7 +2483,9 @@ describe("AggregatedOracle#update w/ 1 underlying oracle and an allowed TVL dist await currentBlockTimestamp() ); - const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.observations(token); + const [poPrice, poTokenLiquidity, poQuoteTokenLiquidity, poTimestamp] = await oracle.getLatestObservation( + token + ); await hre.timeAndMine.setTimeNextBlock(timestamp); @@ -2450,7 +2497,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle and an allowed TVL dist BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(poPrice); expect(oTokenLiquidity).to.equal(poTokenLiquidity); @@ -2483,7 +2530,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle and an allowed TVL dist BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(price); expect(oTokenLiquidity).to.equal(tokenLiquidity); @@ -2516,7 +2563,7 @@ describe("AggregatedOracle#update w/ 1 underlying oracle and an allowed TVL dist BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(price); expect(oTokenLiquidity).to.equal(tokenLiquidity); @@ -2557,6 +2604,7 @@ describe("AggregatedOracle#update w/ 2 underlying oracles but one failing valida [underlyingOracle1.address, underlyingOracle2.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -2608,7 +2656,7 @@ describe("AggregatedOracle#update w/ 2 underlying oracles but one failing valida BigNumber.from(1) ); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation(token); expect(oPrice).to.equal(price); expect(oTokenLiquidity).to.equal(totalTokenLiquidity); @@ -2658,6 +2706,7 @@ describe("AggregatedOracle#sanityCheckQuoteTokenLiquidity", function () { [underlyingOracle.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, minimumQuoteTokenLiquidity ); @@ -2775,6 +2824,7 @@ describe("AggregatedOracle#sanityCheckTokenLiquidityValue", function () { [underlyingOracle.address], [], PERIOD, + GRANULARITY, minimumTokenLiquidityValue, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -2843,6 +2893,7 @@ describe("AggregatedOracle#sanityCheckTvlDistributionRatio", function () { [underlyingOracle.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -2956,6 +3007,7 @@ describe("AggregatedOracle#validateUnderlyingConsultation", function () { [underlyingOracle.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -3042,6 +3094,7 @@ describe("AggregatedOracle#calculateMaxAge", function () { [underlyingOracle.address], [], 1, // period + GRANULARITY, 0, 0 ); @@ -3062,6 +3115,7 @@ describe("AggregatedOracle#calculateMaxAge", function () { [underlyingOracle.address], [], period, // period + GRANULARITY, 0, 0 ); @@ -3092,6 +3146,7 @@ describe("AggregatedOracle#supportsInterface(interfaceId)", function () { [underlyingOracle.address], [], PERIOD, + GRANULARITY, MINIMUM_TOKEN_LIQUIDITY_VALUE, MINIMUM_QUOTE_TOKEN_LIQUIDITY ); @@ -3132,4 +3187,9 @@ describe("AggregatedOracle#supportsInterface(interfaceId)", function () { const interfaceId = await interfaceIds.iUpdateable(); expect(await oracle["supportsInterface(bytes4)"](interfaceId)).to.equal(true); }); + + it("Should support IHistoricalOracle", async () => { + const interfaceId = await interfaceIds.iHistoricalOracle(); + expect(await oracle["supportsInterface(bytes4)"](interfaceId)).to.equal(true); + }); }); diff --git a/test/oracles/periodic-accumulation-oracle.js b/test/oracles/periodic-accumulation-oracle.js index 2ece9ca..86d2cd8 100644 --- a/test/oracles/periodic-accumulation-oracle.js +++ b/test/oracles/periodic-accumulation-oracle.js @@ -8,6 +8,8 @@ const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; const PERIOD = 100; +const GRANULARITY = 1; + // Credits: https://stackoverflow.com/questions/53311809/all-possible-combinations-of-a-2d-array-in-javascript function combos(list, n = 0, result = [], current = []) { if (n === list.length) result.push(current); @@ -34,7 +36,8 @@ describe("PeriodicAccumulationOracle#constructor", async function () { [AddressZero, USDC], // liquidityAccumulator [AddressZero, USDC], // priceAccumulator [AddressZero, USDC], // quoteToken - [(BigNumber.from(1), BigNumber.from(100))], // period + [(BigNumber.from(10), BigNumber.from(100))], // period + [1, 2], // granularity ]; for (const combo of combos(testPermutations)) { @@ -44,6 +47,7 @@ describe("PeriodicAccumulationOracle#constructor", async function () { priceAccumulator: combo[1], quoteToken: combo[2], period: combo[3], + granularity: combo[4], }, }); } @@ -58,7 +62,8 @@ describe("PeriodicAccumulationOracle#constructor", async function () { args["liquidityAccumulator"], args["priceAccumulator"], args["quoteToken"], - args["period"] + args["period"], + args["granularity"] ); expect(await oracle.liquidityAccumulator()).to.equal(args["liquidityAccumulator"]); @@ -66,6 +71,7 @@ describe("PeriodicAccumulationOracle#constructor", async function () { expect(await oracle.quoteToken()).to.equal(args["quoteToken"]); expect(await oracle.quoteTokenAddress()).to.equal(args["quoteToken"]); expect(await oracle.period()).to.equal(args["period"]); + expect(await oracle.granularity()).to.equal(args["granularity"]); if (args["quoteToken"] === USDC) { expect(await oracle.quoteTokenName()).to.equal("USD Coin"); @@ -76,10 +82,28 @@ describe("PeriodicAccumulationOracle#constructor", async function () { }); it("Should revert if the period is zero", async function () { - await expect(oracleFactory.deploy(AddressZero, AddressZero, USDC, 0)).to.be.revertedWith( + await expect(oracleFactory.deploy(AddressZero, AddressZero, USDC, 0, GRANULARITY)).to.be.revertedWith( "PeriodicOracle: INVALID_PERIOD" ); }); + + it("Should revert if the granularity is zero", async function () { + await expect(oracleFactory.deploy(AddressZero, AddressZero, USDC, PERIOD, 0)).to.be.revertedWith( + "PeriodicOracle: INVALID_GRANULARITY" + ); + }); + + it("Should revert if the period is not divisible by the granularity", async function () { + const period = 5; + const granularity = 2; + + // Assert that the period is not divisible by the granularity + expect(period % granularity).to.not.equal(0); + + await expect(oracleFactory.deploy(AddressZero, AddressZero, USDC, period, granularity)).to.be.revertedWith( + "PeriodicOracle: INVALID_PERIOD_GRANULARITY" + ); + }); }); describe("PeriodicAccumulationOracle#liquidityDecimals", function () { @@ -91,7 +115,7 @@ describe("PeriodicAccumulationOracle#liquidityDecimals", function () { await la.stubSetLiquidityDecimals(liquidityDecimals); const oracleFactory = await ethers.getContractFactory("PeriodicAccumulationOracle"); - const oracle = await oracleFactory.deploy(la.address, AddressZero, USDC, 100); + const oracle = await oracleFactory.deploy(la.address, AddressZero, USDC, PERIOD, GRANULARITY); expect(await oracle.liquidityDecimals()).to.equal(liquidityDecimals); } @@ -119,7 +143,7 @@ describe("PeriodicAccumulationOracle#needsUpdate", function () { beforeEach(async () => { const oracleFactory = await ethers.getContractFactory("PeriodicAccumulationOracleStub"); - oracle = await oracleFactory.deploy(AddressZero, AddressZero, AddressZero, PERIOD); + oracle = await oracleFactory.deploy(AddressZero, AddressZero, AddressZero, PERIOD, GRANULARITY); // Time increases by 1 second with each block mined await hre.timeAndMine.setTimeIncrease(1); @@ -200,7 +224,13 @@ describe("PeriodicAccumulationOracle#canUpdate", function () { await priceAccumulator.deployed(); await liquidityAccumulator.deployed(); - oracle = await oracleFactory.deploy(liquidityAccumulator.address, priceAccumulator.address, USDC, PERIOD); + oracle = await oracleFactory.deploy( + liquidityAccumulator.address, + priceAccumulator.address, + USDC, + PERIOD, + GRANULARITY + ); accumulatorUpdateDelayTolerance = await oracle.accumulatorUpdateDelayTolerance(); }); @@ -578,7 +608,7 @@ describe("PeriodicAccumulationOracle#consultPrice(token)", function () { beforeEach(async () => { const oracleFactory = await ethers.getContractFactory("PeriodicAccumulationOracleStub"); - oracle = await oracleFactory.deploy(AddressZero, AddressZero, USDC, PERIOD); + oracle = await oracleFactory.deploy(AddressZero, AddressZero, USDC, PERIOD, GRANULARITY); }); it("Should revert when there's no observation", async () => { @@ -632,7 +662,13 @@ describe("PeriodicAccumulationOracle#consultPrice(token, maxAge = 0)", function await priceAccumulator.deployed(); await liquidityAccumulator.deployed(); - oracle = await oracleFactory.deploy(liquidityAccumulator.address, priceAccumulator.address, USDC, PERIOD); + oracle = await oracleFactory.deploy( + liquidityAccumulator.address, + priceAccumulator.address, + USDC, + PERIOD, + GRANULARITY + ); }); it("Should get the set price (=1)", async () => { @@ -655,7 +691,7 @@ describe("PeriodicAccumulationOracle#consultPrice(token, maxAge)", function () { beforeEach(async () => { const oracleFactory = await ethers.getContractFactory("PeriodicAccumulationOracleStub"); - oracle = await oracleFactory.deploy(AddressZero, AddressZero, USDC, PERIOD); + oracle = await oracleFactory.deploy(AddressZero, AddressZero, USDC, PERIOD, GRANULARITY); // Time increases by 1 second with each block mined await hre.timeAndMine.setTimeIncrease(1); @@ -785,7 +821,7 @@ describe("PeriodicAccumulationOracle#consultLiquidity(token)", function () { beforeEach(async () => { const oracleFactory = await ethers.getContractFactory("PeriodicAccumulationOracleStub"); - oracle = await oracleFactory.deploy(AddressZero, AddressZero, USDC, PERIOD); + oracle = await oracleFactory.deploy(AddressZero, AddressZero, USDC, PERIOD, GRANULARITY); }); it("Should revert when there's no observation", async () => { @@ -841,7 +877,13 @@ describe("PeriodicAccumulationOracle#consultLiquidity(token, maxAge = 0)", funct await priceAccumulator.deployed(); await liquidityAccumulator.deployed(); - oracle = await oracleFactory.deploy(liquidityAccumulator.address, priceAccumulator.address, USDC, PERIOD); + oracle = await oracleFactory.deploy( + liquidityAccumulator.address, + priceAccumulator.address, + USDC, + PERIOD, + GRANULARITY + ); }); it("Should get the set liquidities (2,3)", async () => { @@ -898,7 +940,7 @@ describe("PeriodicAccumulationOracle#consultLiquidity(token, maxAge)", function beforeEach(async () => { const oracleFactory = await ethers.getContractFactory("PeriodicAccumulationOracleStub"); - oracle = await oracleFactory.deploy(AddressZero, AddressZero, USDC, PERIOD); + oracle = await oracleFactory.deploy(AddressZero, AddressZero, USDC, PERIOD, GRANULARITY); // Time increases by 1 second with each block mined await hre.timeAndMine.setTimeIncrease(1); @@ -1060,7 +1102,7 @@ describe("PeriodicAccumulationOracle#consult(token)", function () { beforeEach(async () => { const oracleFactory = await ethers.getContractFactory("PeriodicAccumulationOracleStub"); - oracle = await oracleFactory.deploy(AddressZero, AddressZero, USDC, PERIOD); + oracle = await oracleFactory.deploy(AddressZero, AddressZero, USDC, PERIOD, GRANULARITY); }); it("Should revert when there's no observation", async () => { @@ -1118,7 +1160,13 @@ describe("PeriodicAccumulationOracle#consult(token, maxAge = 0)", function () { await priceAccumulator.deployed(); await liquidityAccumulator.deployed(); - oracle = await oracleFactory.deploy(liquidityAccumulator.address, priceAccumulator.address, USDC, PERIOD); + oracle = await oracleFactory.deploy( + liquidityAccumulator.address, + priceAccumulator.address, + USDC, + PERIOD, + GRANULARITY + ); }); it("Should get the set price (1) and liquidities (2,3)", async () => { @@ -1204,7 +1252,7 @@ describe("PeriodicAccumulationOracle#consult(token, maxAge)", function () { beforeEach(async () => { const oracleFactory = await ethers.getContractFactory("PeriodicAccumulationOracleStub"); - oracle = await oracleFactory.deploy(AddressZero, AddressZero, USDC, PERIOD); + oracle = await oracleFactory.deploy(AddressZero, AddressZero, USDC, PERIOD, GRANULARITY); // Time increases by 1 second with each block mined await hre.timeAndMine.setTimeIncrease(1); @@ -1382,7 +1430,8 @@ describe("PeriodicAccumulationOracle#update", function () { liquidityAccumulator.address, priceAccumulator.address, quoteToken.address, - 1 + 1, + GRANULARITY ); accumulatorUpdateDelayTolerance = await oracle.accumulatorUpdateDelayTolerance(); @@ -1438,7 +1487,8 @@ describe("PeriodicAccumulationOracle#update", function () { liquidityAccumulator.address, priceAccumulator.address, quoteToken.address, - 1 + 1, + GRANULARITY ); }); @@ -1537,7 +1587,8 @@ describe("PeriodicAccumulationOracle#update", function () { liquidityAccumulator.address, priceAccumulator.address, quoteToken.address, - 600 // 10 minutes + 600, // 10 minutes + GRANULARITY ); }); @@ -1641,7 +1692,8 @@ describe("PeriodicAccumulationOracle#update", function () { liquidityAccumulator.address, priceAccumulator.address, quoteToken.address, - 1 + 1, + GRANULARITY ); }); @@ -1834,7 +1886,9 @@ describe("PeriodicAccumulationOracle#update", function () { await expect(updateTx).to.not.emit(oracle, "Updated"); - const [oPrice, oTokenLiqudity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token.address); + const [oPrice, oTokenLiqudity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation( + token.address + ); expect(oPrice, "Observation price").to.equal(fakePrice); expect(oTokenLiqudity, "Observation token liquidity").to.equal(fakeTokenLiquidity); @@ -1907,7 +1961,7 @@ describe("PeriodicAccumulationOracle#update", function () { await oracle.liquidityAccumulations(token.address); // Record old observation - const [oldPrice, oldTokenLiquidity, oldQuoteTokenLiquidity, oldTimestamp] = await oracle.observations( + const [oldPrice, oldTokenLiquidity, oldQuoteTokenLiquidity, oldTimestamp] = await oracle.getLatestObservation( token.address ); @@ -1917,7 +1971,9 @@ describe("PeriodicAccumulationOracle#update", function () { // Shouldn't emit Updated await expect(updateTx).to.not.emit(oracle, "Updated"); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token.address); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation( + token.address + ); // Observation should not update expect(oPrice, "Observation price").to.equal(oldPrice); @@ -1996,7 +2052,7 @@ describe("PeriodicAccumulationOracle#update", function () { await oracle.liquidityAccumulations(token.address); // Record old observation - const [oldPrice, oldTokenLiquidity, oldQuoteTokenLiquidity, oldTimestamp] = await oracle.observations( + const [oldPrice, oldTokenLiquidity, oldQuoteTokenLiquidity, oldTimestamp] = await oracle.getLatestObservation( token.address ); @@ -2014,7 +2070,9 @@ describe("PeriodicAccumulationOracle#update", function () { // Shouldn't emit Updated await expect(updateTx).to.not.emit(oracle, "Updated"); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token.address); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation( + token.address + ); // Observation should not update expect(oPrice, "Observation price").to.equal(oldPrice); @@ -2093,7 +2151,7 @@ describe("PeriodicAccumulationOracle#update", function () { await oracle.liquidityAccumulations(token.address); // Record old observation - const [oldPrice, oldTokenLiquidity, oldQuoteTokenLiquidity, oldTimestamp] = await oracle.observations( + const [oldPrice, oldTokenLiquidity, oldQuoteTokenLiquidity, oldTimestamp] = await oracle.getLatestObservation( token.address ); @@ -2106,7 +2164,9 @@ describe("PeriodicAccumulationOracle#update", function () { // Shouldn't emit Updated await expect(updateTx).to.not.emit(oracle, "Updated"); - const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.observations(token.address); + const [oPrice, oTokenLiquidity, oQuoteTokenLiquidity, oTimestamp] = await oracle.getLatestObservation( + token.address + ); // Observation should not update expect(oPrice, "Observation price").to.equal(oldPrice); @@ -2148,7 +2208,7 @@ describe("PeriodicAccumulationOracle#update", function () { // Shouldn't emite Updated since the oracle doesn't have enough info to calculate price await expect(updateTx).to.not.emit(oracle, "Updated"); - const [, , , oTimestamp] = await oracle.observations(token.address); + const [, , , oTimestamp] = await oracle.getLatestObservation(token.address); // Observation timestamp should not update since the oracle doesn't have enough info to calculate price expect(oTimestamp, "Observation timestamp").to.equal(0); @@ -2178,13 +2238,15 @@ describe("PeriodicAccumulationOracle#update", function () { expect(await oracle.callStatic.update(ethers.utils.hexZeroPad(token.address, 32))).to.equal(false); - const [pPrice, pTokenLiqudity, pQuoteTokenLiquidity, pTimestamp] = await oracle.observations(token.address); + const [pPrice, pTokenLiqudity, pQuoteTokenLiquidity, pTimestamp] = await oracle.getLatestObservation( + token.address + ); const updateTxPromise = oracle.update(ethers.utils.hexZeroPad(token.address, 32)); await expect(updateTxPromise).to.not.emit(oracle, "Updated"); - const [price, tokenLiqudity, quoteTokenLiquidity, timestamp] = await oracle.observations(token.address); + const [price, tokenLiqudity, quoteTokenLiquidity, timestamp] = await oracle.getLatestObservation(token.address); // Verify the current observation hasn't changed expect(price).to.equal(pPrice); @@ -2228,7 +2290,7 @@ describe("PeriodicAccumulationOracle#update", function () { const updateReceipt = await oracle.update(ethers.utils.hexZeroPad(token.address, 32)); - [price, tokenLiquidity, quoteTokenLiquidity, timestamp] = await oracle.observations(token.address); + [price, tokenLiquidity, quoteTokenLiquidity, timestamp] = await oracle.getLatestObservation(token.address); // Verify that the observation matches what's expected { @@ -2324,7 +2386,7 @@ describe("PeriodicAccumulationOracle#supportsInterface(interfaceId)", function ( const oracleFactory = await ethers.getContractFactory("PeriodicAccumulationOracleStub"); const interfaceIdsFactory = await ethers.getContractFactory("InterfaceIds"); - oracle = await oracleFactory.deploy(AddressZero, AddressZero, AddressZero, PERIOD); + oracle = await oracleFactory.deploy(AddressZero, AddressZero, AddressZero, PERIOD, GRANULARITY); interfaceIds = await interfaceIdsFactory.deploy(); }); From 8cf622b4f18fb38f920ca1556715d6a935bfd3f7 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Thu, 2 Feb 2023 20:46:56 -0800 Subject: [PATCH 13/32] Add tests for AggregatedOracle IHistoricalOracle implementation --- .../test/oracles/AggregatedOracleStub.sol | 25 + test/oracles/aggregated-oracle.js | 723 ++++++++++++++++++ 2 files changed, 748 insertions(+) diff --git a/contracts/test/oracles/AggregatedOracleStub.sol b/contracts/test/oracles/AggregatedOracleStub.sol index 8a93adf..8ef2541 100644 --- a/contracts/test/oracles/AggregatedOracleStub.sol +++ b/contracts/test/oracles/AggregatedOracleStub.sol @@ -53,6 +53,31 @@ contract AggregatedOracleStub is AggregatedOracle { overrideValidateUnderlyingConsultation(true, true); // Skip validation by default } + function stubPush( + address token, + uint112 price, + uint112 tokenLiquidity, + uint112 quoteTokenLiquidity, + uint32 timestamp + ) public { + ObservationLibrary.Observation memory observation; + + observation.price = price; + observation.tokenLiquidity = tokenLiquidity; + observation.quoteTokenLiquidity = quoteTokenLiquidity; + observation.timestamp = timestamp; + + push(token, observation); + } + + function stubInitializeBuffers(address token) public { + initializeBuffers(token); + } + + function stubInitialCardinality() public view returns (uint256) { + return _initialCardinality; + } + function stubSetLiquidityDecimals(uint8 decimals) public { config.liquidityDecimalsOverridden = true; config.liquidityDecimals = decimals; diff --git a/test/oracles/aggregated-oracle.js b/test/oracles/aggregated-oracle.js index d397671..99d5b24 100644 --- a/test/oracles/aggregated-oracle.js +++ b/test/oracles/aggregated-oracle.js @@ -3193,3 +3193,726 @@ describe("AggregatedOracle#supportsInterface(interfaceId)", function () { expect(await oracle["supportsInterface(bytes4)"](interfaceId)).to.equal(true); }); }); + +describe("AggregatedOracle - IHistoricalOracle implementation", function () { + var oracle; + var underlyingOracle; + + beforeEach(async () => { + const mockOracleFactory = await ethers.getContractFactory("MockOracle"); + const oracleFactory = await ethers.getContractFactory("AggregatedOracleStub"); + + underlyingOracle = await mockOracleFactory.deploy(USDC); + await underlyingOracle.deployed(); + + oracle = await oracleFactory.deploy( + "USD Coin", + USDC, + "USDC", + 6, // quote token decimals + 0, // liquidity decimals + [underlyingOracle.address], + [], + PERIOD, + GRANULARITY, + MINIMUM_TOKEN_LIQUIDITY_VALUE, + MINIMUM_QUOTE_TOKEN_LIQUIDITY + ); + }); + + describe("AggregatedOracle#initializeBuffers", function () { + it("Can't be called twice", async function () { + await oracle.stubInitializeBuffers(GRT); + + await expect(oracle.stubInitializeBuffers(GRT)).to.be.revertedWith("AggregatedOracle: ALREADY_INITIALIZED"); + }); + }); + + describe("AggregatedOracle#setObservationCapacity", function () { + it("Should revert if the amount is less than the existing capacity", async function () { + await oracle.setObservationsCapacity(GRT, 4); + + await expect(oracle.setObservationsCapacity(GRT, 2)).to.be.revertedWith( + "AggregatedOracle: CAPACITY_CANNOT_BE_DECREASED" + ); + }); + + it("Should revert if the amount is 0", async function () { + await expect(oracle.setObservationsCapacity(GRT, 0)).to.be.revertedWith( + "AggregatedOracle: CAPACITY_CANNOT_BE_DECREASED" + ); + }); + + it("Should revert if the amount is larger than the maximum capacity", async function () { + await expect(oracle.setObservationsCapacity(GRT, 65536)).to.be.revertedWith( + "AggregatedOracle: CAPACITY_TOO_LARGE" + ); + }); + + it("Should emit an event when the capacity is changed", async function () { + const amount = 20; + + const initialAmount = await oracle.getObservationsCapacity(GRT); + + // Sanity check that the new amount is greater than the initial amount + expect(amount).to.be.greaterThan(initialAmount.toNumber()); + + await expect(oracle.setObservationsCapacity(GRT, amount)) + .to.emit(oracle, "ObservationCapacityIncreased") + .withArgs(GRT, initialAmount, amount); + }); + + it("Should not emit an event when the capacity is not changed (with default capacity)", async function () { + const initialAmount = await oracle.getObservationsCapacity(GRT); + + await expect(oracle.setObservationsCapacity(GRT, initialAmount)).to.not.emit( + oracle, + "ObservationCapacityIncreased" + ); + }); + + it("Should not emit an event when the capacity is not changed (with non-default capacity)", async function () { + const initialAmount = await oracle.getObservationsCapacity(GRT); + const amount = 20; + + // Sanity check that the new amount is greater than the initial amount + expect(amount).to.be.greaterThan(initialAmount.toNumber()); + + await oracle.setObservationsCapacity(GRT, amount); + + // Sanity check that the capacity is now the new amount + expect(await oracle.getObservationsCapacity(GRT)).to.equal(amount); + + // Try again to set it to the same amount + await expect(oracle.setObservationsCapacity(GRT, amount)).to.not.emit( + oracle, + "ObservationCapacityIncreased" + ); + }); + + it("Should update the capacity", async function () { + const amount = 20; + + // Sanity check that the new amount is greater than the initial amount + expect(amount).to.be.greaterThan((await oracle.getObservationsCapacity(GRT)).toNumber()); + + await oracle.setObservationsCapacity(GRT, amount); + + expect(await oracle.getObservationsCapacity(GRT)).to.equal(amount); + }); + + it("Added capacity should not be filled until our latest observation is beside an uninitialized observation", async function () { + const workingCapacity = 6; + + // Set the capacity to the working capacity + await oracle.setObservationsCapacity(GRT, workingCapacity); + + // Push workingCapacity + 1 observations so that the buffer is full and the latest observation is at the start of the buffer + for (let i = 0; i < workingCapacity + 1; ++i) { + await oracle.stubPush(GRT, 1, 1, 1, 1); + } + + // Sanity check that the buffer is full + expect(await oracle.getObservationsCount(GRT)).to.equal(workingCapacity); + + // Increase the capacity by 1 + await oracle.setObservationsCapacity(GRT, workingCapacity + 1); + + // We should need to push workingCapacity observations before the new capacity is filled + for (let i = 0; i < workingCapacity - 1; ++i) { + await oracle.stubPush(GRT, 1, 1, 1, 1); + + // Sanity check that the buffer is still not full + expect(await oracle.getObservationsCount(GRT)).to.equal(workingCapacity); + } + + // Push one more observation. This should fill the new capacity + await oracle.stubPush(GRT, 1, 1, 1, 1); + + // Check that the buffer is now full + expect(await oracle.getObservationsCount(GRT)).to.equal(workingCapacity + 1); + }); + }); + + describe("AggregatedOracle#getObservationsCapacity", function () { + it("Should return the default capacity when the buffer is uninitialized", async function () { + const initialCapacity = await oracle.stubInitialCardinality(); + + expect(await oracle.getObservationsCapacity(GRT)).to.equal(initialCapacity); + }); + + it("Should return the capacity when the buffer is initialized", async function () { + await oracle.stubInitializeBuffers(GRT); + + const initialCapacity = await oracle.stubInitialCardinality(); + + expect(await oracle.getObservationsCapacity(GRT)).to.equal(initialCapacity); + }); + + it("Should return the capacity after the buffer has been resized", async function () { + const amount = 20; + + // Sanity check that the new amount is greater than the initial amount + expect(amount).to.be.greaterThan((await oracle.getObservationsCapacity(GRT)).toNumber()); + + await oracle.setObservationsCapacity(GRT, amount); + + expect(await oracle.getObservationsCapacity(GRT)).to.equal(amount); + }); + }); + + describe("AggregatedOracle#getObservationsCount", function () { + it("Should return 0 when the buffer is uninitialized", async function () { + expect(await oracle.getObservationsCount(GRT)).to.equal(0); + }); + + it("Should return 0 when the buffer is initialized but empty", async function () { + await oracle.stubInitializeBuffers(GRT); + + expect(await oracle.getObservationsCount(GRT)).to.equal(0); + }); + + it("Increasing capacity should not change the observations count", async function () { + const initialAmount = 4; + + await oracle.setObservationsCapacity(GRT, initialAmount); + + // Push 2 observations + await oracle.stubPush(GRT, 1, 1, 1, 1); + await oracle.stubPush(GRT, 1, 1, 1, 1); + + // Sanity check that the observations count is 2 + expect(await oracle.getObservationsCount(GRT)).to.equal(2); + + // Increase the capacity by 1 + await oracle.setObservationsCapacity(GRT, initialAmount + 1); + + // The observations count should still be 2 + expect(await oracle.getObservationsCount(GRT)).to.equal(2); + }); + + it("Should be limited by the capacity", async function () { + const capacity = 6; + + await oracle.setObservationsCapacity(GRT, capacity); + + // Push capacity + 1 observations + for (let i = 0; i < capacity + 1; ++i) { + await oracle.stubPush(GRT, 1, 1, 1, 1); + } + + // The observations count should be limited by the capacity + expect(await oracle.getObservationsCount(GRT)).to.equal(capacity); + }); + }); + + describe("AggregatedOracle#getObservations(token, amount, offset, increment)", function () { + it("Should return an empty array when amount is 0", async function () { + // Push 1 observation + await oracle.stubPush(GRT, 1, 1, 1, 1); + + const observations = await oracle["getObservations(address,uint256,uint256,uint256)"](GRT, 0, 0, 1); + + expect(observations.length).to.equal(0); + }); + + it("Should revert if the offset equals the number of observations", async function () { + // Push 1 observation + await oracle.stubPush(GRT, 1, 1, 1, 1); + + await expect(oracle["getObservations(address,uint256,uint256,uint256)"](GRT, 1, 1, 1)).to.be.revertedWith( + "AggregatedOracle: INSUFFICIENT_DATA" + ); + }); + + it("Should revert if the offset equals the number of observations but is less than the capacity", async function () { + const capacity = 6; + + await oracle.setObservationsCapacity(GRT, capacity); + + // Sanity check the capacity + expect(await oracle.getObservationsCapacity(GRT)).to.equal(capacity); + + // Push 1 observation + await oracle.stubPush(GRT, 1, 1, 1, 1); + + await expect(oracle["getObservations(address,uint256,uint256,uint256)"](GRT, 1, 1, 1)).to.be.revertedWith( + "AggregatedOracle: INSUFFICIENT_DATA" + ); + }); + + it("Should revert if the amount exceeds the number of observations", async function () { + // Push 1 observation + await oracle.stubPush(GRT, 1, 1, 1, 1); + + await expect(oracle["getObservations(address,uint256,uint256,uint256)"](GRT, 2, 0, 1)).to.be.revertedWith( + "AggregatedOracle: INSUFFICIENT_DATA" + ); + }); + + it("Should revert if the amount exceeds the number of observations but is less than the capacity", async function () { + const capacity = 6; + const amountToGet = 2; + + await oracle.setObservationsCapacity(GRT, capacity); + + // Sanity check the capacity + expect(await oracle.getObservationsCapacity(GRT)).to.equal(capacity); + + // Push 1 observation + await oracle.stubPush(GRT, 1, 1, 1, 1); + + // Sanity check that the amount to get is less than the capacity + expect(amountToGet).to.be.lessThan(capacity); + + await expect( + oracle["getObservations(address,uint256,uint256,uint256)"](GRT, amountToGet, 0, 1) + ).to.be.revertedWith("AggregatedOracle: INSUFFICIENT_DATA"); + }); + + it("Should revert if the amount and offset exceed the number of observations", async function () { + const capacity = 2; + const amountToGet = 2; + + await oracle.setObservationsCapacity(GRT, capacity); + + // Sanity check the capacity + expect(await oracle.getObservationsCapacity(GRT)).to.equal(capacity); + + // Push 2 observation + await oracle.stubPush(GRT, 1, 1, 1, 1); + await oracle.stubPush(GRT, 1, 1, 1, 1); + + await expect( + oracle["getObservations(address,uint256,uint256,uint256)"](GRT, amountToGet, 1, 1) + ).to.be.revertedWith("AggregatedOracle: INSUFFICIENT_DATA"); + }); + + it("Should revert if the amount and offset exceed the number of observations but is less than the capacity", async function () { + const capacity = 6; + const amountToGet = 2; + + await oracle.setObservationsCapacity(GRT, capacity); + + // Sanity check the capacity + expect(await oracle.getObservationsCapacity(GRT)).to.equal(capacity); + + // Push 2 observation + await oracle.stubPush(GRT, 1, 1, 1, 1); + await oracle.stubPush(GRT, 1, 1, 1, 1); + + await expect( + oracle["getObservations(address,uint256,uint256,uint256)"](GRT, amountToGet, 1, 1) + ).to.be.revertedWith("AggregatedOracle: INSUFFICIENT_DATA"); + }); + + it("Should revert if the increment and amount exceeds the number of observations", async function () { + const capacity = 2; + const amountToGet = 2; + const offset = 0; + const increment = 2; + + await oracle.setObservationsCapacity(GRT, capacity); + + // Sanity check the capacity + expect(await oracle.getObservationsCapacity(GRT)).to.equal(capacity); + + // Push 2 observation + await oracle.stubPush(GRT, 1, 1, 1, 1); + await oracle.stubPush(GRT, 1, 1, 1, 1); + + await expect( + oracle["getObservations(address,uint256,uint256,uint256)"](GRT, amountToGet, offset, increment) + ).to.be.revertedWith("AggregatedOracle: INSUFFICIENT_DATA"); + }); + + it("Should revert if the increment and amount exceeds the number of observations but is less than the capacity", async function () { + const capacity = 6; + const amountToGet = 2; + const offset = 0; + const increment = 2; + + await oracle.setObservationsCapacity(GRT, capacity); + + // Sanity check the capacity + expect(await oracle.getObservationsCapacity(GRT)).to.equal(capacity); + + // Push 2 observation + await oracle.stubPush(GRT, 1, 1, 1, 1); + await oracle.stubPush(GRT, 1, 1, 1, 1); + + await expect( + oracle["getObservations(address,uint256,uint256,uint256)"](GRT, amountToGet, offset, increment) + ).to.be.revertedWith("AggregatedOracle: INSUFFICIENT_DATA"); + }); + + it("Should revert if the increment, amount, and offset exceeds the number of observations", async function () { + const capacity = 2; + const amountToGet = 2; + const offset = 1; + const increment = 2; + + await oracle.setObservationsCapacity(GRT, capacity); + + // Sanity check the capacity + expect(await oracle.getObservationsCapacity(GRT)).to.equal(capacity); + + // Push 3 observation + await oracle.stubPush(GRT, 1, 1, 1, 1); + await oracle.stubPush(GRT, 1, 1, 1, 1); + await oracle.stubPush(GRT, 1, 1, 1, 1); + + await expect( + oracle["getObservations(address,uint256,uint256,uint256)"](GRT, amountToGet, offset, increment) + ).to.be.revertedWith("AggregatedOracle: INSUFFICIENT_DATA"); + }); + + it("Should revert if the increment, amount, and offset exceeds the number of observations but is less than the capacity", async function () { + const capacity = 6; + const amountToGet = 2; + const offset = 1; + const increment = 2; + + await oracle.setObservationsCapacity(GRT, capacity); + + // Sanity check the capacity + expect(await oracle.getObservationsCapacity(GRT)).to.equal(capacity); + + // Push 3 observation + await oracle.stubPush(GRT, 1, 1, 1, 1); + await oracle.stubPush(GRT, 1, 1, 1, 1); + await oracle.stubPush(GRT, 1, 1, 1, 1); + + await expect( + oracle["getObservations(address,uint256,uint256,uint256)"](GRT, amountToGet, offset, increment) + ).to.be.revertedWith("AggregatedOracle: INSUFFICIENT_DATA"); + }); + + it("Should return the latest observation many times when increment is 0", async function () { + const capacity = 2; + const amountToGet = 2; + const offset = 0; + const increment = 0; + + await oracle.setObservationsCapacity(GRT, capacity); + + // Sanity check the capacity + expect(await oracle.getObservationsCapacity(GRT)).to.equal(capacity); + + // Push 2 observation + await oracle.stubPush(GRT, 1, 1, 1, 1); + await oracle.stubPush(GRT, 2, 2, 2, 2); + + const observations = await oracle["getObservations(address,uint256,uint256,uint256)"]( + GRT, + amountToGet, + offset, + increment + ); + + expect(observations.length).to.equal(amountToGet); + + for (let i = 0; i < amountToGet; ++i) { + expect(observations[i].price).to.equal(2); + expect(observations[i].tokenLiquidity).to.equal(2); + expect(observations[i].quoteTokenLiquidity).to.equal(2); + expect(observations[i].timestamp).to.equal(2); + } + }); + + async function pushAndCheckObservations(capacity, amountToGet, offset, increment, observationsToPush) { + await oracle.setObservationsCapacity(GRT, capacity); + + // Sanity check the capacity + expect(await oracle.getObservationsCapacity(GRT)).to.equal(capacity); + + for (let i = 0; i < observationsToPush; i++) { + await oracle.stubPush(GRT, i, i, i, i); + } + + const observations = await oracle["getObservations(address,uint256,uint256,uint256)"]( + GRT, + amountToGet, + offset, + increment + ); + + expect(observations.length).to.equal(amountToGet); + + for (let i = 0; i < amountToGet; ++i) { + // The latest observation is at index 0 and will have the highest expected values + // The following observations will have the expected values decrementing by 1 + const expected = observationsToPush - i * increment - 1 - offset; + + expect(observations[i].price).to.equal(expected); + expect(observations[i].tokenLiquidity).to.equal(expected); + expect(observations[i].quoteTokenLiquidity).to.equal(expected); + expect(observations[i].timestamp).to.equal(expected); + } + } + + describe("An increment of 1", function () { + describe("An offset of 0", function () { + describe("The latest observation is at index 0", function () { + it("Should return the observations in order", async function () { + const capacity = 6; + const amountToGet = 6; + const offset = 0; + const increment = 1; + + // Push capacity + 1 observations so that the latest observation is at index 0 + const observationsToPush = capacity + 1; + + await pushAndCheckObservations(capacity, amountToGet, offset, increment, observationsToPush); + }); + }); + + describe("The latest observation is at index n-1", function () { + it("Should return the observations in order", async function () { + const capacity = 6; + const amountToGet = 6; + const offset = 0; + const increment = 1; + + // Push capacity observations so that the latest observation is at index n-1 + const observationsToPush = capacity; + + await pushAndCheckObservations(capacity, amountToGet, offset, increment, observationsToPush); + }); + }); + }); + + describe("An offset of 1", function () { + describe("The latest observation is at index 0", function () { + it("Should return the observations in order", async function () { + const capacity = 6; + const amountToGet = 5; + const offset = 1; + const increment = 1; + + // Push capacity + 1 observations so that the latest observation is at index 0 + const observationsToPush = capacity + 1; + + await pushAndCheckObservations(capacity, amountToGet, offset, increment, observationsToPush); + }); + }); + + describe("The latest observation is at index n-1", function () { + it("Should return the observations in order", async function () { + const capacity = 6; + const amountToGet = 5; + const offset = 1; + const increment = 1; + + // Push capacity observations so that the latest observation is at index n-1 + const observationsToPush = capacity; + + await pushAndCheckObservations(capacity, amountToGet, offset, increment, observationsToPush); + }); + }); + }); + }); + + describe("An increment of 2", function () { + describe("An offset of 0", function () { + describe("The latest observation is at index 0", function () { + it("Should return the observations in order", async function () { + const capacity = 6; + const amountToGet = 3; + const offset = 0; + const increment = 2; + + // Push capacity + 1 observations so that the latest observation is at index 0 + const observationsToPush = capacity + 1; + + await pushAndCheckObservations(capacity, amountToGet, offset, increment, observationsToPush); + }); + }); + + describe("The latest observation is at index n-1", function () { + it("Should return the observations in order", async function () { + const capacity = 6; + const amountToGet = 3; + const offset = 0; + const increment = 2; + + // Push capacity observations so that the latest observation is at index n-1 + const observationsToPush = capacity; + + await pushAndCheckObservations(capacity, amountToGet, offset, increment, observationsToPush); + }); + }); + }); + + describe("An offset of 1", function () { + describe("The latest observation is at index 0", function () { + it("Should return the observations in order", async function () { + const capacity = 6; + const amountToGet = 2; + const offset = 1; + const increment = 2; + + // Push capacity + 1 observations so that the latest observation is at index 0 + const observationsToPush = capacity + 1; + + await pushAndCheckObservations(capacity, amountToGet, offset, increment, observationsToPush); + }); + }); + + describe("The latest observation is at index n-1", function () { + it("Should return the observations in order", async function () { + const capacity = 6; + const amountToGet = 2; + const offset = 1; + const increment = 2; + + // Push capacity observations so that the latest observation is at index n-1 + const observationsToPush = capacity; + + await pushAndCheckObservations(capacity, amountToGet, offset, increment, observationsToPush); + }); + }); + }); + }); + }); + + describe("AggregatedOracle#getObservations(token, amount)", function () { + async function pushAndCheckObservations(capacity, amountToGet, offset, increment, observationsToPush) { + await oracle.setObservationsCapacity(GRT, capacity); + + // Sanity check the capacity + expect(await oracle.getObservationsCapacity(GRT)).to.equal(capacity); + + for (let i = 0; i < observationsToPush; i++) { + await oracle.stubPush(GRT, i, i, i, i); + } + + const observations = await oracle["getObservations(address,uint256)"](GRT, amountToGet); + + expect(observations.length).to.equal(amountToGet); + + for (let i = 0; i < amountToGet; ++i) { + // The latest observation is at index 0 and will have the highest expected values + // The following observations will have the expected values decrementing by 1 + const expected = observationsToPush - i * increment - 1 - offset; + + expect(observations[i].price).to.equal(expected); + expect(observations[i].tokenLiquidity).to.equal(expected); + expect(observations[i].quoteTokenLiquidity).to.equal(expected); + expect(observations[i].timestamp).to.equal(expected); + } + } + + it("Default offset is 0 and increment is 1", async function () { + const capacity = 6; + const amountToGet = 6; + + // Push capacity observations so that the latest observation is at index n-1 + const observationsToPush = capacity; + + await pushAndCheckObservations(capacity, amountToGet, 0, 1, observationsToPush); + }); + }); + + describe("AggregatedOracle#getObservationAt", function () { + it("Should revert if the buffer is uninitialized", async function () { + await expect(oracle.getObservationAt(GRT, 0)).to.be.revertedWith("AggregatedOracle: INVALID_INDEX"); + }); + + it("Should revert if the buffer is initialized but empty", async function () { + await oracle.stubInitializeBuffers(GRT); + + await expect(oracle.getObservationAt(GRT, 0)).to.be.revertedWith("AggregatedOracle: INVALID_INDEX"); + }); + + it("Should revert if the index exceeds the number of observations with a full buffer", async function () { + const capacity = 6; + + await oracle.setObservationsCapacity(GRT, capacity); + + // Push capacity observations + for (let i = 0; i < capacity; ++i) { + await oracle.stubPush(GRT, 1, 1, 1, 1); + } + + await expect(oracle.getObservationAt(GRT, capacity)).to.be.revertedWith("AggregatedOracle: INVALID_INDEX"); + }); + + it("Should revert if the index exceeds the number of observations but is within the capacity", async function () { + const capacity = 6; + + await oracle.setObservationsCapacity(GRT, capacity); + + // Push capacity - 1 observations + for (let i = 0; i < capacity - 1; ++i) { + await oracle.stubPush(GRT, 1, 1, 1, 1); + } + + await expect(oracle.getObservationAt(GRT, capacity - 1)).to.be.revertedWith( + "AggregatedOracle: INVALID_INDEX" + ); + }); + + it("Should return the latest observation when index = 0", async function () { + await oracle.setObservationsCapacity(GRT, 2); + + // Push capacity observations + await oracle.stubPush(GRT, 1, 1, 1, 1); + await oracle.stubPush(GRT, 2, 2, 2, 2); + + const observation = await oracle.getObservationAt(GRT, 0); + + expect(observation.price).to.equal(2); + expect(observation.tokenLiquidity).to.equal(2); + expect(observation.quoteTokenLiquidity).to.equal(2); + expect(observation.timestamp).to.equal(2); + }); + + it("Should return the latest observation when index = 0 and the start was just overwritten", async function () { + await oracle.setObservationsCapacity(GRT, 2); + + // Push capacity + 1 observations + await oracle.stubPush(GRT, 1, 1, 1, 1); + await oracle.stubPush(GRT, 2, 2, 2, 2); + await oracle.stubPush(GRT, 3, 3, 3, 3); + + const observation = await oracle.getObservationAt(GRT, 0); + + expect(observation.price).to.equal(3); + expect(observation.tokenLiquidity).to.equal(3); + expect(observation.quoteTokenLiquidity).to.equal(3); + expect(observation.timestamp).to.equal(3); + }); + + it("Should return the correct observation when index = 1 and the latest observation is at the start of the buffer", async function () { + await oracle.setObservationsCapacity(GRT, 2); + + // Push capacity + 1 observations + await oracle.stubPush(GRT, 1, 1, 1, 1); + await oracle.stubPush(GRT, 2, 2, 2, 2); + await oracle.stubPush(GRT, 3, 3, 3, 3); + + const observation = await oracle.getObservationAt(GRT, 1); + + expect(observation.price).to.equal(2); + expect(observation.tokenLiquidity).to.equal(2); + expect(observation.quoteTokenLiquidity).to.equal(2); + expect(observation.timestamp).to.equal(2); + }); + + it("Should return the correct observation when index = 1 and the latest observation is at the end of the buffer", async function () { + await oracle.setObservationsCapacity(GRT, 2); + + // Push capacity observations + await oracle.stubPush(GRT, 1, 1, 1, 1); + await oracle.stubPush(GRT, 2, 2, 2, 2); + + const observation = await oracle.getObservationAt(GRT, 1); + + expect(observation.price).to.equal(1); + expect(observation.tokenLiquidity).to.equal(1); + expect(observation.quoteTokenLiquidity).to.equal(1); + expect(observation.timestamp).to.equal(1); + }); + }); +}); From 20cf909ada2d2e2d73f9ac2e4e928a2e9883a921 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Fri, 3 Feb 2023 16:18:38 -0800 Subject: [PATCH 14/32] Hide AggregatedOracle buffers - IHistoricalOracle implementation should be used instead --- contracts/oracles/AggregatedOracle.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/oracles/AggregatedOracle.sol b/contracts/oracles/AggregatedOracle.sol index 82a0c07..35ebe26 100644 --- a/contracts/oracles/AggregatedOracle.sol +++ b/contracts/oracles/AggregatedOracle.sol @@ -38,9 +38,9 @@ contract AggregatedOracle is IAggregatedOracle, IHistoricalOracle, PeriodicOracl uint16 maxSize; } - mapping(address => BufferMetadata) public observationBufferMetadata; + mapping(address => BufferMetadata) internal observationBufferMetadata; - mapping(address => ObservationLibrary.Observation[]) public observationBuffers; + mapping(address => ObservationLibrary.Observation[]) internal observationBuffers; /// @notice The minimum quote token denominated value of the token liquidity, scaled by this oracle's liquidity /// decimals, required for all underlying oracles to be considered valid and thus included in the aggregation. From 545dcb622844e24fcbf10711c74bf6d318080507 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Fri, 3 Feb 2023 16:21:54 -0800 Subject: [PATCH 15/32] Promote PeriodicAccumulationOracle buffer metadata variables to uint16 - There's still plenty of metadata space for future use at the same storage cost - This allows for up to 65535 accumulations rather than 256 --- contracts/oracles/PeriodicAccumulationOracle.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/oracles/PeriodicAccumulationOracle.sol b/contracts/oracles/PeriodicAccumulationOracle.sol index 0a9c79c..0ea2e19 100644 --- a/contracts/oracles/PeriodicAccumulationOracle.sol +++ b/contracts/oracles/PeriodicAccumulationOracle.sol @@ -17,9 +17,9 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, using SafeCast for uint256; struct BufferMetadata { - uint8 start; - uint8 end; - uint8 size; + uint16 start; + uint16 end; + uint16 size; } address public immutable override liquidityAccumulator; @@ -195,7 +195,7 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, return false; } - meta.end = (meta.end + 1) % uint8(granularity); + meta.end = (meta.end + 1) % uint16(granularity); } priceAccumulationBuffers[token][meta.end] = priceAccumulation; @@ -205,7 +205,7 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, meta.size++; } else { // start was just overwritten - meta.start = (meta.start + 1) % uint8(granularity); + meta.start = (meta.start + 1) % uint16(granularity); } return true; From f0d2e848261f76820e39de4a1d031d3194a20304 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Fri, 3 Feb 2023 16:33:41 -0800 Subject: [PATCH 16/32] Replace use of granularity with metadata maxSize in PeriodicAccumulationOracle - Use the same logic as AggregatedOracle for increased consistency - Allows for later expansion to support storing historical accumulations that can be used to calculate time-weighted averages of various lengths --- contracts/oracles/PeriodicAccumulationOracle.sol | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/contracts/oracles/PeriodicAccumulationOracle.sol b/contracts/oracles/PeriodicAccumulationOracle.sol index 0ea2e19..b423cc7 100644 --- a/contracts/oracles/PeriodicAccumulationOracle.sol +++ b/contracts/oracles/PeriodicAccumulationOracle.sol @@ -20,6 +20,7 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, uint16 start; uint16 end; uint16 size; + uint16 maxSize; } address public immutable override liquidityAccumulator; @@ -142,6 +143,7 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, meta.start = 0; meta.end = 0; meta.size = 0; + meta.maxSize = uint16(granularity); } function push( @@ -152,8 +154,10 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, BufferMetadata storage meta = accumulationBufferMetadata[token]; if (meta.size == 0) { - // Initialize the buffers - initializeBuffers(token); + if (meta.maxSize == 0) { + // Initialize the buffers + initializeBuffers(token); + } } else { // We have multiple accumulations now @@ -195,17 +199,18 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, return false; } - meta.end = (meta.end + 1) % uint16(granularity); + meta.end = (meta.end + 1) % meta.maxSize; } priceAccumulationBuffers[token][meta.end] = priceAccumulation; liquidityAccumulationBuffers[token][meta.end] = liquidityAccumulation; - if (meta.size < granularity) { + if (meta.size < meta.maxSize && meta.end == meta.size) { + // We are at the end of the array and we have not yet filled it meta.size++; } else { // start was just overwritten - meta.start = (meta.start + 1) % uint16(granularity); + meta.start = (meta.start + 1) % meta.size; } return true; From 2ed35d260c914e311c2b044e9ac469d568d2c316 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Sat, 4 Feb 2023 14:36:50 -0800 Subject: [PATCH 17/32] Fix problems in PeriodicAccumulationOracle - We should have done freshness checking against the latest accumulations rather than the oldest --- .../oracles/PeriodicAccumulationOracle.sol | 88 +++++++++++-------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/contracts/oracles/PeriodicAccumulationOracle.sol b/contracts/oracles/PeriodicAccumulationOracle.sol index b423cc7..9651b37 100644 --- a/contracts/oracles/PeriodicAccumulationOracle.sol +++ b/contracts/oracles/PeriodicAccumulationOracle.sol @@ -159,44 +159,56 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, initializeBuffers(token); } } else { - // We have multiple accumulations now - - uint256 firstPriceAccumulationTime = priceAccumulationBuffers[token][meta.start].timestamp; - uint256 pricePeriodTimeElapsed = priceAccumulation.timestamp - firstPriceAccumulationTime; - - uint256 firstLiquidityAccumulationTime = liquidityAccumulationBuffers[token][meta.start].timestamp; - uint256 liquidityPeriodTimeElapsed = liquidityAccumulation.timestamp - firstLiquidityAccumulationTime; - - uint256 maxUpdateGap = period + updateDelayTolerance(); - - if ( - meta.size == granularity && - pricePeriodTimeElapsed <= maxUpdateGap && - pricePeriodTimeElapsed >= period && - liquidityPeriodTimeElapsed <= maxUpdateGap && - liquidityPeriodTimeElapsed >= period - ) { - ObservationLibrary.Observation storage observation = observations[token]; - - observation.price = IPriceAccumulator(priceAccumulator).calculatePrice( - priceAccumulationBuffers[token][meta.start], - priceAccumulation - ); - (observation.tokenLiquidity, observation.quoteTokenLiquidity) = ILiquidityAccumulator( - liquidityAccumulator - ).calculateLiquidity(liquidityAccumulationBuffers[token][meta.start], liquidityAccumulation); - observation.timestamp = block.timestamp.toUint32(); - - emit Updated( - token, - observation.price, - observation.tokenLiquidity, - observation.quoteTokenLiquidity, - observation.timestamp - ); - } else if (pricePeriodTimeElapsed == 0 && liquidityPeriodTimeElapsed == 0) { - // Both accumulations haven't changed, so we don't need to update - return false; + // Check that at least one accumulation is newer than the last one + { + uint256 lastPriceAccumulationTimestamp = priceAccumulationBuffers[token][meta.end].timestamp; + uint256 lastLiquidityAccumulationTimestamp = liquidityAccumulationBuffers[token][meta.end].timestamp; + + // Note: Reverts if the new accumulations are older than the last ones + uint256 lastPriceAccumulationTimeElapsed = priceAccumulation.timestamp - lastPriceAccumulationTimestamp; + uint256 lastLiquidityAccumulationTimeElapsed = liquidityAccumulation.timestamp - + lastLiquidityAccumulationTimestamp; + + if (lastPriceAccumulationTimeElapsed == 0 && lastLiquidityAccumulationTimeElapsed == 0) { + // Both accumulations haven't changed, so we don't need to update + return false; + } + } + + if (meta.size >= granularity) { + uint256 firstPriceAccumulationTime = priceAccumulationBuffers[token][meta.start].timestamp; + uint256 pricePeriodTimeElapsed = priceAccumulation.timestamp - firstPriceAccumulationTime; + + uint256 firstLiquidityAccumulationTime = liquidityAccumulationBuffers[token][meta.start].timestamp; + uint256 liquidityPeriodTimeElapsed = liquidityAccumulation.timestamp - firstLiquidityAccumulationTime; + + uint256 maxUpdateGap = period + updateDelayTolerance(); + + if ( + pricePeriodTimeElapsed <= maxUpdateGap && + pricePeriodTimeElapsed >= period && + liquidityPeriodTimeElapsed <= maxUpdateGap && + liquidityPeriodTimeElapsed >= period + ) { + ObservationLibrary.Observation storage observation = observations[token]; + + observation.price = IPriceAccumulator(priceAccumulator).calculatePrice( + priceAccumulationBuffers[token][meta.start], + priceAccumulation + ); + (observation.tokenLiquidity, observation.quoteTokenLiquidity) = ILiquidityAccumulator( + liquidityAccumulator + ).calculateLiquidity(liquidityAccumulationBuffers[token][meta.start], liquidityAccumulation); + observation.timestamp = block.timestamp.toUint32(); + + emit Updated( + token, + observation.price, + observation.tokenLiquidity, + observation.quoteTokenLiquidity, + observation.timestamp + ); + } } meta.end = (meta.end + 1) % meta.maxSize; From 79279a380277d8774af27d46d34a27e028a3855e Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Sat, 4 Feb 2023 14:49:47 -0800 Subject: [PATCH 18/32] Allow PeriodicAccumulationOracle's buffer size to be larger than its granularity --- .../oracles/PeriodicAccumulationOracle.sol | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/contracts/oracles/PeriodicAccumulationOracle.sol b/contracts/oracles/PeriodicAccumulationOracle.sol index 9651b37..4e1bf56 100644 --- a/contracts/oracles/PeriodicAccumulationOracle.sol +++ b/contracts/oracles/PeriodicAccumulationOracle.sol @@ -175,12 +175,21 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, } } + // Check if we have enough accumulations for a new observation if (meta.size >= granularity) { - uint256 firstPriceAccumulationTime = priceAccumulationBuffers[token][meta.start].timestamp; - uint256 pricePeriodTimeElapsed = priceAccumulation.timestamp - firstPriceAccumulationTime; + uint256 startIndex = meta.end < granularity + ? meta.end + meta.size - granularity + : meta.end - granularity; - uint256 firstLiquidityAccumulationTime = liquidityAccumulationBuffers[token][meta.start].timestamp; - uint256 liquidityPeriodTimeElapsed = liquidityAccumulation.timestamp - firstLiquidityAccumulationTime; + AccumulationLibrary.PriceAccumulator memory firstPriceAccumulation = priceAccumulationBuffers[token][ + startIndex + ]; + AccumulationLibrary.LiquidityAccumulator + memory firstLiquidityAccumulation = liquidityAccumulationBuffers[token][startIndex]; + + uint256 pricePeriodTimeElapsed = priceAccumulation.timestamp - firstPriceAccumulation.timestamp; + uint256 liquidityPeriodTimeElapsed = liquidityAccumulation.timestamp - + firstLiquidityAccumulation.timestamp; uint256 maxUpdateGap = period + updateDelayTolerance(); @@ -193,12 +202,12 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, ObservationLibrary.Observation storage observation = observations[token]; observation.price = IPriceAccumulator(priceAccumulator).calculatePrice( - priceAccumulationBuffers[token][meta.start], + firstPriceAccumulation, priceAccumulation ); (observation.tokenLiquidity, observation.quoteTokenLiquidity) = ILiquidityAccumulator( liquidityAccumulator - ).calculateLiquidity(liquidityAccumulationBuffers[token][meta.start], liquidityAccumulation); + ).calculateLiquidity(firstLiquidityAccumulation, liquidityAccumulation); observation.timestamp = block.timestamp.toUint32(); emit Updated( From db4d7dff878c5777704c0261557e2f3bc03490e5 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Sat, 4 Feb 2023 14:53:55 -0800 Subject: [PATCH 19/32] Adjust PeriodicAccumulationOracle#updateDelayTolerance - Since this tolerance is used in respect to all accumulations made in a period, it makes more sense to use the whole period rather than the granular update frequency --- contracts/oracles/PeriodicAccumulationOracle.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/oracles/PeriodicAccumulationOracle.sol b/contracts/oracles/PeriodicAccumulationOracle.sol index 4e1bf56..d215923 100644 --- a/contracts/oracles/PeriodicAccumulationOracle.sol +++ b/contracts/oracles/PeriodicAccumulationOracle.sol @@ -108,8 +108,9 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, return 1 hours; } - /// @notice The grace period that we allow for the oracle to be in need of an update before we discard the last - /// accumulation. If this grace period is exceeded, it will take two updates to get a new observation. + /// @notice The grace period that we allow for the oracle to be in need of an update (as the sum of all update + /// delays in a period) before we discard the last accumulation. If this grace period is exceeded, it will take + /// more updates to get a new observation. /// @dev This is to prevent longer time-weighted averages than we desire. The maximum period is then the period of /// this oracle plus this grace period. /// @return The grace period in seconds. @@ -117,7 +118,7 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, // We tolerate two missed periods plus 5 minutes (to allow for some time to update the oracles). // We trade off some freshness for greater reliability. Using too low of a tolerance reduces the cost of DoS // attacks. - return (_updateEvery * 2) + 5 minutes; + return (period * 2) + 5 minutes; } function initializeBuffers(address token) internal virtual { From 7539bbe83e87b2d3d4ff3ec9ad4d5b4027aea108 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Sun, 5 Feb 2023 20:13:20 -0800 Subject: [PATCH 20/32] Add IHistoricalPriceAccumulationOracle and IHistoricalLiquidityAccumulationOracle interfaces --- ...IHistoricalLiquidityAccumulationOracle.sol | 57 +++++++++++++++++++ .../IHistoricalPriceAccumulationOracle.sol | 57 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 contracts/interfaces/IHistoricalLiquidityAccumulationOracle.sol create mode 100644 contracts/interfaces/IHistoricalPriceAccumulationOracle.sol diff --git a/contracts/interfaces/IHistoricalLiquidityAccumulationOracle.sol b/contracts/interfaces/IHistoricalLiquidityAccumulationOracle.sol new file mode 100644 index 0000000..54cdaf0 --- /dev/null +++ b/contracts/interfaces/IHistoricalLiquidityAccumulationOracle.sol @@ -0,0 +1,57 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.5.0 <0.9.0; + +import "../libraries/AccumulationLibrary.sol"; + +/** + * @title IHistoricalLiquidityAccumulationOracle + * @notice An interface that defines an oracle contract that stores historical liquidity accumulations. + */ +interface IHistoricalLiquidityAccumulationOracle { + /// @notice Gets a liquidity accumulation for a token at a specific index. + /// @param token The address of the token to get the accumulation for. + /// @param index The index of the accumulation to get, where index 0 contains the latest accumulation, and the last + /// index contains the oldest accumulation (uses reverse chronological ordering). + /// @return The accumulation for the token at the specified index. + function getLiquidityAccumulationAt( + address token, + uint256 index + ) external view returns (AccumulationLibrary.LiquidityAccumulator memory); + + /// @notice Gets the latest liquidity accumulations for a token. + /// @param token The address of the token to get the accumulations for. + /// @param amount The number of accumulations to get. + /// @return The latest accumulations for the token, in reverse chronological order, from newest to oldest. + function getLiquidityAccumulations( + address token, + uint256 amount + ) external view returns (AccumulationLibrary.LiquidityAccumulator[] memory); + + /// @notice Gets the latest liquidity accumulations for a token. + /// @param token The address of the token to get the accumulations for. + /// @param amount The number of accumulations to get. + /// @param offset The index of the first accumulations to get (default: 0). + /// @param increment The increment between accumulations to get (default: 1). + /// @return The latest accumulations for the token, in reverse chronological order, from newest to oldest. + function getLiquidityAccumulations( + address token, + uint256 amount, + uint256 offset, + uint256 increment + ) external view returns (AccumulationLibrary.LiquidityAccumulator[] memory); + + /// @notice Gets the number of liquidity accumulations for a token. + /// @param token The address of the token to get the number of accumulations for. + /// @return count The number of accumulations for the token. + function getLiquidityAccumulationsCount(address token) external view returns (uint256); + + /// @notice Gets the capacity of liquidity accumulations for a token. + /// @param token The address of the token to get the capacity of accumulations for. + /// @return capacity The capacity of accumulations for the token. + function getLiquidityAccumulationsCapacity(address token) external view returns (uint256); + + /// @notice Sets the capacity of liquidity accumulations for a token. + /// @param token The address of the token to set the capacity of accumulations for. + /// @param amount The new capacity of accumulations for the token. + function setLiquidityAccumulationsCapacity(address token, uint256 amount) external; +} diff --git a/contracts/interfaces/IHistoricalPriceAccumulationOracle.sol b/contracts/interfaces/IHistoricalPriceAccumulationOracle.sol new file mode 100644 index 0000000..cc93af8 --- /dev/null +++ b/contracts/interfaces/IHistoricalPriceAccumulationOracle.sol @@ -0,0 +1,57 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.5.0 <0.9.0; + +import "../libraries/AccumulationLibrary.sol"; + +/** + * @title IHistoricalPriceAccumulationOracle + * @notice An interface that defines an oracle contract that stores historical price accumulations. + */ +interface IHistoricalPriceAccumulationOracle { + /// @notice Gets a price accumulation for a token at a specific index. + /// @param token The address of the token to get the accumulation for. + /// @param index The index of the accumulation to get, where index 0 contains the latest accumulation, and the last + /// index contains the oldest accumulation (uses reverse chronological ordering). + /// @return The accumulation for the token at the specified index. + function getPriceAccumulationAt( + address token, + uint256 index + ) external view returns (AccumulationLibrary.PriceAccumulator memory); + + /// @notice Gets the latest price accumulations for a token. + /// @param token The address of the token to get the accumulations for. + /// @param amount The number of accumulations to get. + /// @return The latest accumulations for the token, in reverse chronological order, from newest to oldest. + function getPriceAccumulations( + address token, + uint256 amount + ) external view returns (AccumulationLibrary.PriceAccumulator[] memory); + + /// @notice Gets the latest price accumulations for a token. + /// @param token The address of the token to get the accumulations for. + /// @param amount The number of accumulations to get. + /// @param offset The index of the first accumulations to get (default: 0). + /// @param increment The increment between accumulations to get (default: 1). + /// @return The latest accumulations for the token, in reverse chronological order, from newest to oldest. + function getPriceAccumulations( + address token, + uint256 amount, + uint256 offset, + uint256 increment + ) external view returns (AccumulationLibrary.PriceAccumulator[] memory); + + /// @notice Gets the number of price accumulations for a token. + /// @param token The address of the token to get the number of accumulations for. + /// @return count The number of accumulations for the token. + function getPriceAccumulationsCount(address token) external view returns (uint256); + + /// @notice Gets the capacity of price accumulations for a token. + /// @param token The address of the token to get the capacity of accumulations for. + /// @return capacity The capacity of accumulations for the token. + function getPriceAccumulationsCapacity(address token) external view returns (uint256); + + /// @notice Sets the capacity of price accumulations for a token. + /// @param token The address of the token to set the capacity of accumulations for. + /// @param amount The new capacity of accumulations for the token. + function setPriceAccumulationsCapacity(address token, uint256 amount) external; +} From 1cea4dfbffab2a6ae6bdc6da7bed1f99c451e989 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Sun, 5 Feb 2023 20:14:18 -0800 Subject: [PATCH 21/32] Make PeriodicAccumulationOracle implement IHistoricalPriceAccumulationOracle and IHistoricalLiquidityAccumulationOracle --- .../oracles/PeriodicAccumulationOracle.sol | 215 +++++++++++++++++- 1 file changed, 214 insertions(+), 1 deletion(-) diff --git a/contracts/oracles/PeriodicAccumulationOracle.sol b/contracts/oracles/PeriodicAccumulationOracle.sol index d215923..293a9c5 100644 --- a/contracts/oracles/PeriodicAccumulationOracle.sol +++ b/contracts/oracles/PeriodicAccumulationOracle.sol @@ -9,11 +9,19 @@ import "../interfaces/ILiquidityAccumulator.sol"; import "../interfaces/IHasLiquidityAccumulator.sol"; import "../interfaces/IPriceAccumulator.sol"; import "../interfaces/IHasPriceAccumulator.sol"; +import "../interfaces/IHistoricalPriceAccumulationOracle.sol"; +import "../interfaces/IHistoricalLiquidityAccumulationOracle.sol"; import "../libraries/AccumulationLibrary.sol"; import "../libraries/ObservationLibrary.sol"; -contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, IHasPriceAccumulator { +contract PeriodicAccumulationOracle is + IHistoricalPriceAccumulationOracle, + IHistoricalLiquidityAccumulationOracle, + PeriodicOracle, + IHasLiquidityAccumulator, + IHasPriceAccumulator +{ using SafeCast for uint256; struct BufferMetadata { @@ -33,6 +41,13 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, mapping(address => ObservationLibrary.Observation) internal observations; + /// @notice Event emitted when an accumulation buffer's capacity is increased past the initial capacity. + /// @dev Buffer initialization does not emit an event. + /// @param token The token for which the accumulation buffer's capacity was increased. + /// @param oldCapacity The previous capacity of the observation buffer. + /// @param newCapacity The new capacity of the observation buffer. + event AccumulationCapacityIncreased(address indexed token, uint256 oldCapacity, uint256 newCapacity); + constructor( address liquidityAccumulator_, address priceAccumulator_, @@ -44,6 +59,110 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, priceAccumulator = priceAccumulator_; } + /// @inheritdoc IHistoricalPriceAccumulationOracle + function getPriceAccumulationAt( + address token, + uint256 index + ) external view virtual override returns (AccumulationLibrary.PriceAccumulator memory) { + BufferMetadata memory meta = accumulationBufferMetadata[token]; + + require(index < meta.size, "PeriodicAccumulationOracle: INVALID_INDEX"); + + uint256 bufferIndex = meta.end < index ? meta.end + meta.size - index : meta.end - index; + + return priceAccumulationBuffers[token][bufferIndex]; + } + + /// @inheritdoc IHistoricalPriceAccumulationOracle + function getPriceAccumulations( + address token, + uint256 amount + ) external view virtual override returns (AccumulationLibrary.PriceAccumulator[] memory) { + return getPriceAccumulationsInternal(token, amount, 0, 1); + } + + /// @inheritdoc IHistoricalPriceAccumulationOracle + function getPriceAccumulations( + address token, + uint256 amount, + uint256 offset, + uint256 increment + ) external view virtual returns (AccumulationLibrary.PriceAccumulator[] memory) { + return getPriceAccumulationsInternal(token, amount, offset, increment); + } + + /// @inheritdoc IHistoricalPriceAccumulationOracle + function getPriceAccumulationsCount(address token) external view override returns (uint256) { + return accumulationBufferMetadata[token].size; + } + + /// @inheritdoc IHistoricalPriceAccumulationOracle + function getPriceAccumulationsCapacity(address token) external view virtual override returns (uint256) { + uint256 maxSize = accumulationBufferMetadata[token].maxSize; + if (maxSize == 0) return granularity; + + return maxSize; + } + + /// @inheritdoc IHistoricalPriceAccumulationOracle + /// @param amount The new capacity of accumulations for the token. Must be greater than the current capacity, but + /// less than 65536. + function setPriceAccumulationsCapacity(address token, uint256 amount) external virtual override { + setAccumulationsCapacityInternal(token, amount); + } + + /// @inheritdoc IHistoricalLiquidityAccumulationOracle + function getLiquidityAccumulationAt( + address token, + uint256 index + ) external view virtual override returns (AccumulationLibrary.LiquidityAccumulator memory) { + BufferMetadata memory meta = accumulationBufferMetadata[token]; + + require(index < meta.size, "PeriodicAccumulationOracle: INVALID_INDEX"); + + uint256 bufferIndex = meta.end < index ? meta.end + meta.size - index : meta.end - index; + + return liquidityAccumulationBuffers[token][bufferIndex]; + } + + /// @inheritdoc IHistoricalLiquidityAccumulationOracle + function getLiquidityAccumulations( + address token, + uint256 amount + ) external view virtual override returns (AccumulationLibrary.LiquidityAccumulator[] memory) { + return getLiquidityAccumulationsInternal(token, amount, 0, 1); + } + + /// @inheritdoc IHistoricalLiquidityAccumulationOracle + function getLiquidityAccumulations( + address token, + uint256 amount, + uint256 offset, + uint256 increment + ) external view virtual returns (AccumulationLibrary.LiquidityAccumulator[] memory) { + return getLiquidityAccumulationsInternal(token, amount, offset, increment); + } + + /// @inheritdoc IHistoricalLiquidityAccumulationOracle + function getLiquidityAccumulationsCount(address token) external view override returns (uint256) { + return accumulationBufferMetadata[token].size; + } + + /// @inheritdoc IHistoricalLiquidityAccumulationOracle + function getLiquidityAccumulationsCapacity(address token) external view virtual override returns (uint256) { + uint256 maxSize = accumulationBufferMetadata[token].maxSize; + if (maxSize == 0) return granularity; + + return maxSize; + } + + /// @inheritdoc IHistoricalLiquidityAccumulationOracle + /// @param amount The new capacity of accumulations for the token. Must be greater than the current capacity, but + /// less than 65536. + function setLiquidityAccumulationsCapacity(address token, uint256 amount) external virtual override { + setAccumulationsCapacityInternal(token, amount); + } + function getLatestObservation( address token ) public view virtual override returns (ObservationLibrary.Observation memory observation) { @@ -89,6 +208,8 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, return interfaceId == type(IHasLiquidityAccumulator).interfaceId || interfaceId == type(IHasPriceAccumulator).interfaceId || + interfaceId == type(IHistoricalPriceAccumulationOracle).interfaceId || + interfaceId == type(IHistoricalLiquidityAccumulationOracle).interfaceId || super.supportsInterface(interfaceId); } @@ -121,6 +242,98 @@ contract PeriodicAccumulationOracle is PeriodicOracle, IHasLiquidityAccumulator, return (period * 2) + 5 minutes; } + function setAccumulationsCapacityInternal(address token, uint256 amount) internal virtual { + BufferMetadata storage meta = accumulationBufferMetadata[token]; + if (meta.maxSize == 0) { + // Buffer is not initialized yet + initializeBuffers(token); + } + + require(amount >= meta.maxSize, "PeriodicAccumulationOracle: CAPACITY_CANNOT_BE_DECREASED"); + require(amount <= type(uint16).max, "PeriodicAccumulationOracle: CAPACITY_TOO_LARGE"); + + AccumulationLibrary.PriceAccumulator[] storage priceAccumulationBuffer = priceAccumulationBuffers[token]; + AccumulationLibrary.LiquidityAccumulator[] storage liquidityAccumulationBuffer = liquidityAccumulationBuffers[ + token + ]; + + // Add new slots to the buffer + uint256 capacityToAdd = amount - meta.maxSize; + for (uint256 i = 0; i < capacityToAdd; ++i) { + // Push dummy accumulations with non-zero values to put most of the gas cost on the caller + priceAccumulationBuffer.push(AccumulationLibrary.PriceAccumulator({cumulativePrice: 1, timestamp: 1})); + liquidityAccumulationBuffer.push( + AccumulationLibrary.LiquidityAccumulator({ + cumulativeTokenLiquidity: 1, + cumulativeQuoteTokenLiquidity: 1, + timestamp: 1 + }) + ); + } + + if (meta.maxSize != amount) { + emit AccumulationCapacityIncreased(token, meta.maxSize, amount); + + // Update the metadata + meta.maxSize = uint16(amount); + } + } + + function getPriceAccumulationsInternal( + address token, + uint256 amount, + uint256 offset, + uint256 increment + ) internal view virtual returns (AccumulationLibrary.PriceAccumulator[] memory) { + if (amount == 0) return new AccumulationLibrary.PriceAccumulator[](0); + + BufferMetadata memory meta = accumulationBufferMetadata[token]; + require(meta.size > (amount - 1) * increment + offset, "PeriodicAccumulationOracle: INSUFFICIENT_DATA"); + + AccumulationLibrary.PriceAccumulator[] memory accumulations = new AccumulationLibrary.PriceAccumulator[]( + amount + ); + + uint256 count = 0; + + for ( + uint256 i = meta.end < offset ? meta.end + meta.size - offset : meta.end - offset; + count < amount; + i = (i < increment) ? (i + meta.size) - increment : i - increment + ) { + accumulations[count++] = priceAccumulationBuffers[token][i]; + } + + return accumulations; + } + + function getLiquidityAccumulationsInternal( + address token, + uint256 amount, + uint256 offset, + uint256 increment + ) internal view virtual returns (AccumulationLibrary.LiquidityAccumulator[] memory) { + if (amount == 0) return new AccumulationLibrary.LiquidityAccumulator[](0); + + BufferMetadata memory meta = accumulationBufferMetadata[token]; + require(meta.size > (amount - 1) * increment + offset, "PeriodicAccumulationOracle: INSUFFICIENT_DATA"); + + AccumulationLibrary.LiquidityAccumulator[] + memory accumulations = new AccumulationLibrary.LiquidityAccumulator[](amount); + + uint256 count = 0; + + for ( + uint256 i = meta.end < offset ? meta.end + meta.size - offset : meta.end - offset; + count < amount; + i = (i < increment) ? (i + meta.size) - increment : i - increment + ) { + accumulations[count++] = liquidityAccumulationBuffers[token][i]; + } + + return accumulations; + } + function initializeBuffers(address token) internal virtual { require( priceAccumulationBuffers[token].length == 0 && liquidityAccumulationBuffers[token].length == 0, From 99b33b962692e1c17d8ae31bdaae7febd605448b Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Sun, 5 Feb 2023 20:15:25 -0800 Subject: [PATCH 22/32] Add tests for PeriodicAccumulationOracle in relation to IHistoricalPriceAccumulationOracle and IHistoricalLiquidityAccumulationOracle --- contracts/test/InterfaceIds.sol | 10 + .../PeriodicAccumulationOracleStub.sol | 25 + test/oracles/periodic-accumulation-oracle.js | 775 ++++++++++++++++++ 3 files changed, 810 insertions(+) diff --git a/contracts/test/InterfaceIds.sol b/contracts/test/InterfaceIds.sol index 22653a4..421e5a2 100644 --- a/contracts/test/InterfaceIds.sol +++ b/contracts/test/InterfaceIds.sol @@ -14,6 +14,8 @@ import "../interfaces/IQuoteToken.sol"; import "../interfaces/IUpdateable.sol"; import "../interfaces/IAccumulator.sol"; import "../interfaces/IHistoricalOracle.sol"; +import "../interfaces/IHistoricalPriceAccumulationOracle.sol"; +import "../interfaces/IHistoricalLiquidityAccumulationOracle.sol"; contract InterfaceIds { function iAggregatedOracle() external pure returns (bytes4) { @@ -67,4 +69,12 @@ contract InterfaceIds { function iHistoricalOracle() external pure returns (bytes4) { return type(IHistoricalOracle).interfaceId; } + + function iHistoricalPriceAccumulationOracle() external pure returns (bytes4) { + return type(IHistoricalPriceAccumulationOracle).interfaceId; + } + + function iHistoricalLiquidityAccumulationOracle() external pure returns (bytes4) { + return type(IHistoricalLiquidityAccumulationOracle).interfaceId; + } } diff --git a/contracts/test/oracles/PeriodicAccumulationOracleStub.sol b/contracts/test/oracles/PeriodicAccumulationOracleStub.sol index c7e3a01..fcc8ffd 100644 --- a/contracts/test/oracles/PeriodicAccumulationOracleStub.sol +++ b/contracts/test/oracles/PeriodicAccumulationOracleStub.sol @@ -19,6 +19,31 @@ contract PeriodicAccumulationOracleStub is PeriodicAccumulationOracle { uint256 granularity_ ) PeriodicAccumulationOracle(liquidityAccumulator_, priceAccumulator_, quoteToken_, period_, granularity_) {} + function stubPush( + address token, + uint224 cumulativePrice, + uint32 priceTimestamp, + uint112 cumulativeTokenLiquidity, + uint112 cumulativeQuoteTokenLiquidity, + uint32 liquidityTimestamp + ) public { + AccumulationLibrary.PriceAccumulator memory priceAccumulation; + AccumulationLibrary.LiquidityAccumulator memory liquidityAccumulation; + + priceAccumulation.cumulativePrice = cumulativePrice; + priceAccumulation.timestamp = priceTimestamp; + + liquidityAccumulation.cumulativeTokenLiquidity = cumulativeTokenLiquidity; + liquidityAccumulation.cumulativeQuoteTokenLiquidity = cumulativeQuoteTokenLiquidity; + liquidityAccumulation.timestamp = liquidityTimestamp; + + push(token, priceAccumulation, liquidityAccumulation); + } + + function stubInitializeBuffers(address token) public { + initializeBuffers(token); + } + function priceAccumulations(address token) public view returns (AccumulationLibrary.PriceAccumulator memory) { return priceAccumulationBuffers[token][accumulationBufferMetadata[token].end]; } diff --git a/test/oracles/periodic-accumulation-oracle.js b/test/oracles/periodic-accumulation-oracle.js index 86d2cd8..59af764 100644 --- a/test/oracles/periodic-accumulation-oracle.js +++ b/test/oracles/periodic-accumulation-oracle.js @@ -5,6 +5,7 @@ const AddressZero = ethers.constants.AddressZero; const MaxUint256 = ethers.constants.MaxUint256; const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; +const GRT = "0xc944E90C64B2c07662A292be6244BDf05Cda44a7"; const PERIOD = 100; @@ -2429,4 +2430,778 @@ describe("PeriodicAccumulationOracle#supportsInterface(interfaceId)", function ( const interfaceId = await interfaceIds.iHasPriceAccumulator(); expect(await oracle["supportsInterface(bytes4)"](interfaceId)).to.equal(true); }); + + it("Should support IHistoricalPriceAccumulationOracle", async () => { + const interfaceId = await interfaceIds.iHistoricalPriceAccumulationOracle(); + expect(await oracle["supportsInterface(bytes4)"](interfaceId)).to.equal(true); + }); + + it("Should support IHistoricalLiquidityAccumulationOracle", async () => { + const interfaceId = await interfaceIds.iHistoricalLiquidityAccumulationOracle(); + expect(await oracle["supportsInterface(bytes4)"](interfaceId)).to.equal(true); + }); }); + +function describeHistoricalAccumulationOracleTests(type) { + const MIN_UPDATE_DELAY = 1; + const MAX_UPDATE_DELAY = 60; + const TWO_PERCENT_CHANGE = 2000000; + + describe("PeriodicAccumulationOracle - IHistorical" + type + "AccumulationOracle implementation", function () { + var priceAccumulator; + var liquidityAccumulator; + var oracle; + + beforeEach(async () => { + // Deploy liquidity accumulator + const liquidityAccumulatorFactory = await ethers.getContractFactory("LiquidityAccumulatorStub"); + liquidityAccumulator = await liquidityAccumulatorFactory.deploy( + USDC, + TWO_PERCENT_CHANGE, + MIN_UPDATE_DELAY, + MAX_UPDATE_DELAY + ); + await liquidityAccumulator.deployed(); + + // Deploy price accumulator + const priceAccumulatorFactory = await ethers.getContractFactory("PriceAccumulatorStub"); + priceAccumulator = await priceAccumulatorFactory.deploy( + USDC, + TWO_PERCENT_CHANGE, + MIN_UPDATE_DELAY, + MAX_UPDATE_DELAY + ); + await priceAccumulator.deployed(); + + // Deploy oracle + const oracleFactory = await ethers.getContractFactory("PeriodicAccumulationOracleStub"); + oracle = await oracleFactory.deploy( + liquidityAccumulator.address, + priceAccumulator.address, + USDC, + 1, + GRANULARITY + ); + }); + + describe("PeriodicAccumulationOracle#initializeBuffers", function () { + it("Can't be called twice", async function () { + await oracle.stubInitializeBuffers(GRT); + + await expect(oracle.stubInitializeBuffers(GRT)).to.be.revertedWith( + "PeriodicAccumulationOracle: ALREADY_INITIALIZED" + ); + }); + }); + + const setCapacityFunctionName = "set" + type + "AccumulationsCapacity"; + const getCapacityFunctionName = "get" + type + "AccumulationsCapacity"; + const getCountFunctionName = "get" + type + "AccumulationsCount"; + const getFunctionName2Params = "get" + type + "Accumulations(address,uint256)"; + const getFunctionName4Params = "get" + type + "Accumulations(address,uint256,uint256,uint256)"; + const getAtFunctionName = "get" + type + "AccumulationAt"; + + describe("PeriodicAccumulationOracle#" + setCapacityFunctionName, function () { + it("Should revert if the amount is less than the existing capacity", async function () { + await oracle[setCapacityFunctionName](GRT, 4); + + await expect(oracle[setCapacityFunctionName](GRT, 2)).to.be.revertedWith( + "PeriodicAccumulationOracle: CAPACITY_CANNOT_BE_DECREASED" + ); + }); + + it("Should revert if the amount is 0", async function () { + await expect(oracle[setCapacityFunctionName](GRT, 0)).to.be.revertedWith( + "PeriodicAccumulationOracle: CAPACITY_CANNOT_BE_DECREASED" + ); + }); + + it("Should revert if the amount is larger than the maximum capacity", async function () { + await expect(oracle[setCapacityFunctionName](GRT, 65536)).to.be.revertedWith( + "PeriodicAccumulationOracle: CAPACITY_TOO_LARGE" + ); + }); + + it("Should emit an event when the capacity is changed", async function () { + const amount = 20; + + const initialAmount = await oracle[getCapacityFunctionName](GRT); + + // Sanity check that the new amount is greater than the initial amount + expect(amount).to.be.greaterThan(initialAmount.toNumber()); + + await expect(oracle[setCapacityFunctionName](GRT, amount)) + .to.emit(oracle, "AccumulationCapacityIncreased") + .withArgs(GRT, initialAmount, amount); + }); + + it("Should not emit an event when the capacity is not changed (with default capacity)", async function () { + const initialAmount = await oracle[getCapacityFunctionName](GRT); + + await expect(oracle[setCapacityFunctionName](GRT, initialAmount)).to.not.emit( + oracle, + "AccumulationCapacityIncreased" + ); + }); + + it("Should not emit an event when the capacity is not changed (with non-default capacity)", async function () { + const initialAmount = await oracle[getCapacityFunctionName](GRT); + const amount = 20; + + // Sanity check that the new amount is greater than the initial amount + expect(amount).to.be.greaterThan(initialAmount.toNumber()); + + await oracle[setCapacityFunctionName](GRT, amount); + + // Sanity check that the capacity is now the new amount + expect(await oracle[getCapacityFunctionName](GRT)).to.equal(amount); + + // Try again to set it to the same amount + await expect(oracle[setCapacityFunctionName](GRT, amount)).to.not.emit( + oracle, + "AccumulationCapacityIncreased" + ); + }); + + it("Should update the capacity", async function () { + const amount = 20; + + // Sanity check that the new amount is greater than the initial amount + expect(amount).to.be.greaterThan((await oracle[getCapacityFunctionName](GRT)).toNumber()); + + await oracle[setCapacityFunctionName](GRT, amount); + + expect(await oracle[getCapacityFunctionName](GRT)).to.equal(amount); + }); + + it("Added capacity should not be filled until our latest accumulation is beside an uninitialized accumulation", async function () { + const workingCapacity = 6; + + var totalPushed = 0; + + // Set the capacity to the working capacity + await oracle[setCapacityFunctionName](GRT, workingCapacity); + + // Push workingCapacity + 1 accumulations so that the buffer is full and the latest accumulation is at the start of the buffer + for (let i = 0; i < workingCapacity + 1; ++i) { + ++totalPushed; + await oracle.stubPush(GRT, totalPushed, totalPushed, totalPushed, totalPushed, totalPushed); + } + + // Sanity check that the buffer is full + expect(await oracle[getCountFunctionName](GRT)).to.equal(workingCapacity); + + // Increase the capacity by 1 + await oracle[setCapacityFunctionName](GRT, workingCapacity + 1); + + // We should need to push workingCapacity accumulations before the new capacity is filled + for (let i = 0; i < workingCapacity - 1; ++i) { + ++totalPushed; + await oracle.stubPush(GRT, totalPushed, totalPushed, totalPushed, totalPushed, totalPushed); + + // Sanity check that the buffer is still not full + expect(await oracle[getCountFunctionName](GRT)).to.equal(workingCapacity); + } + + // Push one more accumulation. This should fill the new capacity + ++totalPushed; + await oracle.stubPush(GRT, totalPushed, totalPushed, totalPushed, totalPushed, totalPushed); + + // Check that the buffer is now full + expect(await oracle[getCountFunctionName](GRT)).to.equal(workingCapacity + 1); + }); + }); + + describe("PeriodicAccumulationOracle#" + getCapacityFunctionName, function () { + it("Should return the default capacity when the buffer is uninitialized", async function () { + const initialCapacity = await oracle.granularity(); + + expect(await oracle[getCapacityFunctionName](GRT)).to.equal(initialCapacity); + }); + + it("Should return the capacity when the buffer is initialized", async function () { + await oracle.stubInitializeBuffers(GRT); + + const initialCapacity = await oracle.granularity(); + + expect(await oracle[getCapacityFunctionName](GRT)).to.equal(initialCapacity); + }); + + it("Should return the capacity after the buffer has been resized", async function () { + const amount = 20; + + // Sanity check that the new amount is greater than the initial amount + expect(amount).to.be.greaterThan((await oracle[getCapacityFunctionName](GRT)).toNumber()); + + await oracle[setCapacityFunctionName](GRT, amount); + + expect(await oracle[getCapacityFunctionName](GRT)).to.equal(amount); + }); + }); + + describe("PeriodicAccumulationOracle#" + getCountFunctionName, function () { + it("Should return 0 when the buffer is uninitialized", async function () { + expect(await oracle[getCountFunctionName](GRT)).to.equal(0); + }); + + it("Should return 0 when the buffer is initialized but empty", async function () { + await oracle.stubInitializeBuffers(GRT); + + expect(await oracle[getCountFunctionName](GRT)).to.equal(0); + }); + + it("Increasing capacity should not change the accumulations count", async function () { + const initialAmount = 4; + + await oracle[setCapacityFunctionName](GRT, initialAmount); + + // Push 2 accumulations + await oracle.stubPush(GRT, 1, 1, 1, 1, 1); + await oracle.stubPush(GRT, 2, 2, 2, 2, 2); + + // Sanity check that the accumulations count is 2 + expect(await oracle[getCountFunctionName](GRT)).to.equal(2); + + // Increase the capacity by 1 + await oracle[setCapacityFunctionName](GRT, initialAmount + 1); + + // The accumulations count should still be 2 + expect(await oracle[getCountFunctionName](GRT)).to.equal(2); + }); + + it("Should be limited by the capacity", async function () { + const capacity = 6; + + var totalPushed = 0; + + await oracle[setCapacityFunctionName](GRT, capacity); + + // Push capacity + 1 accumulations + for (let i = 0; i < capacity + 1; ++i) { + ++totalPushed; + await oracle.stubPush(GRT, totalPushed, totalPushed, totalPushed, totalPushed, totalPushed); + } + + // The accumulations count should be limited by the capacity + expect(await oracle[getCountFunctionName](GRT)).to.equal(capacity); + }); + }); + + describe("PeriodicAccumulationOracle#" + getFunctionName4Params, function () { + it("Should return an empty array when amount is 0", async function () { + await oracle.stubPush(GRT, 1, 1, 1, 1, 1); + + const accumulations = await oracle[getFunctionName4Params](GRT, 0, 0, 1); + + expect(accumulations.length).to.equal(0); + }); + + it("Should revert if the offset equals the number of accumulations", async function () { + // Push 1 accumulation + await oracle.stubPush(GRT, 1, 1, 1, 1, 1); + + await expect(oracle[getFunctionName4Params](GRT, 1, 1, 1)).to.be.revertedWith( + "PeriodicAccumulationOracle: INSUFFICIENT_DATA" + ); + }); + + it("Should revert if the offset equals the number of accumulations but is less than the capacity", async function () { + const capacity = 6; + + await oracle[setCapacityFunctionName](GRT, capacity); + + // Sanity check the capacity + expect(await oracle[getCapacityFunctionName](GRT)).to.equal(capacity); + + // Push 1 accumulation + await oracle.stubPush(GRT, 1, 1, 1, 1, 1); + + await expect(oracle[getFunctionName4Params](GRT, 1, 1, 1)).to.be.revertedWith( + "PeriodicAccumulationOracle: INSUFFICIENT_DATA" + ); + }); + + it("Should revert if the amount exceeds the number of accumulations", async function () { + // Push 1 accumulation + await oracle.stubPush(GRT, 1, 1, 1, 1, 1); + + await expect(oracle[getFunctionName4Params](GRT, 2, 0, 1)).to.be.revertedWith( + "PeriodicAccumulationOracle: INSUFFICIENT_DATA" + ); + }); + + it("Should revert if the amount exceeds the number of accumulations but is less than the capacity", async function () { + const capacity = 6; + const amountToGet = 2; + + await oracle[setCapacityFunctionName](GRT, capacity); + + // Sanity check the capacity + expect(await oracle[getCapacityFunctionName](GRT)).to.equal(capacity); + + // Push 1 accumulation + await oracle.stubPush(GRT, 1, 1, 1, 1, 1); + + // Sanity check that the amount to get is less than the capacity + expect(amountToGet).to.be.lessThan(capacity); + + await expect(oracle[getFunctionName4Params](GRT, amountToGet, 0, 1)).to.be.revertedWith( + "PeriodicAccumulationOracle: INSUFFICIENT_DATA" + ); + }); + + it("Should revert if the amount and offset exceed the number of accumulations", async function () { + const capacity = 2; + const amountToGet = 2; + + await oracle[setCapacityFunctionName](GRT, capacity); + + // Sanity check the capacity + expect(await oracle[getCapacityFunctionName](GRT)).to.equal(capacity); + + // Push 2 accumulations + await oracle.stubPush(GRT, 1, 1, 1, 1, 1); + await oracle.stubPush(GRT, 2, 2, 2, 2, 2); + + await expect(oracle[getFunctionName4Params](GRT, amountToGet, 1, 1)).to.be.revertedWith( + "PeriodicAccumulationOracle: INSUFFICIENT_DATA" + ); + }); + + it("Should revert if the amount and offset exceed the number of accumulations but is less than the capacity", async function () { + const capacity = 6; + const amountToGet = 2; + + await oracle[setCapacityFunctionName](GRT, capacity); + + // Sanity check the capacity + expect(await oracle[getCapacityFunctionName](GRT)).to.equal(capacity); + + // Push 2 accumulation + await oracle.stubPush(GRT, 1, 1, 1, 1, 1); + await oracle.stubPush(GRT, 2, 2, 2, 2, 2); + + await expect(oracle[getFunctionName4Params](GRT, amountToGet, 1, 1)).to.be.revertedWith( + "PeriodicAccumulationOracle: INSUFFICIENT_DATA" + ); + }); + + it("Should revert if the increment and amount exceeds the number of accumulations", async function () { + const capacity = 2; + const amountToGet = 2; + const offset = 0; + const increment = 2; + + await oracle[setCapacityFunctionName](GRT, capacity); + + // Sanity check the capacity + expect(await oracle[getCapacityFunctionName](GRT)).to.equal(capacity); + + // Push 2 accumulation + await oracle.stubPush(GRT, 1, 1, 1, 1, 1); + await oracle.stubPush(GRT, 2, 2, 2, 2, 2); + + await expect(oracle[getFunctionName4Params](GRT, amountToGet, offset, increment)).to.be.revertedWith( + "PeriodicAccumulationOracle: INSUFFICIENT_DATA" + ); + }); + + it("Should revert if the increment and amount exceeds the number of accumulations but is less than the capacity", async function () { + const capacity = 6; + const amountToGet = 2; + const offset = 0; + const increment = 2; + + await oracle[setCapacityFunctionName](GRT, capacity); + + // Sanity check the capacity + expect(await oracle[getCapacityFunctionName](GRT)).to.equal(capacity); + + // Push 2 accumulation + await oracle.stubPush(GRT, 1, 1, 1, 1, 1); + await oracle.stubPush(GRT, 2, 2, 2, 2, 2); + + await expect(oracle[getFunctionName4Params](GRT, amountToGet, offset, increment)).to.be.revertedWith( + "PeriodicAccumulationOracle: INSUFFICIENT_DATA" + ); + }); + + it("Should revert if the increment, amount, and offset exceeds the number of accumulations", async function () { + const capacity = 2; + const amountToGet = 2; + const offset = 1; + const increment = 2; + + await oracle[setCapacityFunctionName](GRT, capacity); + + // Sanity check the capacity + expect(await oracle[getCapacityFunctionName](GRT)).to.equal(capacity); + + // Push 3 accumulations + await oracle.stubPush(GRT, 1, 1, 1, 1, 1); + await oracle.stubPush(GRT, 2, 2, 2, 2, 2); + await oracle.stubPush(GRT, 3, 3, 3, 3, 3); + + await expect(oracle[getFunctionName4Params](GRT, amountToGet, offset, increment)).to.be.revertedWith( + "PeriodicAccumulationOracle: INSUFFICIENT_DATA" + ); + }); + + it("Should revert if the increment, amount, and offset exceeds the number of accumulations but is less than the capacity", async function () { + const capacity = 6; + const amountToGet = 2; + const offset = 1; + const increment = 2; + + await oracle[setCapacityFunctionName](GRT, capacity); + + // Sanity check the capacity + expect(await oracle[getCapacityFunctionName](GRT)).to.equal(capacity); + + // Push 3 accumulations + await oracle.stubPush(GRT, 1, 1, 1, 1, 1); + await oracle.stubPush(GRT, 2, 2, 2, 2, 2); + await oracle.stubPush(GRT, 3, 3, 3, 3, 3); + + await expect(oracle[getFunctionName4Params](GRT, amountToGet, offset, increment)).to.be.revertedWith( + "PeriodicAccumulationOracle: INSUFFICIENT_DATA" + ); + }); + + it("Should return the latest accumulation many times when increment is 0", async function () { + const capacity = 2; + const amountToGet = 2; + const offset = 0; + const increment = 0; + + await oracle[setCapacityFunctionName](GRT, capacity); + + // Sanity check the capacity + expect(await oracle[getCapacityFunctionName](GRT)).to.equal(capacity); + + // Push 2 accumulation + await oracle.stubPush(GRT, 1, 1, 1, 1, 1); + await oracle.stubPush(GRT, 2, 2, 2, 2, 2); + + const accumulations = await oracle[getFunctionName4Params](GRT, amountToGet, offset, increment); + + expect(accumulations.length).to.equal(amountToGet); + + for (let i = 0; i < amountToGet; ++i) { + expect(accumulations[i].timestamp).to.equal(2); + } + }); + + async function pushAndCheck(capacity, amountToGet, offset, increment, amountToPush) { + await oracle[setCapacityFunctionName](GRT, capacity); + + // Sanity check the capacity + expect(await oracle[getCapacityFunctionName](GRT)).to.equal(capacity); + + for (let i = 0; i < amountToPush; i++) { + await oracle.stubPush(GRT, i + 1, i + 1, i + 1, i + 1, i + 1); + } + + // Sanity check the count + expect(await oracle[getCountFunctionName](GRT)).to.equal(Math.min(amountToPush, capacity)); + + const accumulations = await oracle[getFunctionName4Params](GRT, amountToGet, offset, increment); + + expect(accumulations.length).to.equal(amountToGet); + + for (let i = 0; i < amountToGet; ++i) { + // The latest accumulation is at index 0 and will have the highest expected values + // The following accumulations will have the expected values decrementing by 1 + const expected = amountToPush - i * increment - 1 - offset + 1; + + expect(accumulations[i].timestamp).to.equal(expected); + } + } + + describe("An increment of 1", function () { + describe("An offset of 0", function () { + describe("The latest accumulation is at index 0", function () { + it("Should return the accumulations in order", async function () { + const capacity = 6; + const amountToGet = 6; + const offset = 0; + const increment = 1; + + // Push capacity + 1 accumulations so that the latest accumulation is at index 0 + const amountToPush = capacity + 1; + + await pushAndCheck(capacity, amountToGet, offset, increment, amountToPush); + }); + }); + + describe("The latest accumulation is at index n-1", function () { + it("Should return the accumulations in order", async function () { + const capacity = 6; + const amountToGet = 6; + const offset = 0; + const increment = 1; + + // Push capacity accumulations so that the latest accumulation is at index n-1 + const amountToPush = capacity; + + await pushAndCheck(capacity, amountToGet, offset, increment, amountToPush); + }); + }); + }); + + describe("An offset of 1", function () { + describe("The latest accumulation is at index 0", function () { + it("Should return the accumulations in order", async function () { + const capacity = 6; + const amountToGet = 5; + const offset = 1; + const increment = 1; + + // Push capacity + 1 accumulations so that the latest accumulation is at index 0 + const amountToPush = capacity + 1; + + await pushAndCheck(capacity, amountToGet, offset, increment, amountToPush); + }); + }); + + describe("The latest accumulation is at index n-1", function () { + it("Should return the accumulations in order", async function () { + const capacity = 6; + const amountToGet = 5; + const offset = 1; + const increment = 1; + + // Push capacity accumulations so that the latest accumulation is at index n-1 + const amountToPush = capacity; + + await pushAndCheck(capacity, amountToGet, offset, increment, amountToPush); + }); + }); + }); + }); + + describe("An increment of 2", function () { + describe("An offset of 0", function () { + describe("The latest accumulation is at index 0", function () { + it("Should return the accumulations in order", async function () { + const capacity = 6; + const amountToGet = 3; + const offset = 0; + const increment = 2; + + // Push capacity + 1 accumulations so that the latest accumulation is at index 0 + const amountToPush = capacity + 1; + + await pushAndCheck(capacity, amountToGet, offset, increment, amountToPush); + }); + }); + + describe("The latest accumulation is at index n-1", function () { + it("Should return the accumulations in order", async function () { + const capacity = 6; + const amountToGet = 3; + const offset = 0; + const increment = 2; + + // Push capacity accumulations so that the latest accumulation is at index n-1 + const amountToPush = capacity; + + await pushAndCheck(capacity, amountToGet, offset, increment, amountToPush); + }); + }); + }); + + describe("An offset of 1", function () { + describe("The latest accumulation is at index 0", function () { + it("Should return the accumulations in order", async function () { + const capacity = 6; + const amountToGet = 2; + const offset = 1; + const increment = 2; + + // Push capacity + 1 accumulations so that the latest accumulation is at index 0 + const amountToPush = capacity + 1; + + await pushAndCheck(capacity, amountToGet, offset, increment, amountToPush); + }); + }); + + describe("The latest accumulation is at index n-1", function () { + it("Should return the accumulations in order", async function () { + const capacity = 6; + const amountToGet = 2; + const offset = 1; + const increment = 2; + + // Push capacity accumulations so that the latest accumulation is at index n-1 + const amountToPush = capacity; + + await pushAndCheck(capacity, amountToGet, offset, increment, amountToPush); + }); + }); + }); + }); + }); + + describe("PeriodicAccumulationOracle#" + getFunctionName2Params, function () { + async function pushAndCheck(capacity, amountToGet, offset, increment, amountToPush) { + await oracle[setCapacityFunctionName](GRT, capacity); + + // Sanity check the capacity + expect(await oracle[getCapacityFunctionName](GRT)).to.equal(capacity); + + for (let i = 0; i < amountToPush; i++) { + await oracle.stubPush(GRT, i + 1, i + 1, i + 1, i + 1, i + 1); + } + + // Sanity check the count + expect(await oracle[getCountFunctionName](GRT)).to.equal(Math.min(amountToPush, capacity)); + + const accumulations = await oracle[getFunctionName2Params](GRT, amountToGet); + + expect(accumulations.length).to.equal(amountToGet); + + for (let i = 0; i < amountToGet; ++i) { + // The latest accumulation is at index 0 and will have the highest expected values + // The following accumulations will have the expected values decrementing by 1 + const expected = amountToPush - i * increment - 1 - offset + 1; + + expect(accumulations[i].timestamp).to.equal(expected); + } + } + + it("Default offset is 0 and increment is 1", async function () { + const capacity = 6; + const amountToGet = 6; + + // Push capacity accumulations so that the latest accumulation is at index n-1 + const amountToPush = capacity; + + await pushAndCheck(capacity, amountToGet, 0, 1, amountToPush); + }); + }); + + describe("PeriodicAccumulationOracle#" + getAtFunctionName, function () { + it("Should revert if the buffer is uninitialized", async function () { + // Sanity check the count + expect(await oracle[getCountFunctionName](GRT)).to.equal(0); + + await expect(oracle[getAtFunctionName](GRT, 0)).to.be.revertedWith( + "PeriodicAccumulationOracle: INVALID_INDEX" + ); + }); + + it("Should revert if the buffer is initialized but empty", async function () { + await oracle.stubInitializeBuffers(GRT); + + // Sanity check the count + expect(await oracle[getCountFunctionName](GRT)).to.equal(0); + + await expect(oracle[getAtFunctionName](GRT, 0)).to.be.revertedWith( + "PeriodicAccumulationOracle: INVALID_INDEX" + ); + }); + + it("Should revert if the index exceeds the number of accumulations with a full buffer", async function () { + const capacity = 6; + + await oracle[setCapacityFunctionName](GRT, capacity); + + // Push capacity accumulations + for (let i = 0; i < capacity; ++i) { + await oracle.stubPush(GRT, i + 1, i + 1, i + 1, i + 1, i + 1); + } + + // Sanity check the count + expect(await oracle[getCountFunctionName](GRT)).to.equal(capacity); + + await expect(oracle[getAtFunctionName](GRT, capacity)).to.be.revertedWith( + "PeriodicAccumulationOracle: INVALID_INDEX" + ); + }); + + it("Should revert if the index exceeds the number of accumulations but is within the capacity", async function () { + const capacity = 6; + + await oracle[setCapacityFunctionName](GRT, capacity); + + // Push capacity - 1 accumulations + for (let i = 0; i < capacity - 1; ++i) { + await oracle.stubPush(GRT, i + 1, i + 1, i + 1, i + 1, i + 1); + } + + // Sanity check the count + expect(await oracle[getCountFunctionName](GRT)).to.equal(capacity - 1); + + await expect(oracle[getAtFunctionName](GRT, capacity - 1)).to.be.revertedWith( + "PeriodicAccumulationOracle: INVALID_INDEX" + ); + }); + + it("Should return the latest accumulation when index = 0", async function () { + await oracle[setCapacityFunctionName](GRT, 2); + + // Push capacity accumulations + await oracle.stubPush(GRT, 1, 1, 1, 1, 1); + await oracle.stubPush(GRT, 2, 2, 2, 2, 2); + + // Sanity check the count + expect(await oracle[getCountFunctionName](GRT)).to.equal(2); + + const accumulation = await oracle[getAtFunctionName](GRT, 0); + + expect(accumulation.timestamp).to.equal(2); + }); + + it("Should return the latest accumulation when index = 0 and the start was just overwritten", async function () { + await oracle[setCapacityFunctionName](GRT, 2); + + // Push capacity + 1 accumulations + await oracle.stubPush(GRT, 1, 1, 1, 1, 1); + await oracle.stubPush(GRT, 2, 2, 2, 2, 2); + await oracle.stubPush(GRT, 3, 3, 3, 3, 3); + + // Sanity check the count + expect(await oracle[getCountFunctionName](GRT)).to.equal(2); + + const accumulation = await oracle[getAtFunctionName](GRT, 0); + + expect(accumulation.timestamp).to.equal(3); + }); + + it("Should return the correct accumulation when index = 1 and the latest accumulation is at the start of the buffer", async function () { + await oracle[setCapacityFunctionName](GRT, 2); + + // Push capacity + 1 accumulations + await oracle.stubPush(GRT, 1, 1, 1, 1, 1); + await oracle.stubPush(GRT, 2, 2, 2, 2, 2); + await oracle.stubPush(GRT, 3, 3, 3, 3, 3); + + // Sanity check the count + expect(await oracle[getCountFunctionName](GRT)).to.equal(2); + + const accumulation = await oracle[getAtFunctionName](GRT, 1); + + expect(accumulation.timestamp).to.equal(2); + }); + + it("Should return the correct accumulation when index = 1 and the latest accumulation is at the end of the buffer", async function () { + await oracle[setCapacityFunctionName](GRT, 2); + + // Push capacity accumulations + await oracle.stubPush(GRT, 1, 1, 1, 1, 1); + await oracle.stubPush(GRT, 2, 2, 2, 2, 2); + + // Sanity check the count + expect(await oracle[getCountFunctionName](GRT)).to.equal(2); + + const accumulation = await oracle[getAtFunctionName](GRT, 1); + + expect(accumulation.timestamp).to.equal(1); + }); + }); + }); +} + +describeHistoricalAccumulationOracleTests("Price"); +describeHistoricalAccumulationOracleTests("Liquidity"); From 8be66103667adb5660b19e5bb9f25e890b846de5 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Sun, 5 Feb 2023 20:16:14 -0800 Subject: [PATCH 23/32] Add sanity checks for some AggregatedOracle tests --- test/oracles/aggregated-oracle.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/oracles/aggregated-oracle.js b/test/oracles/aggregated-oracle.js index 99d5b24..54e8da5 100644 --- a/test/oracles/aggregated-oracle.js +++ b/test/oracles/aggregated-oracle.js @@ -3630,6 +3630,9 @@ describe("AggregatedOracle - IHistoricalOracle implementation", function () { await oracle.stubPush(GRT, i, i, i, i); } + // Sanity check the count + expect(await oracle.getObservationsCount(GRT)).to.equal(Math.min(observationsToPush, capacity)); + const observations = await oracle["getObservations(address,uint256,uint256,uint256)"]( GRT, amountToGet, @@ -3787,6 +3790,9 @@ describe("AggregatedOracle - IHistoricalOracle implementation", function () { await oracle.stubPush(GRT, i, i, i, i); } + // Sanity check the count + expect(await oracle.getObservationsCount(GRT)).to.equal(Math.min(observationsToPush, capacity)); + const observations = await oracle["getObservations(address,uint256)"](GRT, amountToGet); expect(observations.length).to.equal(amountToGet); @@ -3816,12 +3822,18 @@ describe("AggregatedOracle - IHistoricalOracle implementation", function () { describe("AggregatedOracle#getObservationAt", function () { it("Should revert if the buffer is uninitialized", async function () { + // Sanity check the observations count + expect(await oracle.getObservationsCount(GRT)).to.equal(0); + await expect(oracle.getObservationAt(GRT, 0)).to.be.revertedWith("AggregatedOracle: INVALID_INDEX"); }); it("Should revert if the buffer is initialized but empty", async function () { await oracle.stubInitializeBuffers(GRT); + // Sanity check the observations count + expect(await oracle.getObservationsCount(GRT)).to.equal(0); + await expect(oracle.getObservationAt(GRT, 0)).to.be.revertedWith("AggregatedOracle: INVALID_INDEX"); }); @@ -3835,6 +3847,9 @@ describe("AggregatedOracle - IHistoricalOracle implementation", function () { await oracle.stubPush(GRT, 1, 1, 1, 1); } + // Sanity check the observations count + expect(await oracle.getObservationsCount(GRT)).to.equal(capacity); + await expect(oracle.getObservationAt(GRT, capacity)).to.be.revertedWith("AggregatedOracle: INVALID_INDEX"); }); @@ -3848,6 +3863,9 @@ describe("AggregatedOracle - IHistoricalOracle implementation", function () { await oracle.stubPush(GRT, 1, 1, 1, 1); } + // Sanity check the observations count + expect(await oracle.getObservationsCount(GRT)).to.equal(capacity - 1); + await expect(oracle.getObservationAt(GRT, capacity - 1)).to.be.revertedWith( "AggregatedOracle: INVALID_INDEX" ); @@ -3860,6 +3878,9 @@ describe("AggregatedOracle - IHistoricalOracle implementation", function () { await oracle.stubPush(GRT, 1, 1, 1, 1); await oracle.stubPush(GRT, 2, 2, 2, 2); + // Sanity check the observations count + expect(await oracle.getObservationsCount(GRT)).to.equal(2); + const observation = await oracle.getObservationAt(GRT, 0); expect(observation.price).to.equal(2); @@ -3876,6 +3897,9 @@ describe("AggregatedOracle - IHistoricalOracle implementation", function () { await oracle.stubPush(GRT, 2, 2, 2, 2); await oracle.stubPush(GRT, 3, 3, 3, 3); + // Sanity check the observations count + expect(await oracle.getObservationsCount(GRT)).to.equal(2); + const observation = await oracle.getObservationAt(GRT, 0); expect(observation.price).to.equal(3); @@ -3892,6 +3916,9 @@ describe("AggregatedOracle - IHistoricalOracle implementation", function () { await oracle.stubPush(GRT, 2, 2, 2, 2); await oracle.stubPush(GRT, 3, 3, 3, 3); + // Sanity check the observations count + expect(await oracle.getObservationsCount(GRT)).to.equal(2); + const observation = await oracle.getObservationAt(GRT, 1); expect(observation.price).to.equal(2); @@ -3907,6 +3934,9 @@ describe("AggregatedOracle - IHistoricalOracle implementation", function () { await oracle.stubPush(GRT, 1, 1, 1, 1); await oracle.stubPush(GRT, 2, 2, 2, 2); + // Sanity check the observations count + expect(await oracle.getObservationsCount(GRT)).to.equal(2); + const observation = await oracle.getObservationAt(GRT, 1); expect(observation.price).to.equal(1); From ecf686ec3b68c5d593d8f7359af83bb9b11f9d35 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Mon, 6 Feb 2023 12:35:32 -0800 Subject: [PATCH 24/32] Fix bug in PeriodicAccumulationOracle - The accumulations from the previous update were being used rather than the accumulations from 'granularity' number of updates ago --- contracts/oracles/PeriodicAccumulationOracle.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/oracles/PeriodicAccumulationOracle.sol b/contracts/oracles/PeriodicAccumulationOracle.sol index 293a9c5..f8729af 100644 --- a/contracts/oracles/PeriodicAccumulationOracle.sol +++ b/contracts/oracles/PeriodicAccumulationOracle.sol @@ -389,6 +389,8 @@ contract PeriodicAccumulationOracle is } } + meta.end = (meta.end + 1) % meta.maxSize; + // Check if we have enough accumulations for a new observation if (meta.size >= granularity) { uint256 startIndex = meta.end < granularity @@ -433,8 +435,6 @@ contract PeriodicAccumulationOracle is ); } } - - meta.end = (meta.end + 1) % meta.maxSize; } priceAccumulationBuffers[token][meta.end] = priceAccumulation; From 2ad6e9c1cabf701c4879283758b17b611078133b Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Tue, 7 Feb 2023 12:11:43 -0800 Subject: [PATCH 25/32] Add tests for PeriodicAccumulationOracle granularity --- test/oracles/periodic-accumulation-oracle.js | 183 ++++++++++++++++++- 1 file changed, 182 insertions(+), 1 deletion(-) diff --git a/test/oracles/periodic-accumulation-oracle.js b/test/oracles/periodic-accumulation-oracle.js index 59af764..033b879 100644 --- a/test/oracles/periodic-accumulation-oracle.js +++ b/test/oracles/periodic-accumulation-oracle.js @@ -2442,11 +2442,192 @@ describe("PeriodicAccumulationOracle#supportsInterface(interfaceId)", function ( }); }); -function describeHistoricalAccumulationOracleTests(type) { +describe("PeriodicAccumulationOracle#push w/ higher granularity", function () { const MIN_UPDATE_DELAY = 1; const MAX_UPDATE_DELAY = 60; const TWO_PERCENT_CHANGE = 2000000; + const OUR_PERIOD = 4; + const OUR_GRANULARITY = 4; + + var priceAccumulator; + var liquidityAccumulator; + var oracle; + + beforeEach(async () => { + // Deploy liquidity accumulator + const liquidityAccumulatorFactory = await ethers.getContractFactory("LiquidityAccumulatorStub"); + liquidityAccumulator = await liquidityAccumulatorFactory.deploy( + USDC, + TWO_PERCENT_CHANGE, + MIN_UPDATE_DELAY, + MAX_UPDATE_DELAY + ); + await liquidityAccumulator.deployed(); + + // Deploy price accumulator + const priceAccumulatorFactory = await ethers.getContractFactory("PriceAccumulatorStub"); + priceAccumulator = await priceAccumulatorFactory.deploy( + USDC, + TWO_PERCENT_CHANGE, + MIN_UPDATE_DELAY, + MAX_UPDATE_DELAY + ); + await priceAccumulator.deployed(); + + // Deploy oracle + const oracleFactory = await ethers.getContractFactory("PeriodicAccumulationOracleStub"); + oracle = await oracleFactory.deploy( + liquidityAccumulator.address, + priceAccumulator.address, + USDC, + OUR_PERIOD, + OUR_GRANULARITY + ); + }); + + it("Doesn't update the observation until we have at least 'granularity' number of accumulations already in the buffer", async () => { + var totalPushed = 0; + + // Push OUR_GRANULARITY times + for (var i = 0; i < OUR_GRANULARITY; ++i) { + ++totalPushed; + await expect( + await oracle.stubPush(GRT, totalPushed, totalPushed, totalPushed, totalPushed, totalPushed) + ).to.not.emit(oracle, "Updated"); + } + + // Sanity check that we have OUR_GRANULARITY accumulations + expect(await oracle.getPriceAccumulationsCount(GRT)).to.equal(OUR_GRANULARITY); + expect(await oracle.getLiquidityAccumulationsCount(GRT)).to.equal(OUR_GRANULARITY); + + // Sanity check that we don't have any observations + var observation = await oracle.getLatestObservation(GRT); + expect(observation.timestamp).to.equal(0); + + // Push one more time. This should trigger an observation. + ++totalPushed; + await expect( + await oracle.stubPush(GRT, totalPushed, totalPushed, totalPushed, totalPushed, totalPushed) + ).to.emit(oracle, "Updated"); + + // Sanity check that we have OUR_GRANULARITY accumulations + expect(await oracle.getPriceAccumulationsCount(GRT)).to.equal(OUR_GRANULARITY); + expect(await oracle.getLiquidityAccumulationsCount(GRT)).to.equal(OUR_GRANULARITY); + + // Sanity check that we have an observation + observation = await oracle.getLatestObservation(GRT); + expect(observation.timestamp).to.not.equal(0); + }); + + async function verifyObservationCorrectness() { + var totalPushed = 0; + + // Push OUR_GRANULARITY times + for (var i = 0; i < OUR_GRANULARITY; ++i) { + ++totalPushed; + await oracle.stubPush(GRT, totalPushed ** 2, totalPushed, totalPushed ** 2, totalPushed ** 2, totalPushed); + } + + // Sanity check that we have OUR_GRANULARITY accumulations + expect(await oracle.getPriceAccumulationsCount(GRT)).to.equal(OUR_GRANULARITY); + expect(await oracle.getLiquidityAccumulationsCount(GRT)).to.equal(OUR_GRANULARITY); + + // We should start generating observations at the next push. We now push 4x our capacity. + const capacity = await oracle.getPriceAccumulationsCapacity(GRT); + + var totalObservations = 0; + + for (var i = 0; i < capacity * 4; ++i) { + ++totalPushed; + const pushReceipt = await oracle.stubPush( + GRT, + totalPushed ** 2, + totalPushed, + totalPushed ** 2, + totalPushed ** 2, + totalPushed + ); + ++totalObservations; + + const observation = await oracle.getLatestObservation(GRT); + + // Note: manually verified that this formula is correct + const expectedValue = OUR_GRANULARITY + totalObservations * 2; + + // Check that the observation is correct + expect(observation.price).to.equal(expectedValue); + expect(observation.tokenLiquidity).to.equal(expectedValue); + expect(observation.quoteTokenLiquidity).to.equal(expectedValue); + expect(observation.timestamp).to.equal(await blockTimestamp(pushReceipt.blockNum)); + + // Check that the event params match the observation + await expect(pushReceipt) + .to.emit(oracle, "Updated") + .withArgs( + GRT, + observation.price, + observation.tokenLiquidity, + observation.quoteTokenLiquidity, + observation.timestamp + ); + } + } + + it("Uses the correct accumulations to calculate the observation when the accumulation buffers use the default capacity", async () => { + await verifyObservationCorrectness(); + }); + + it("Uses the correct accumulations to calculate the observation when the accumulation buffers are 1.5x larger than the default capacity", async () => { + const newCapacity = Math.trunc(OUR_GRANULARITY * 1.5); + + // Sanity check that newCapacity is larger than the default + expect(newCapacity).to.be.greaterThan(OUR_GRANULARITY); + + // Set the new capacity + await oracle.setPriceAccumulationsCapacity(GRT, newCapacity); + + // Sanity check that the new capacity is set + expect(await oracle.getPriceAccumulationsCapacity(GRT)).to.equal(newCapacity); + expect(await oracle.getLiquidityAccumulationsCapacity(GRT)).to.equal(newCapacity); + + await verifyObservationCorrectness(); + }); + + it("Uses the correct accumulations to calculate the observation when the accumulation buffers are 2x larger than the default capacity", async () => { + const newCapacity = OUR_GRANULARITY * 2; + + // Sanity check that newCapacity is larger than the default + expect(newCapacity).to.be.greaterThan(OUR_GRANULARITY); + + // Set the new capacity + await oracle.setPriceAccumulationsCapacity(GRT, newCapacity); + + // Sanity check that the new capacity is set + expect(await oracle.getPriceAccumulationsCapacity(GRT)).to.equal(newCapacity); + expect(await oracle.getLiquidityAccumulationsCapacity(GRT)).to.equal(newCapacity); + + await verifyObservationCorrectness(); + }); + + it("Uses the correct accumulations to calculate the observation when the accumulation buffers are 4x larger than the default capacity", async () => { + const newCapacity = OUR_GRANULARITY * 4; + + // Sanity check that newCapacity is larger than the default + expect(newCapacity).to.be.greaterThan(OUR_GRANULARITY); + + // Set the new capacity + await oracle.setPriceAccumulationsCapacity(GRT, newCapacity); + + // Sanity check that the new capacity is set + expect(await oracle.getPriceAccumulationsCapacity(GRT)).to.equal(newCapacity); + expect(await oracle.getLiquidityAccumulationsCapacity(GRT)).to.equal(newCapacity); + + await verifyObservationCorrectness(); + }); +}); + +function describeHistoricalAccumulationOracleTests(type) { describe("PeriodicAccumulationOracle - IHistorical" + type + "AccumulationOracle implementation", function () { var priceAccumulator; var liquidityAccumulator; From 7bad1d20a01f44b45026b3921eab71657b86b0b0 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Tue, 7 Feb 2023 12:19:26 -0800 Subject: [PATCH 26/32] Fix tests --- test/oracles/periodic-accumulation-oracle.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/oracles/periodic-accumulation-oracle.js b/test/oracles/periodic-accumulation-oracle.js index 033b879..825cf14 100644 --- a/test/oracles/periodic-accumulation-oracle.js +++ b/test/oracles/periodic-accumulation-oracle.js @@ -2628,6 +2628,10 @@ describe("PeriodicAccumulationOracle#push w/ higher granularity", function () { }); function describeHistoricalAccumulationOracleTests(type) { + const MIN_UPDATE_DELAY = 1; + const MAX_UPDATE_DELAY = 60; + const TWO_PERCENT_CHANGE = 2000000; + describe("PeriodicAccumulationOracle - IHistorical" + type + "AccumulationOracle implementation", function () { var priceAccumulator; var liquidityAccumulator; From 0f3b8bb4c1fb5a002826f94d060e5436d47c5282 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Tue, 7 Feb 2023 12:26:52 -0800 Subject: [PATCH 27/32] Add buffer initialization events --- contracts/oracles/AggregatedOracle.sol | 7 +++++++ .../oracles/PeriodicAccumulationOracle.sol | 17 ++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/contracts/oracles/AggregatedOracle.sol b/contracts/oracles/AggregatedOracle.sol index 35ebe26..0793d51 100644 --- a/contracts/oracles/AggregatedOracle.sol +++ b/contracts/oracles/AggregatedOracle.sol @@ -78,6 +78,11 @@ contract AggregatedOracle is IAggregatedOracle, IHistoricalOracle, PeriodicOracl /// @param newCapacity The new capacity of the observation buffer. event ObservationCapacityIncreased(address indexed token, uint256 oldCapacity, uint256 newCapacity); + /// @notice Event emitted when an observation buffer's capacity is initialized. + /// @param token The token for which the observation buffer's capacity was initialized. + /// @param capacity The capacity of the observation buffer. + event ObservationCapacityInitialized(address indexed token, uint256 capacity); + /* * Constructors */ @@ -408,6 +413,8 @@ contract AggregatedOracle is IAggregatedOracle, IHistoricalOracle, PeriodicOracl meta.end = 0; meta.size = 0; meta.maxSize = _initialCardinality; + + emit ObservationCapacityInitialized(token, meta.maxSize); } function push(address token, ObservationLibrary.Observation memory observation) internal virtual { diff --git a/contracts/oracles/PeriodicAccumulationOracle.sol b/contracts/oracles/PeriodicAccumulationOracle.sol index f8729af..960c531 100644 --- a/contracts/oracles/PeriodicAccumulationOracle.sol +++ b/contracts/oracles/PeriodicAccumulationOracle.sol @@ -34,20 +34,25 @@ contract PeriodicAccumulationOracle is address public immutable override liquidityAccumulator; address public immutable override priceAccumulator; - mapping(address => BufferMetadata) public accumulationBufferMetadata; + mapping(address => BufferMetadata) internal accumulationBufferMetadata; - mapping(address => AccumulationLibrary.PriceAccumulator[]) public priceAccumulationBuffers; - mapping(address => AccumulationLibrary.LiquidityAccumulator[]) public liquidityAccumulationBuffers; + mapping(address => AccumulationLibrary.PriceAccumulator[]) internal priceAccumulationBuffers; + mapping(address => AccumulationLibrary.LiquidityAccumulator[]) internal liquidityAccumulationBuffers; mapping(address => ObservationLibrary.Observation) internal observations; /// @notice Event emitted when an accumulation buffer's capacity is increased past the initial capacity. /// @dev Buffer initialization does not emit an event. /// @param token The token for which the accumulation buffer's capacity was increased. - /// @param oldCapacity The previous capacity of the observation buffer. - /// @param newCapacity The new capacity of the observation buffer. + /// @param oldCapacity The previous capacity of the accumulation buffer. + /// @param newCapacity The new capacity of the accumulation buffer. event AccumulationCapacityIncreased(address indexed token, uint256 oldCapacity, uint256 newCapacity); + /// @notice Event emitted when an accumulation buffer's capacity is initialized. + /// @param token The token for which the accumulation buffer's capacity was initialized. + /// @param capacity The capacity of the accumulation buffer. + event AccumulationCapacityInitialized(address indexed token, uint256 capacity); + constructor( address liquidityAccumulator_, address priceAccumulator_, @@ -358,6 +363,8 @@ contract PeriodicAccumulationOracle is meta.end = 0; meta.size = 0; meta.maxSize = uint16(granularity); + + emit AccumulationCapacityInitialized(token, meta.maxSize); } function push( From 27b999e5de77d58eee2121d8b66e9d986ac91e05 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Tue, 7 Feb 2023 12:27:20 -0800 Subject: [PATCH 28/32] Add tests for buffer initialization events --- test/oracles/aggregated-oracle.js | 6 ++++++ test/oracles/periodic-accumulation-oracle.js | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/test/oracles/aggregated-oracle.js b/test/oracles/aggregated-oracle.js index 54e8da5..6c48778 100644 --- a/test/oracles/aggregated-oracle.js +++ b/test/oracles/aggregated-oracle.js @@ -3226,6 +3226,12 @@ describe("AggregatedOracle - IHistoricalOracle implementation", function () { await expect(oracle.stubInitializeBuffers(GRT)).to.be.revertedWith("AggregatedOracle: ALREADY_INITIALIZED"); }); + + it("Emits the correct event", async function () { + await expect(oracle.stubInitializeBuffers(GRT)) + .to.emit(oracle, "ObservationCapacityInitialized") + .withArgs(GRT, GRANULARITY); + }); }); describe("AggregatedOracle#setObservationCapacity", function () { diff --git a/test/oracles/periodic-accumulation-oracle.js b/test/oracles/periodic-accumulation-oracle.js index 825cf14..f23a2da 100644 --- a/test/oracles/periodic-accumulation-oracle.js +++ b/test/oracles/periodic-accumulation-oracle.js @@ -2677,6 +2677,12 @@ function describeHistoricalAccumulationOracleTests(type) { "PeriodicAccumulationOracle: ALREADY_INITIALIZED" ); }); + + it("Emits the correct event", async function () { + await expect(oracle.stubInitializeBuffers(GRT)) + .to.emit(oracle, "AccumulationCapacityInitialized") + .withArgs(GRT, GRANULARITY); + }); }); const setCapacityFunctionName = "set" + type + "AccumulationsCapacity"; From 6faca73aaef9e793459adc64f2d2482f560f1a4a Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Tue, 7 Feb 2023 12:45:07 -0800 Subject: [PATCH 29/32] Add event for pushing accumulations --- .../oracles/PeriodicAccumulationOracle.sol | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/contracts/oracles/PeriodicAccumulationOracle.sol b/contracts/oracles/PeriodicAccumulationOracle.sol index 960c531..6d6f4a0 100644 --- a/contracts/oracles/PeriodicAccumulationOracle.sol +++ b/contracts/oracles/PeriodicAccumulationOracle.sol @@ -53,6 +53,22 @@ contract PeriodicAccumulationOracle is /// @param capacity The capacity of the accumulation buffer. event AccumulationCapacityInitialized(address indexed token, uint256 capacity); + /// @notice Event emitted when an accumulation is pushed to the buffer. + /// @param token The token for which the accumulation was pushed. + /// @param priceCumulative The cumulative price of the token. + /// @param priceTimestamp The timestamp of the cumulative price. + /// @param tokenLiquidityCumulative The cumulative token liquidity of the token. + /// @param quoteTokenLiquidityCumulative The cumulative quote token liquidity of the token. + /// @param liquidityTimestamp The timestamp of the cumulative liquidity. + event AccumulationPushed( + address indexed token, + uint256 priceCumulative, + uint256 priceTimestamp, + uint256 tokenLiquidityCumulative, + uint256 quoteTokenLiquidityCumulative, + uint256 liquidityTimestamp + ); + constructor( address liquidityAccumulator_, address priceAccumulator_, @@ -447,6 +463,15 @@ contract PeriodicAccumulationOracle is priceAccumulationBuffers[token][meta.end] = priceAccumulation; liquidityAccumulationBuffers[token][meta.end] = liquidityAccumulation; + emit AccumulationPushed( + token, + priceAccumulation.cumulativePrice, + priceAccumulation.timestamp, + liquidityAccumulation.cumulativeTokenLiquidity, + liquidityAccumulation.cumulativeQuoteTokenLiquidity, + liquidityAccumulation.timestamp + ); + if (meta.size < meta.maxSize && meta.end == meta.size) { // We are at the end of the array and we have not yet filled it meta.size++; From 3c96ec1055ae6ce97711a863128961ff02b4a6e4 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Tue, 7 Feb 2023 12:45:26 -0800 Subject: [PATCH 30/32] Add expectation for AccumulationPushed event to be emitted --- test/oracles/periodic-accumulation-oracle.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/test/oracles/periodic-accumulation-oracle.js b/test/oracles/periodic-accumulation-oracle.js index f23a2da..a582fa0 100644 --- a/test/oracles/periodic-accumulation-oracle.js +++ b/test/oracles/periodic-accumulation-oracle.js @@ -2526,7 +2526,19 @@ describe("PeriodicAccumulationOracle#push w/ higher granularity", function () { // Push OUR_GRANULARITY times for (var i = 0; i < OUR_GRANULARITY; ++i) { ++totalPushed; - await oracle.stubPush(GRT, totalPushed ** 2, totalPushed, totalPushed ** 2, totalPushed ** 2, totalPushed); + const pushReceipt = await oracle.stubPush( + GRT, + totalPushed ** 2, + totalPushed, + totalPushed ** 2, + totalPushed ** 2, + totalPushed + ); + + // Check that the event params match the latest accumulation + await expect(pushReceipt) + .to.emit(oracle, "AccumulationPushed") + .withArgs(GRT, totalPushed ** 2, totalPushed, totalPushed ** 2, totalPushed ** 2, totalPushed); } // Sanity check that we have OUR_GRANULARITY accumulations @@ -2571,6 +2583,11 @@ describe("PeriodicAccumulationOracle#push w/ higher granularity", function () { observation.quoteTokenLiquidity, observation.timestamp ); + + // Check that the event params match the latest accumulation + await expect(pushReceipt) + .to.emit(oracle, "AccumulationPushed") + .withArgs(GRT, totalPushed ** 2, totalPushed, totalPushed ** 2, totalPushed ** 2, totalPushed); } } From 2170d8f0a72dc42701df9b9c7f09d939517fab2c Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Tue, 7 Feb 2023 16:35:45 -0800 Subject: [PATCH 31/32] Update number of tests reported in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d19ddcc..09500fa 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Adrastia Core [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) -![2904 out of 2904 tests passing](https://img.shields.io/badge/tests-2904/2904%20passing-brightgreen.svg?style=flat-square) +![3060 out of 3060 tests passing](https://img.shields.io/badge/tests-3060/3060%20passing-brightgreen.svg?style=flat-square) ![test-coverage 100%](https://img.shields.io/badge/test%20coverage-100%25-brightgreen.svg?style=flat-square) Adrastia Core is a set of Solidity smart contracts for building EVM oracle solutions. From de71a5b826b7f41829c202e1ea031f1dad4d1bc7 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Tue, 7 Feb 2023 16:43:05 -0800 Subject: [PATCH 32/32] Update version to v3.0.0-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4ff152a..1ff94c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adrastia-oracle/adrastia-core", - "version": "2.0.0", + "version": "3.0.0-beta.1", "main": "index.js", "author": "TRILEZ SOFTWARE INC.", "license": "MIT",