From e84b4bdbf3f6d23c2e082a97996a30314d90bc2a Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Wed, 24 Jul 2024 13:55:31 -0700 Subject: [PATCH 01/10] Upgrade OZ contracts to v4.9.6 --- README.md | 2 +- .../accumulators/LiquidityAccumulator.sol | 4 --- contracts/accumulators/PriceAccumulator.sol | 4 --- .../accumulators/ValueAndErrorAccumulator.sol | 5 ++- .../proto/aave/AaveV2RateAccumulator.sol | 5 +-- .../proto/aave/AaveV3RateAccumulator.sol | 5 +-- .../proto/aave/AaveV3SBAccumulator.sol | 5 +-- .../adrastia/AdrastiaPriceAccumulator.sol | 4 +-- ...AdrastiaUtilizationAndErrorAccumulator.sol | 5 +-- .../algebra/AlgebraLiquidityAccumulator.sol | 5 ++- .../proto/algebra/AlgebraPriceAccumulator.sol | 4 +-- .../BalancerV2LiquidityAccumulator.sol | 4 +-- .../BalancerV2StablePriceAccumulator.sol | 4 +-- .../BalancerV2WeightedPriceAccumulator.sol | 4 +-- .../proto/compound/CometRateAccumulator.sol | 5 +-- .../proto/compound/CometSBAccumulator.sol | 5 +-- .../compound/CompoundV2RateAccumulator.sol | 5 +-- .../compound/CompoundV2SBAccumulator.sol | 5 +-- .../proto/curve/CurveLiquidityAccumulator.sol | 6 ++-- .../proto/curve/CurvePriceAccumulator.sol | 7 ++-- .../offchain/OffchainLiquidityAccumulator.sol | 6 ++-- .../offchain/OffchainPriceAccumulator.sol | 6 ++-- .../static/StaticLiquidityAccumulator.sol | 6 ++-- .../proto/static/StaticPriceAccumulator.sol | 6 ++-- .../AlocUtilizationAndErrorAccumulator.sol | 5 +-- .../uniswap/UniswapV2LiquidityAccumulator.sol | 6 ++-- .../uniswap/UniswapV2PriceAccumulator.sol | 6 ++-- .../uniswap/UniswapV3LiquidityAccumulator.sol | 6 ++-- .../uniswap/UniswapV3PriceAccumulator.sol | 5 ++- contracts/libraries/SafeCastExt.sol | 36 ------------------- contracts/oracles/PriceVolatilityOracle.sol | 5 +-- contracts/test/SafeCastExtStub.sol | 12 ------- hardhat.config.js | 2 +- package.json | 2 +- test/libraries/safecastext.js | 32 ----------------- yarn.lock | 8 ++--- 36 files changed, 71 insertions(+), 171 deletions(-) delete mode 100644 contracts/libraries/SafeCastExt.sol delete mode 100644 contracts/test/SafeCastExtStub.sol delete mode 100644 test/libraries/safecastext.js diff --git a/README.md b/README.md index 749ce55..d76ece4 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) -![7261 out of 7261 tests passing](https://img.shields.io/badge/tests-7261/7261%20passing-brightgreen.svg?style=flat-square) +![7323 out of 7323 tests passing](https://img.shields.io/badge/tests-7323/7323%20passing-brightgreen.svg?style=flat-square) ![test-coverage >99%](https://img.shields.io/badge/test%20coverage-%3E99%25-brightgreen.svg?style=flat-square) Adrastia Core is a set of Solidity smart contracts for building EVM oracle solutions. diff --git a/contracts/accumulators/LiquidityAccumulator.sol b/contracts/accumulators/LiquidityAccumulator.sol index a271856..21b5cf1 100644 --- a/contracts/accumulators/LiquidityAccumulator.sol +++ b/contracts/accumulators/LiquidityAccumulator.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; -pragma experimental ABIEncoderV2; - import "@openzeppelin-v4/contracts/utils/introspection/IERC165.sol"; import "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; @@ -11,7 +9,6 @@ import "../interfaces/ILiquidityAccumulator.sol"; import "../interfaces/ILiquidityOracle.sol"; import "../libraries/ObservationLibrary.sol"; import "../libraries/AddressLibrary.sol"; -import "../libraries/SafeCastExt.sol"; import "../utils/SimpleQuotationMetadata.sol"; import "../strategies/averaging/IAveragingStrategy.sol"; @@ -24,7 +21,6 @@ abstract contract LiquidityAccumulator is { using AddressLibrary for address; using SafeCast for uint256; - using SafeCastExt for uint256; IAveragingStrategy public immutable averagingStrategy; diff --git a/contracts/accumulators/PriceAccumulator.sol b/contracts/accumulators/PriceAccumulator.sol index c0312f0..3899629 100644 --- a/contracts/accumulators/PriceAccumulator.sol +++ b/contracts/accumulators/PriceAccumulator.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; -pragma experimental ABIEncoderV2; - import "@openzeppelin-v4/contracts/utils/introspection/IERC165.sol"; import "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; @@ -11,7 +9,6 @@ import "../interfaces/IPriceAccumulator.sol"; import "../interfaces/IPriceOracle.sol"; import "../libraries/ObservationLibrary.sol"; import "../libraries/AddressLibrary.sol"; -import "../libraries/SafeCastExt.sol"; import "../utils/SimpleQuotationMetadata.sol"; import "../strategies/averaging/IAveragingStrategy.sol"; @@ -24,7 +21,6 @@ abstract contract PriceAccumulator is { using AddressLibrary for address; using SafeCast for uint256; - using SafeCastExt for uint256; IAveragingStrategy public immutable averagingStrategy; diff --git a/contracts/accumulators/ValueAndErrorAccumulator.sol b/contracts/accumulators/ValueAndErrorAccumulator.sol index aa601da..dede31a 100644 --- a/contracts/accumulators/ValueAndErrorAccumulator.sol +++ b/contracts/accumulators/ValueAndErrorAccumulator.sol @@ -1,14 +1,13 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; -import "./LiquidityAccumulator.sol"; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; -import "hardhat/console.sol"; +import "./LiquidityAccumulator.sol"; abstract contract ValueAndErrorAccumulator is LiquidityAccumulator { using AddressLibrary for address; using SafeCast for uint256; - using SafeCastExt for uint256; uint112 public constant ERROR_ZERO = 1e18; diff --git a/contracts/accumulators/proto/aave/AaveV2RateAccumulator.sol b/contracts/accumulators/proto/aave/AaveV2RateAccumulator.sol index 7803cb9..476e86e 100644 --- a/contracts/accumulators/proto/aave/AaveV2RateAccumulator.sol +++ b/contracts/accumulators/proto/aave/AaveV2RateAccumulator.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; + import "../../PriceAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; abstract contract IAaveV2Pool { struct ReserveData { @@ -33,7 +34,7 @@ abstract contract IAaveV2Pool { } contract AaveV2RateAccumulator is PriceAccumulator { - using SafeCastExt for uint256; + using SafeCast for uint256; address public immutable aaveV2Pool; diff --git a/contracts/accumulators/proto/aave/AaveV3RateAccumulator.sol b/contracts/accumulators/proto/aave/AaveV3RateAccumulator.sol index c3ee7b1..d30b23c 100644 --- a/contracts/accumulators/proto/aave/AaveV3RateAccumulator.sol +++ b/contracts/accumulators/proto/aave/AaveV3RateAccumulator.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; + import "../../PriceAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; abstract contract IAaveV3Pool { struct ReserveData { @@ -42,7 +43,7 @@ abstract contract IAaveV3Pool { } contract AaveV3RateAccumulator is PriceAccumulator { - using SafeCastExt for uint256; + using SafeCast for uint256; address public immutable aaveV3Pool; diff --git a/contracts/accumulators/proto/aave/AaveV3SBAccumulator.sol b/contracts/accumulators/proto/aave/AaveV3SBAccumulator.sol index 8f6ee42..6adc6e1 100644 --- a/contracts/accumulators/proto/aave/AaveV3SBAccumulator.sol +++ b/contracts/accumulators/proto/aave/AaveV3SBAccumulator.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; + import "../../LiquidityAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; abstract contract IAaveV3Pool { struct ReserveData { @@ -42,7 +43,7 @@ abstract contract IAaveV3Pool { } contract AaveV3SBAccumulator is LiquidityAccumulator { - using SafeCastExt for uint256; + using SafeCast for uint256; address public immutable aaveV3Pool; diff --git a/contracts/accumulators/proto/adrastia/AdrastiaPriceAccumulator.sol b/contracts/accumulators/proto/adrastia/AdrastiaPriceAccumulator.sol index 171f42e..74e265a 100644 --- a/contracts/accumulators/proto/adrastia/AdrastiaPriceAccumulator.sol +++ b/contracts/accumulators/proto/adrastia/AdrastiaPriceAccumulator.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; import "@openzeppelin-v4/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../PriceAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; contract AdrastiaPriceAccumulator is PriceAccumulator { using AddressLibrary for address; - using SafeCastExt for uint256; + using SafeCast for uint256; address public immutable adrastiaOracle; diff --git a/contracts/accumulators/proto/adrastia/AdrastiaUtilizationAndErrorAccumulator.sol b/contracts/accumulators/proto/adrastia/AdrastiaUtilizationAndErrorAccumulator.sol index dcda08e..0183b12 100644 --- a/contracts/accumulators/proto/adrastia/AdrastiaUtilizationAndErrorAccumulator.sol +++ b/contracts/accumulators/proto/adrastia/AdrastiaUtilizationAndErrorAccumulator.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; + import "../../ValueAndErrorAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; contract AdrastiaUtilizationAndErrorAccumulator is ValueAndErrorAccumulator { - using SafeCastExt for uint256; + using SafeCast for uint256; uint8 internal immutable _liquidityDecimals; uint256 internal immutable _decimalFactor; diff --git a/contracts/accumulators/proto/algebra/AlgebraLiquidityAccumulator.sol b/contracts/accumulators/proto/algebra/AlgebraLiquidityAccumulator.sol index b335ee6..a3b63ea 100644 --- a/contracts/accumulators/proto/algebra/AlgebraLiquidityAccumulator.sol +++ b/contracts/accumulators/proto/algebra/AlgebraLiquidityAccumulator.sol @@ -1,10 +1,9 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; import "@openzeppelin-v4/contracts/token/ERC20/IERC20.sol"; -import "../../../libraries/SafeCastExt.sol"; - import "../../LiquidityAccumulator.sol"; /// @dev Credit to Uniswap Labs under GPL-2.0-or-later license: @@ -20,7 +19,7 @@ interface IAlgebraPoolState { contract AlgebraLiquidityAccumulator is LiquidityAccumulator { using AddressLibrary for address; - using SafeCastExt for uint256; + using SafeCast for uint256; error InvalidToken(address token); diff --git a/contracts/accumulators/proto/algebra/AlgebraPriceAccumulator.sol b/contracts/accumulators/proto/algebra/AlgebraPriceAccumulator.sol index 1a84857..793ab8c 100644 --- a/contracts/accumulators/proto/algebra/AlgebraPriceAccumulator.sol +++ b/contracts/accumulators/proto/algebra/AlgebraPriceAccumulator.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; import "@openzeppelin-v4/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../PriceAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; import "../../../libraries/uniswap-lib/FullMath.sol"; /// @dev Credit to Uniswap Labs under GPL-2.0-or-later license: @@ -20,7 +20,7 @@ interface IAlgebraPoolState { contract AlgebraPriceAccumulator is PriceAccumulator { using AddressLibrary for address; - using SafeCastExt for uint256; + using SafeCast for uint256; error InvalidToken(address token); error NoLiquidity(address token); diff --git a/contracts/accumulators/proto/balancer/BalancerV2LiquidityAccumulator.sol b/contracts/accumulators/proto/balancer/BalancerV2LiquidityAccumulator.sol index 616d1c6..abf7c76 100644 --- a/contracts/accumulators/proto/balancer/BalancerV2LiquidityAccumulator.sol +++ b/contracts/accumulators/proto/balancer/BalancerV2LiquidityAccumulator.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; import "@openzeppelin-v4/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../LiquidityAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; interface IVault { function getPoolTokens( @@ -33,7 +33,7 @@ interface ILinearPool { contract BalancerV2LiquidityAccumulator is LiquidityAccumulator { using AddressLibrary for address; - using SafeCastExt for uint256; + using SafeCast for uint256; address public immutable balancerVault; address public immutable poolAddress; diff --git a/contracts/accumulators/proto/balancer/BalancerV2StablePriceAccumulator.sol b/contracts/accumulators/proto/balancer/BalancerV2StablePriceAccumulator.sol index 52bde76..0df6e70 100644 --- a/contracts/accumulators/proto/balancer/BalancerV2StablePriceAccumulator.sol +++ b/contracts/accumulators/proto/balancer/BalancerV2StablePriceAccumulator.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; import "@openzeppelin-v4/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../PriceAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; import "../../../libraries/balancer-v2/StableMath.sol"; import "../../../libraries/balancer-v2/FixedPoint.sol"; @@ -54,7 +54,7 @@ interface ILinearPool { contract BalancerV2StablePriceAccumulator is PriceAccumulator { using AddressLibrary for address; - using SafeCastExt for uint256; + using SafeCast for uint256; using FixedPoint for uint256; address public immutable balancerVault; diff --git a/contracts/accumulators/proto/balancer/BalancerV2WeightedPriceAccumulator.sol b/contracts/accumulators/proto/balancer/BalancerV2WeightedPriceAccumulator.sol index 1502875..310d67f 100644 --- a/contracts/accumulators/proto/balancer/BalancerV2WeightedPriceAccumulator.sol +++ b/contracts/accumulators/proto/balancer/BalancerV2WeightedPriceAccumulator.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; import "@openzeppelin-v4/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../PriceAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; interface IVault { function getPoolTokens( @@ -29,7 +29,7 @@ interface IWeightedPool { contract BalancerV2WeightedPriceAccumulator is PriceAccumulator { using AddressLibrary for address; - using SafeCastExt for uint256; + using SafeCast for uint256; address public immutable balancerVault; address public immutable poolAddress; diff --git a/contracts/accumulators/proto/compound/CometRateAccumulator.sol b/contracts/accumulators/proto/compound/CometRateAccumulator.sol index 939d625..f5760c3 100644 --- a/contracts/accumulators/proto/compound/CometRateAccumulator.sol +++ b/contracts/accumulators/proto/compound/CometRateAccumulator.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; + import "../../PriceAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; abstract contract IComet { function getSupplyRate(uint utilization) public view virtual returns (uint64); @@ -15,7 +16,7 @@ abstract contract IComet { } contract CometRateAccumulator is PriceAccumulator { - using SafeCastExt for uint256; + using SafeCast for uint256; address public immutable comet; diff --git a/contracts/accumulators/proto/compound/CometSBAccumulator.sol b/contracts/accumulators/proto/compound/CometSBAccumulator.sol index 0018d5a..883f77d 100644 --- a/contracts/accumulators/proto/compound/CometSBAccumulator.sol +++ b/contracts/accumulators/proto/compound/CometSBAccumulator.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; + import "../../LiquidityAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; interface IComet { struct TotalsCollateral { @@ -20,7 +21,7 @@ interface IComet { } contract CometSBAccumulator is LiquidityAccumulator { - using SafeCastExt for uint256; + using SafeCast for uint256; address public immutable comet; diff --git a/contracts/accumulators/proto/compound/CompoundV2RateAccumulator.sol b/contracts/accumulators/proto/compound/CompoundV2RateAccumulator.sol index a04f493..235f7f6 100644 --- a/contracts/accumulators/proto/compound/CompoundV2RateAccumulator.sol +++ b/contracts/accumulators/proto/compound/CompoundV2RateAccumulator.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; + import "../../PriceAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; abstract contract ICToken { function supplyRatePerBlock() external view virtual returns (uint256); @@ -11,7 +12,7 @@ abstract contract ICToken { } contract CompoundV2RateAccumulator is PriceAccumulator { - using SafeCastExt for uint256; + using SafeCast for uint256; address public immutable cToken; diff --git a/contracts/accumulators/proto/compound/CompoundV2SBAccumulator.sol b/contracts/accumulators/proto/compound/CompoundV2SBAccumulator.sol index 4fe7cd5..e92fd32 100644 --- a/contracts/accumulators/proto/compound/CompoundV2SBAccumulator.sol +++ b/contracts/accumulators/proto/compound/CompoundV2SBAccumulator.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; + import "../../LiquidityAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; import "../../../libraries/EtherAsTokenLibrary.sol"; interface IComptroller { @@ -20,7 +21,7 @@ interface ICToken { } contract CompoundV2SBAccumulator is LiquidityAccumulator { - using SafeCastExt for uint256; + using SafeCast for uint256; struct TokenInfo { ICToken cToken; diff --git a/contracts/accumulators/proto/curve/CurveLiquidityAccumulator.sol b/contracts/accumulators/proto/curve/CurveLiquidityAccumulator.sol index 9627bbf..2486822 100644 --- a/contracts/accumulators/proto/curve/CurveLiquidityAccumulator.sol +++ b/contracts/accumulators/proto/curve/CurveLiquidityAccumulator.sol @@ -1,15 +1,13 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; -pragma experimental ABIEncoderV2; - -import "../../../libraries/SafeCastExt.sol"; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; import "./ICurvePool.sol"; import "../../LiquidityAccumulator.sol"; contract CurveLiquidityAccumulator is LiquidityAccumulator { - using SafeCastExt for uint256; + using SafeCast for uint256; address public immutable curvePool; diff --git a/contracts/accumulators/proto/curve/CurvePriceAccumulator.sol b/contracts/accumulators/proto/curve/CurvePriceAccumulator.sol index 667c889..2985f1a 100644 --- a/contracts/accumulators/proto/curve/CurvePriceAccumulator.sol +++ b/contracts/accumulators/proto/curve/CurvePriceAccumulator.sol @@ -1,17 +1,14 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; -pragma experimental ABIEncoderV2; - +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; import "@openzeppelin-v4/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import "../../../libraries/SafeCastExt.sol"; - import "./ICurvePool.sol"; import "../../PriceAccumulator.sol"; contract CurvePriceAccumulator is PriceAccumulator { - using SafeCastExt for uint256; + using SafeCast for uint256; struct TokenConfig { uint8 decimals; diff --git a/contracts/accumulators/proto/offchain/OffchainLiquidityAccumulator.sol b/contracts/accumulators/proto/offchain/OffchainLiquidityAccumulator.sol index 0a70d6f..c74c24c 100644 --- a/contracts/accumulators/proto/offchain/OffchainLiquidityAccumulator.sol +++ b/contracts/accumulators/proto/offchain/OffchainLiquidityAccumulator.sol @@ -1,16 +1,14 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; -pragma experimental ABIEncoderV2; - +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; import "@openzeppelin-v4/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../LiquidityAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; contract OffchainLiquidityAccumulator is LiquidityAccumulator { using AddressLibrary for address; - using SafeCastExt for uint256; + using SafeCast for uint256; uint8 internal immutable _liquidityDecimals; diff --git a/contracts/accumulators/proto/offchain/OffchainPriceAccumulator.sol b/contracts/accumulators/proto/offchain/OffchainPriceAccumulator.sol index 54a0c29..051486c 100644 --- a/contracts/accumulators/proto/offchain/OffchainPriceAccumulator.sol +++ b/contracts/accumulators/proto/offchain/OffchainPriceAccumulator.sol @@ -1,16 +1,14 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; -pragma experimental ABIEncoderV2; - +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; import "@openzeppelin-v4/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../PriceAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; contract OffchainPriceAccumulator is PriceAccumulator { using AddressLibrary for address; - using SafeCastExt for uint256; + using SafeCast for uint256; constructor( IAveragingStrategy averagingStrategy_, diff --git a/contracts/accumulators/proto/static/StaticLiquidityAccumulator.sol b/contracts/accumulators/proto/static/StaticLiquidityAccumulator.sol index c57d9cf..7114df5 100644 --- a/contracts/accumulators/proto/static/StaticLiquidityAccumulator.sol +++ b/contracts/accumulators/proto/static/StaticLiquidityAccumulator.sol @@ -1,16 +1,14 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; -pragma experimental ABIEncoderV2; - +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; import "@openzeppelin-v4/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../LiquidityAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; contract StaticLiquidityAccumulator is LiquidityAccumulator { using AddressLibrary for address; - using SafeCastExt for uint256; + using SafeCast for uint256; uint8 internal immutable _liquidityDecimals; diff --git a/contracts/accumulators/proto/static/StaticPriceAccumulator.sol b/contracts/accumulators/proto/static/StaticPriceAccumulator.sol index 78ea7b1..e20d1fb 100644 --- a/contracts/accumulators/proto/static/StaticPriceAccumulator.sol +++ b/contracts/accumulators/proto/static/StaticPriceAccumulator.sol @@ -1,16 +1,14 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; -pragma experimental ABIEncoderV2; - +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; import "@openzeppelin-v4/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../PriceAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; contract StaticPriceAccumulator is PriceAccumulator { using AddressLibrary for address; - using SafeCastExt for uint256; + using SafeCast for uint256; uint112 internal immutable staticPrice; diff --git a/contracts/accumulators/proto/truefi/AlocUtilizationAndErrorAccumulator.sol b/contracts/accumulators/proto/truefi/AlocUtilizationAndErrorAccumulator.sol index 4ebb449..1abeb5a 100644 --- a/contracts/accumulators/proto/truefi/AlocUtilizationAndErrorAccumulator.sol +++ b/contracts/accumulators/proto/truefi/AlocUtilizationAndErrorAccumulator.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; + import "../../ValueAndErrorAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; interface IAloc { function utilization() external view returns (uint256); @@ -13,7 +14,7 @@ interface IAloc { } contract AlocUtilizationAndErrorAccumulator is ValueAndErrorAccumulator { - using SafeCastExt for uint256; + using SafeCast for uint256; uint8 internal immutable _liquidityDecimals; uint256 internal immutable _decimalFactor; diff --git a/contracts/accumulators/proto/uniswap/UniswapV2LiquidityAccumulator.sol b/contracts/accumulators/proto/uniswap/UniswapV2LiquidityAccumulator.sol index e419334..b5bd8e2 100644 --- a/contracts/accumulators/proto/uniswap/UniswapV2LiquidityAccumulator.sol +++ b/contracts/accumulators/proto/uniswap/UniswapV2LiquidityAccumulator.sol @@ -1,16 +1,14 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; -pragma experimental ABIEncoderV2; - +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; import "../../LiquidityAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; contract UniswapV2LiquidityAccumulator is LiquidityAccumulator { using AddressLibrary for address; - using SafeCastExt for uint256; + using SafeCast for uint256; address public immutable uniswapFactory; diff --git a/contracts/accumulators/proto/uniswap/UniswapV2PriceAccumulator.sol b/contracts/accumulators/proto/uniswap/UniswapV2PriceAccumulator.sol index 7f85a35..d37b8c2 100644 --- a/contracts/accumulators/proto/uniswap/UniswapV2PriceAccumulator.sol +++ b/contracts/accumulators/proto/uniswap/UniswapV2PriceAccumulator.sol @@ -1,18 +1,16 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; -pragma experimental ABIEncoderV2; - +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; import "@openzeppelin-v4/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; import "../../PriceAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; contract UniswapV2PriceAccumulator is PriceAccumulator { using AddressLibrary for address; - using SafeCastExt for uint256; + using SafeCast for uint256; address public immutable uniswapFactory; diff --git a/contracts/accumulators/proto/uniswap/UniswapV3LiquidityAccumulator.sol b/contracts/accumulators/proto/uniswap/UniswapV3LiquidityAccumulator.sol index da70749..2bf51dc 100644 --- a/contracts/accumulators/proto/uniswap/UniswapV3LiquidityAccumulator.sol +++ b/contracts/accumulators/proto/uniswap/UniswapV3LiquidityAccumulator.sol @@ -1,19 +1,17 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; -pragma experimental ABIEncoderV2; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; import "@uniswap/v3-core/contracts/interfaces/IERC20Minimal.sol"; -import "../../../libraries/SafeCastExt.sol"; - import "../../LiquidityAccumulator.sol"; contract UniswapV3LiquidityAccumulator is LiquidityAccumulator { using AddressLibrary for address; - using SafeCastExt for uint256; + using SafeCast for uint256; /// @notice The identifying key of the pool struct PoolKey { diff --git a/contracts/accumulators/proto/uniswap/UniswapV3PriceAccumulator.sol b/contracts/accumulators/proto/uniswap/UniswapV3PriceAccumulator.sol index 444756d..5cb520c 100644 --- a/contracts/accumulators/proto/uniswap/UniswapV3PriceAccumulator.sol +++ b/contracts/accumulators/proto/uniswap/UniswapV3PriceAccumulator.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; -pragma experimental ABIEncoderV2; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; @@ -9,12 +9,11 @@ import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; import "@openzeppelin-v4/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../PriceAccumulator.sol"; -import "../../../libraries/SafeCastExt.sol"; import "../../../libraries/uniswap-lib/FullMath.sol"; contract UniswapV3PriceAccumulator is PriceAccumulator { using AddressLibrary for address; - using SafeCastExt for uint256; + using SafeCast for uint256; /// @notice The identifying key of the pool struct PoolKey { diff --git a/contracts/libraries/SafeCastExt.sol b/contracts/libraries/SafeCastExt.sol deleted file mode 100644 index bf232cc..0000000 --- a/contracts/libraries/SafeCastExt.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.0 (utils/math/SafeCast.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Wrappers over Solidity's uintXX/intXX casting operators with added overflow - * checks. - * - * Downcasting from uint256/int256 in Solidity does not revert on overflow. This can - * easily result in undesired exploitation or bugs, since developers usually - * assume that overflows raise errors. `SafeCast` restores this intuition by - * reverting the transaction when such an operation overflows. - * - * Using this library instead of the unchecked operations eliminates an entire - * class of bugs, so it's recommended to use it always. - * - * Can be combined with {SafeMath} and {SignedSafeMath} to extend it to smaller types, by performing - * all math on `uint256` and `int256` and then downcasting. - */ -library SafeCastExt { - /** - * @dev Returns the downcasted uint112 from uint256, reverting on - * overflow (when the input is greater than largest uint112). - * - * Counterpart to Solidity's `uint112` operator. - * - * Requirements: - * - * - input must fit into 112 bits - */ - function toUint112(uint256 value) internal pure returns (uint112) { - require(value <= type(uint112).max, "SafeCast: value doesn't fit in 112 bits"); - return uint112(value); - } -} diff --git a/contracts/oracles/PriceVolatilityOracle.sol b/contracts/oracles/PriceVolatilityOracle.sol index ddbfc3c..df4ea15 100644 --- a/contracts/oracles/PriceVolatilityOracle.sol +++ b/contracts/oracles/PriceVolatilityOracle.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.13; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; + import "./HistoricalAggregatorOracle.sol"; import "./views/VolatilityOracleView.sol"; -import "../libraries/SafeCastExt.sol"; /** * @title PriceVolatilityOracle @@ -12,7 +13,7 @@ import "../libraries/SafeCastExt.sol"; * @dev The volatility is stored in the price field of the Observation struct. */ contract PriceVolatilityOracle is HistoricalAggregatorOracle { - using SafeCastExt for uint256; + using SafeCast for uint256; VolatilityOracleView internal immutable cView; diff --git a/contracts/test/SafeCastExtStub.sol b/contracts/test/SafeCastExtStub.sol deleted file mode 100644 index 9a89afb..0000000 --- a/contracts/test/SafeCastExtStub.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.13; - -import "../libraries/SafeCastExt.sol"; - -contract SafeCastExtStub { - using SafeCastExt for uint256; - - function stubToUint112(uint256 value) external pure returns (uint112) { - return value.toUint112(); - } -} diff --git a/hardhat.config.js b/hardhat.config.js index d83f92f..9e44cdc 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -53,7 +53,7 @@ module.exports = { hardfork: process.env.HARDHAT_HARDFORK || "shanghai", forking: { url: process.env.ETHEREUM_URL || "", - blockNumber: 17500000, + blockNumber: 20370000, }, mining: { auto: true, diff --git a/package.json b/package.json index 06aae45..a11cd7c 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "access": "public" }, "dependencies": { - "@openzeppelin-v4/contracts": "npm:@openzeppelin/contracts@4.6.0", + "@openzeppelin-v4/contracts": "npm:@openzeppelin/contracts@4.9.6", "@prb/math": "^2.5.0", "@uniswap/v2-core": "^1.0.1", "@uniswap/v3-core": "^1.0.1" diff --git a/test/libraries/safecastext.js b/test/libraries/safecastext.js deleted file mode 100644 index c5df368..0000000 --- a/test/libraries/safecastext.js +++ /dev/null @@ -1,32 +0,0 @@ -const { expect } = require("chai"); -const { ethers } = require("hardhat"); -const { BigNumber } = require("ethers"); -const MaxUint256 = ethers.constants.MaxUint256; - -describe("SafeCastExt#toUint112", function () { - var lib; - - beforeEach(async () => { - const libFactory = await ethers.getContractFactory("SafeCastExtStub"); - - lib = await libFactory.deploy(); - }); - - it("Should convert 0 to uint112 without error", async function () { - expect(await lib.stubToUint112(0)).to.equal(0); - }); - - it("Should convert (2^112)-1 to uint112 without error", async function () { - const value = BigNumber.from(2).pow(112).sub(1); - expect(await lib.stubToUint112(value)).to.equal(value); - }); - - it("Should revert with value 2^112", async function () { - const value = BigNumber.from(2).pow(112); - await expect(lib.stubToUint112(value)).to.be.reverted; - }); - - it("Should revert with value (2^256)-1", async function () { - await expect(lib.stubToUint112(MaxUint256)).to.be.reverted; - }); -}); diff --git a/yarn.lock b/yarn.lock index f35e927..de09a6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -677,10 +677,10 @@ resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-waffle/-/hardhat-waffle-2.0.6.tgz#d11cb063a5f61a77806053e54009c40ddee49a54" integrity sha512-+Wz0hwmJGSI17B+BhU/qFRZ1l6/xMW82QGXE/Gi+WTmwgJrQefuBs1lIf7hzQ1hLk6hpkvb/zwcNkpVKRYTQYg== -"@openzeppelin-v4/contracts@npm:@openzeppelin/contracts@4.6.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.6.0.tgz#c91cf64bc27f573836dba4122758b4743418c1b3" - integrity sha512-8vi4d50NNya/bQqCmaVzvHNmwHvS0OBKb7HNtuNwEE3scXWrP31fKQoGxNMT+KbzmrNZzatE3QK5p2gFONI/hg== +"@openzeppelin-v4/contracts@npm:@openzeppelin/contracts@4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.6.tgz#2a880a24eb19b4f8b25adc2a5095f2aa27f39677" + integrity sha512-xSmezSupL+y9VkHZJGDoCBpmnB2ogM13ccaYDWqJTfS3dbuHkgjuwDFUmaFauBCboQMGB/S5UqUl2y54X99BmA== "@openzeppelin/contracts@3.4.2-solc-0.7": version "3.4.2-solc-0.7" From f1f47b31211405ff3df9064e9ae759880c3cad2e Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Tue, 30 Jul 2024 15:49:14 -0700 Subject: [PATCH 02/10] Add functionality to MockOracle --- contracts/test/oracles/MockOracle.sol | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/contracts/test/oracles/MockOracle.sol b/contracts/test/oracles/MockOracle.sol index 1851451..1fe5990 100644 --- a/contracts/test/oracles/MockOracle.sol +++ b/contracts/test/oracles/MockOracle.sol @@ -4,6 +4,13 @@ pragma solidity =0.8.13; import "../../oracles/AbstractOracle.sol"; contract MockOracle is AbstractOracle { + struct PriceDecimalChange { + uint8 decimals; + bool changed; + } + + PriceDecimalChange public priceDecimalChange; + mapping(bytes32 => uint256) public callCounts; bool _needsUpdate; @@ -23,6 +30,14 @@ contract MockOracle is AbstractOracle { _liquidityDecimals = 0; } + function quoteTokenDecimals() public view virtual override(IQuoteToken, SimpleQuotationMetadata) returns (uint8) { + if (priceDecimalChange.changed) { + return priceDecimalChange.decimals; + } + + return super.quoteTokenDecimals(); + } + function getLatestObservation( address token ) public view virtual override returns (ObservationLibrary.Observation memory observation) { @@ -81,6 +96,10 @@ contract MockOracle is AbstractOracle { _liquidityDecimals = decimals; } + function stubSetPriceDecimals(uint8 decimals) public { + priceDecimalChange = PriceDecimalChange(decimals, true); + } + function liquidityDecimals() public view virtual override returns (uint8) { return _liquidityDecimals; } From 1e8b66da4b9b0c006c6fa85752d288ef52035d0a Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Tue, 30 Jul 2024 15:49:34 -0700 Subject: [PATCH 03/10] Add MockVault --- contracts/test/erc4626/MockVault.sol | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 contracts/test/erc4626/MockVault.sol diff --git a/contracts/test/erc4626/MockVault.sol b/contracts/test/erc4626/MockVault.sol new file mode 100644 index 0000000..cdfce82 --- /dev/null +++ b/contracts/test/erc4626/MockVault.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.13; + +import {ERC4626, IERC20, ERC20} from "@openzeppelin-v4/contracts/token/ERC20/extensions/ERC4626.sol"; + +contract MockVault is ERC4626 { + uint8 private _decimalOffset; + + constructor(IERC20 asset) ERC4626(asset) ERC20("Vault", "V") { + _decimalOffset = 0; + } + + function setDecimalOffset(uint8 offset) public { + _decimalOffset = offset; + } + + function _decimalsOffset() internal view virtual override returns (uint8) { + return _decimalOffset; + } +} From 94856005dc918be3fad57ad895bd72a967b80da8 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Tue, 30 Jul 2024 15:50:28 -0700 Subject: [PATCH 04/10] Add SAVPriceAccumulator and tests --- .../proto/erc4626/SAVPriceAccumulator.sol | 120 +++ .../accumulators/SAVPriceAccumulatorStub.sol | 59 ++ .../sav-price-accumulator.js.js | 782 ++++++++++++++++++ 3 files changed, 961 insertions(+) create mode 100644 contracts/accumulators/proto/erc4626/SAVPriceAccumulator.sol create mode 100644 contracts/test/accumulators/SAVPriceAccumulatorStub.sol create mode 100644 test/price-accumulator/sav-price-accumulator.js.js diff --git a/contracts/accumulators/proto/erc4626/SAVPriceAccumulator.sol b/contracts/accumulators/proto/erc4626/SAVPriceAccumulator.sol new file mode 100644 index 0000000..3110dac --- /dev/null +++ b/contracts/accumulators/proto/erc4626/SAVPriceAccumulator.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.13; + +import {IERC20Metadata} from "@openzeppelin-v4/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IERC4626} from "@openzeppelin-v4/contracts/token/ERC20/extensions/ERC4626.sol"; +import {SafeCast} from "@openzeppelin-v4/contracts/utils/math/SafeCast.sol"; + +import {PriceAccumulator} from "../../PriceAccumulator.sol"; +import {IPriceOracle} from "../../../interfaces/IPriceOracle.sol"; +import {IAveragingStrategy} from "../../../strategies/averaging/IAveragingStrategy.sol"; +import {IQuoteToken} from "../../../interfaces/IQuoteToken.sol"; +import {SimpleQuotationMetadata} from "../../../utils/SimpleQuotationMetadata.sol"; + +/** + * @title Single Asset Vault (SAV) Price Accumulator + * @author TRILEZ SOFTWARE INC. + */ +contract SAVPriceAccumulator is PriceAccumulator { + using SafeCast for uint256; + + IPriceOracle internal immutable _underlyingOracle; + + error InvalidAveragingStrategy(address strategy); + + error InvalidOracle(address oracle); + + error InvalidQuoteToken(address quoteToken); + + constructor( + IPriceOracle underlyingOracle_, + IAveragingStrategy averagingStrategy_, + address quoteToken_, + uint256 updateTheshold_, + uint256 minUpdateDelay_, + uint256 maxUpdateDelay_ + ) PriceAccumulator(averagingStrategy_, quoteToken_, updateTheshold_, minUpdateDelay_, maxUpdateDelay_) { + if (address(underlyingOracle_) == address(0)) { + revert InvalidOracle(address(underlyingOracle_)); + } + + if (address(averagingStrategy_) == address(0)) { + revert InvalidAveragingStrategy(address(averagingStrategy_)); + } + + if (quoteToken_ == address(0)) { + revert InvalidQuoteToken(quoteToken_); + } + + _underlyingOracle = underlyingOracle_; + } + + /// @inheritdoc PriceAccumulator + function canUpdate(bytes memory data) public view virtual override returns (bool) { + IERC4626 vault = IERC4626(abi.decode(data, (address))); + + if (address(vault) == address(0) || address(vault) == quoteToken) { + // Invalid token + return false; + } + + // Attempt to get the vault asset using a static call + (bool success, bytes memory assetData) = address(vault).staticcall( + abi.encodeWithSelector(vault.asset.selector) + ); + if (!success) { + return false; + } + + uint256 timeSinceLastUpdate = IPriceOracle(underlyingOracle()).timeSinceLastUpdate(assetData); + uint256 heartbeat = _heartbeat(); + if (timeSinceLastUpdate > heartbeat) { + return false; + } + + return super.canUpdate(data); + } + + function underlyingOracle() public view virtual returns (IPriceOracle) { + return _underlyingOracle; + } + + function fetchPrice(bytes memory data) internal view virtual override returns (uint112) { + IERC4626 vault = IERC4626(abi.decode(data, (address))); + uint256 vaultSupply = vault.totalSupply(); + + address asset = vault.asset(); + + uint256 totalAssets = vault.totalAssets(); + + IPriceOracle oracle = underlyingOracle(); + uint256 assetPrice = oracle.consultPrice(asset, _heartbeat()); + uint256 priceDecimals = oracle.quoteTokenDecimals(); + uint256 assetDecimals = IERC20Metadata(asset).decimals(); + + uint256 vaultDecimals = vault.decimals(); + + uint256 ourDecimals = quoteTokenDecimals(); + + uint256 sharePrice = assetPrice * totalAssets; // In terms of price decimals + asset decimals + + // Convert to our decimals and add vault decimals (we add vault decimals because we are dividing by total supply) + int256 decimalShift = int256(vaultDecimals) + + int256(ourDecimals) - + (int256(priceDecimals) + int256(assetDecimals)); + if (decimalShift > 0) { + sharePrice *= 10 ** uint256(decimalShift); + } else if (decimalShift < 0) { + sharePrice /= 10 ** uint256(-decimalShift); + } + + // If the vault has no supply, the share price is 0 + if (vaultSupply == 0) { + return 0; + } + + sharePrice /= vaultSupply; + + return sharePrice.toUint112(); + } +} diff --git a/contracts/test/accumulators/SAVPriceAccumulatorStub.sol b/contracts/test/accumulators/SAVPriceAccumulatorStub.sol new file mode 100644 index 0000000..1b166b0 --- /dev/null +++ b/contracts/test/accumulators/SAVPriceAccumulatorStub.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.13; + +import "../../accumulators/proto/erc4626/SAVPriceAccumulator.sol"; + +contract SAVPriceAccumulatorStub is SAVPriceAccumulator { + struct DecimalChange { + uint8 decimals; + bool changed; + } + + DecimalChange public decimalChange; + + constructor( + IPriceOracle underlyingOracle_, + IAveragingStrategy averagingStrategy_, + address quoteToken_, + uint256 updateTheshold_, + uint256 minUpdateDelay_, + uint256 maxUpdateDelay_ + ) + SAVPriceAccumulator( + underlyingOracle_, + averagingStrategy_, + quoteToken_, + updateTheshold_, + minUpdateDelay_, + maxUpdateDelay_ + ) + {} + + function quoteTokenDecimals() public view override(IQuoteToken, SimpleQuotationMetadata) returns (uint8) { + if (decimalChange.changed) { + return decimalChange.decimals; + } + + return super.quoteTokenDecimals(); + } + + function stubFetchPrice(address token) public view returns (uint256 price) { + return super.fetchPrice(abi.encode(token)); + } + + function changeDecimals(uint8 decimals) public { + decimalChange = DecimalChange(decimals, true); + } +} + +contract SAVPriceAccumulatorUpdater { + SAVPriceAccumulator public accumulator; + + constructor(SAVPriceAccumulator accumulator_) { + accumulator = accumulator_; + } + + function update(address token) external { + accumulator.update(abi.encode(token)); + } +} diff --git a/test/price-accumulator/sav-price-accumulator.js.js b/test/price-accumulator/sav-price-accumulator.js.js new file mode 100644 index 0000000..8f33223 --- /dev/null +++ b/test/price-accumulator/sav-price-accumulator.js.js @@ -0,0 +1,782 @@ +const { BigNumber } = require("ethers"); +const { expect } = require("chai"); +const { ethers } = require("hardhat"); +const { currentBlockTimestamp, blockTimestamp } = require("../../src/time"); + +const AddressZero = ethers.constants.AddressZero; + +const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; +const GRT = "0xc944E90C64B2c07662A292be6244BDf05Cda44a7"; + +const DEFAULT_PRECISION_DECIMALS = 8; +const DEFAULT_UPDATE_THRESHOLD = ethers.utils.parseUnits("0.02", DEFAULT_PRECISION_DECIMALS); // 2% +const DEFAULT_UPDATE_DELAY = 10; +const DEFAULT_HEARTBEAT = 8 * 60 * 60; + +async function createDefaultDeployment(overrides, contractName = "SAVPriceAccumulatorStub") { + var averagingStrategyAddress = overrides?.averagingStrategyAddress; + if (!averagingStrategyAddress) { + const averagingStrategyFactory = await ethers.getContractFactory("ArithmeticAveraging"); + const averagingStrategy = await averagingStrategyFactory.deploy(); + await averagingStrategy.deployed(); + + averagingStrategyAddress = averagingStrategy.address; + } + + var token = overrides?.token || GRT; + + var quoteToken = overrides?.quoteToken || USDC; + + var oracleAddress = overrides?.oracleAddress; + var oracle; + if (!oracleAddress) { + const oracleFactory = await ethers.getContractFactory("MockOracle"); + oracle = await oracleFactory.deploy(quoteToken); + await oracle.deployed(); + + oracleAddress = oracle.address; + } + + var vaultAddress = overrides?.vaultAddress; + var vault; + if (!vaultAddress) { + const vaultFactory = await ethers.getContractFactory("MockVault"); + vault = await vaultFactory.deploy(token); + await vault.deployed(); + } + + var updateThreshold = overrides?.updateThreshold ?? DEFAULT_UPDATE_THRESHOLD; + var minUpdateDelay = overrides?.minUpdateDelay ?? DEFAULT_UPDATE_DELAY; + var maxUpdateDelay = overrides?.maxUpdateDelay ?? DEFAULT_HEARTBEAT; + + const factory = await ethers.getContractFactory(contractName); + + const accumulator = await factory.deploy( + oracleAddress, + averagingStrategyAddress, + quoteToken, + updateThreshold, + minUpdateDelay, + maxUpdateDelay + ); + + return { + accumulator: accumulator, + averagingStrategy: averagingStrategyAddress, + quoteToken: quoteToken, + updateThreshold: updateThreshold, + updateDelay: minUpdateDelay, + heartbeat: maxUpdateDelay, + oracle: oracle, + vault: vault, + }; +} + +describe("SAVPriceAccumulator#constructor", function () { + it("Deploys correctly with defaults", async function () { + const { accumulator, averagingStrategy, quoteToken, updateThreshold, updateDelay, heartbeat, oracle } = + await createDefaultDeployment(); + + expect(await accumulator.averagingStrategy()).to.equal(averagingStrategy); + expect(await accumulator.quoteToken()).to.equal(quoteToken); + + expect(await accumulator.updateThreshold()).to.equal(updateThreshold); + expect(await accumulator.updateDelay()).to.equal(updateDelay); + expect(await accumulator.heartbeat()).to.equal(heartbeat); + expect(await accumulator.underlyingOracle()).to.equal(oracle.address); + }); + + it("Reverts if the averaging strategy address is zero", async function () { + await expect(createDefaultDeployment({ averagingStrategyAddress: AddressZero })).to.be.revertedWith( + "InvalidAveragingStrategy" + ); + }); + + it("Reverts if the quote token address is zero", async function () { + await expect(createDefaultDeployment({ quoteToken: AddressZero })).to.be.revertedWith("InvalidQuoteToken"); + }); + + it("Reverts if the oracle address is zero", async function () { + await expect(createDefaultDeployment({ oracleAddress: AddressZero })).to.be.revertedWith("InvalidOracle"); + }); +}); + +describe("SAVPriceAccumulator#quoteTokenName", function () { + const tokenNames = ["USD Coin", "Wrapped Ether"]; + + for (const tokenName of tokenNames) { + it(`Returns the correct name for ${tokenName}`, async function () { + const tokenFactory = await ethers.getContractFactory("NotAnErc20"); + + const token = await tokenFactory.deploy(tokenName, "SYMBOL", 18); + await token.deployed(); + + const { accumulator } = await createDefaultDeployment({ quoteToken: token.address }); + + expect(await accumulator.quoteTokenName()).to.equal(tokenName); + }); + } +}); + +describe("SAVPriceAccumulator#quoteTokenSymbol", function () { + const tokenSymbols = ["USDC", "WETH"]; + + for (const tokenSymbol of tokenSymbols) { + it(`Returns the correct symbol for ${tokenSymbol}`, async function () { + const tokenFactory = await ethers.getContractFactory("NotAnErc20"); + + const token = await tokenFactory.deploy("USD Coin", tokenSymbol, 18); + await token.deployed(); + + const { accumulator } = await createDefaultDeployment({ quoteToken: token.address }); + + expect(await accumulator.quoteTokenSymbol()).to.equal(tokenSymbol); + }); + } +}); + +describe("SAVPriceAccumulator#quoteTokenDecimals", function () { + const tokenDecimalss = [0, 1, 6, 8, 18]; + + for (const tokenDecimals of tokenDecimalss) { + it(`Returns the correct decimals for ${tokenDecimals}`, async function () { + const tokenFactory = await ethers.getContractFactory("NotAnErc20"); + + const token = await tokenFactory.deploy("USD Coin", "USDC", tokenDecimals); + await token.deployed(); + + const { accumulator } = await createDefaultDeployment({ quoteToken: token.address }); + + expect(await accumulator.quoteTokenDecimals()).to.equal(tokenDecimals); + }); + } +}); + +describe("SAVPriceAccumulator#canUpdate", function () { + var tokenFactory; + + var deployment; + var quoteToken; + var token; + var signer; + + before(async function () { + tokenFactory = await ethers.getContractFactory("FakeERC20"); + signer = (await ethers.getSigners())[0]; + }); + + beforeEach(async function () { + token = await tokenFactory.deploy("Token 1", "TK1", 18); + await token.deployed(); + quoteToken = await tokenFactory.deploy("Token 2", "TK2", 18); + await quoteToken.deployed(); + + deployment = await createDefaultDeployment({ + token: token.address, + quoteToken: quoteToken.address, + }); + }); + + it("Returns false if the token address is zero", async function () { + var updateData = ethers.utils.defaultAbiCoder.encode(["address"], [AddressZero]); + expect(await deployment.accumulator.canUpdate(updateData)).to.equal(false); + }); + + it("Returns false if the token address is the quote token address", async function () { + var updateData = ethers.utils.defaultAbiCoder.encode(["address"], [quoteToken.address]); + expect(await deployment.accumulator.canUpdate(updateData)).to.equal(false); + }); + + it("Returns false if the token is not a vault", async function () { + var updateData = ethers.utils.defaultAbiCoder.encode(["address"], [USDC]); + expect(await deployment.accumulator.canUpdate(updateData)).to.equal(false); + }); + + it("Returns false if the underlying oracle's observation is old (age=heartbeat+2)", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + const timestamp = await currentBlockTimestamp(); + + // Set the price + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + // Set the blockchain's timestamp to be heartbeat+1 seconds in the future + await hre.timeAndMine.setTime(timestamp + deployment.heartbeat + 2); + + var updateData = ethers.utils.defaultAbiCoder.encode(["address"], [deployment.vault.address]); + expect(await deployment.accumulator.canUpdate(updateData)).to.equal(false); + }); + + it("Returns false if the underlying oracle's observation is old (age=heartbeat+1)", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + const timestamp = await currentBlockTimestamp(); + + // Set the price + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + // Set the blockchain's timestamp to be heartbeat+1 seconds in the future + await hre.timeAndMine.setTime(timestamp + deployment.heartbeat + 1); + + var updateData = ethers.utils.defaultAbiCoder.encode(["address"], [deployment.vault.address]); + expect(await deployment.accumulator.canUpdate(updateData)).to.equal(false); + }); + + it("Returns true if the underlying oracle's observation is fresh (age=heartbeat)", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + const timestamp = await currentBlockTimestamp(); + + // Set the price + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + // Set the blockchain's timestamp to be heartbeat+1 seconds in the future + await hre.timeAndMine.setTime(timestamp + deployment.heartbeat); + + var updateData = ethers.utils.defaultAbiCoder.encode(["address"], [deployment.vault.address]); + expect(await deployment.accumulator.canUpdate(updateData)).to.equal(true); + }); + + it("Returns true if the underlying oracle's observation is fresh (age=heartbeat-1)", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + const timestamp = await currentBlockTimestamp(); + + // Set the price + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + // Set the blockchain's timestamp to be heartbeat+1 seconds in the future + await hre.timeAndMine.setTime(timestamp + deployment.heartbeat - 1); + + var updateData = ethers.utils.defaultAbiCoder.encode(["address"], [deployment.vault.address]); + expect(await deployment.accumulator.canUpdate(updateData)).to.equal(true); + }); + + it("Returns true if the underlying oracle's observation is fresh (age=0)", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + const timestamp = (await currentBlockTimestamp()) + 10; + + // Set the price + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + // Set the blockchain's timestamp to be heartbeat+1 seconds in the future + await hre.timeAndMine.setTime(timestamp); + + var updateData = ethers.utils.defaultAbiCoder.encode(["address"], [deployment.vault.address]); + expect(await deployment.accumulator.canUpdate(updateData)).to.equal(true); + }); + + it("Returns true if the underlying oracle's observation is fresh (age=1)", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + const timestamp = (await currentBlockTimestamp()) + 10; + + // Set the price + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + // Set the blockchain's timestamp to be heartbeat+1 seconds in the future + await hre.timeAndMine.setTime(timestamp + 1); + + var updateData = ethers.utils.defaultAbiCoder.encode(["address"], [deployment.vault.address]); + expect(await deployment.accumulator.canUpdate(updateData)).to.equal(true); + }); +}); + +describe("SAVPriceAccumulator#fetchPrice", function () { + var tokenFactory; + + var deployment; + var quoteToken; + var token; + var signer; + + before(async function () { + tokenFactory = await ethers.getContractFactory("FakeERC20"); + signer = (await ethers.getSigners())[0]; + }); + + beforeEach(async function () { + token = await tokenFactory.deploy("Token 1", "TK1", 18); + await token.deployed(); + quoteToken = await tokenFactory.deploy("Token 2", "TK2", 18); + await quoteToken.deployed(); + + deployment = await createDefaultDeployment({ + token: token.address, + quoteToken: quoteToken.address, + }); + }); + + describe("Standard tests", function () { + it("Returns the correct price", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + const timestamp = await currentBlockTimestamp(); + + // Set the price + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + expect(await deployment.accumulator.stubFetchPrice(deployment.vault.address)).to.equal(price); + }); + + it("Reverts if the underlying oracle's observation is old (age=heartbeat+2)", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const timestamp = await currentBlockTimestamp(); + + // Set the observation + await deployment.oracle.stubSetObservation(token.address, 2, 3, 5, timestamp); + + // Set the blockchain's timestamp to be heartbeat+1 seconds in the future + await hre.timeAndMine.setTime(timestamp + deployment.heartbeat + 2); + + await expect(deployment.accumulator.stubFetchPrice(deployment.vault.address)).to.be.revertedWith( + "AbstractOracle: RATE_TOO_OLD" + ); + }); + + it("Reverts if the underlying oracle's observation is old (age=heartbeat+1)", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const timestamp = await currentBlockTimestamp(); + + // Set the observation + await deployment.oracle.stubSetObservation(token.address, 2, 3, 5, timestamp); + + // Set the blockchain's timestamp to be heartbeat+1 seconds in the future + await hre.timeAndMine.setTime(timestamp + deployment.heartbeat + 1); + + await expect(deployment.accumulator.stubFetchPrice(deployment.vault.address)).to.be.revertedWith( + "AbstractOracle: RATE_TOO_OLD" + ); + }); + + it("Returns the correct price if the underlying oracle's observation is fresh (age=heartbeat)", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + const timestamp = await currentBlockTimestamp(); + + // Set the observation + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + // Set the blockchain's timestamp to be heartbeat+1 seconds in the future + await hre.timeAndMine.setTime(timestamp + deployment.heartbeat); + + expect(await deployment.accumulator.stubFetchPrice(deployment.vault.address)).to.equal(price); + }); + + it("Returns the correct price if the underlying oracle's observation is fresh (age=heartbeat-1)", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + const timestamp = await currentBlockTimestamp(); + + // Set the observation + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + // Set the blockchain's timestamp to be heartbeat+1 seconds in the future + await hre.timeAndMine.setTime(timestamp + deployment.heartbeat - 1); + + expect(await deployment.accumulator.stubFetchPrice(deployment.vault.address)).to.equal(price); + }); + + it("Returns the correct price if the underlying oracle's observation is fresh (age=0)", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + const timestamp = (await currentBlockTimestamp()) + 1; + + // Set the observation + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + // Set the blockchain's timestamp to be heartbeat+1 seconds in the future + await hre.timeAndMine.setTime(timestamp); + + expect(await deployment.accumulator.stubFetchPrice(deployment.vault.address)).to.equal(price); + }); + + it("Returns the correct price if the underlying oracle's observation is fresh (age=1)", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + const timestamp = (await currentBlockTimestamp()) + 1; + + // Set the observation + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + // Set the blockchain's timestamp to be heartbeat+1 seconds in the future + await hre.timeAndMine.setTime(timestamp + 1); + + expect(await deployment.accumulator.stubFetchPrice(deployment.vault.address)).to.equal(price); + }); + }); + + describe("Edge cases", function () { + it("Returns 0 if the vault has no supply", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + const timestamp = await currentBlockTimestamp(); + + // Set the observation + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + expect(await deployment.accumulator.stubFetchPrice(deployment.vault.address)).to.equal(0); + }); + }); + + describe("Differing decimal places (1:1 exchange ratio)", function () { + it("The vault uses 20 decimals", async function () { + await deployment.vault.setDecimalOffset(2); + + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + const timestamp = await currentBlockTimestamp(); + + // Set the observation + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + // This changes the number of decimals in one whole unit, and the price reflects one whole unit + // So this should result in the price being the same. + const expectedPrice = price; + + expect(await deployment.accumulator.stubFetchPrice(deployment.vault.address)).to.equal(expectedPrice); + }); + + it("The accumulator uses 20 decimals", async function () { + await deployment.accumulator.changeDecimals(20); + + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + const timestamp = await currentBlockTimestamp(); + + // Set the observation + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + // The quote token decimals have changed by +2, so the price should be multiplied by 10^2 + const expectedPrice = price.mul(BigNumber.from(10).pow(2)); + + expect(await deployment.accumulator.stubFetchPrice(deployment.vault.address)).to.equal(expectedPrice); + }); + + it("The accumulator uses 16 decimals", async function () { + await deployment.accumulator.changeDecimals(16); + + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + const timestamp = await currentBlockTimestamp(); + + // Set the observation + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + // The quote token decimals have changed by -2, so the price should be divided by 10^2 + const expectedPrice = price.div(BigNumber.from(10).pow(2)); + + expect(await deployment.accumulator.stubFetchPrice(deployment.vault.address)).to.equal(expectedPrice); + }); + + it("The underlying oracle uses 8 decimals", async function () { + await deployment.oracle.stubSetPriceDecimals(8); + + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + const timestamp = await currentBlockTimestamp(); + + // Set the observation + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + // Price is in 8 decimals, so the price should be multiplied by 10^10 + const expectedPrice = price.mul(BigNumber.from(10).pow(10)); + + expect(await deployment.accumulator.stubFetchPrice(deployment.vault.address)).to.equal(expectedPrice); + }); + }); + + describe("Differing exchange ratios", function () { + it("Double the number of assets to shares", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + // Mint some tokens to the vault + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + await token.transfer(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + const timestamp = await currentBlockTimestamp(); + + // Set the observation + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + // Value should be double + const expectedPrice = price.mul(2); + + expect(await deployment.accumulator.stubFetchPrice(deployment.vault.address)).to.equal(expectedPrice); + }); + }); +}); + +describe("SAVPriceAccumulator#update", function () { + describe("Standard tests", function () { + var tokenFactory; + + var deployment; + var quoteToken; + var token; + var signer; + + before(async function () { + tokenFactory = await ethers.getContractFactory("FakeERC20"); + signer = (await ethers.getSigners())[0]; + }); + + beforeEach(async function () { + token = await tokenFactory.deploy("Token 1", "TK1", 18); + await token.deployed(); + quoteToken = await tokenFactory.deploy("Token 2", "TK2", 18); + await quoteToken.deployed(); + + deployment = await createDefaultDeployment({ + token: token.address, + quoteToken: quoteToken.address, + }); + }); + + it("Blocks smart contracts from updating", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const timestamp = await currentBlockTimestamp(); + + const callerFactory = await ethers.getContractFactory("SAVPriceAccumulatorUpdater"); + const caller = await callerFactory.deploy(deployment.accumulator.address); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + // Set the observation + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + await expect(caller.update(deployment.vault.address)).to.be.revertedWith("PriceAccumulator: MUST_BE_EOA"); + }); + + it("Reverts if we try to update with only the token address as the update data", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const timestamp = await currentBlockTimestamp(); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + // Set the observation + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + await expect( + deployment.accumulator.update( + ethers.utils.defaultAbiCoder.encode(["address"], [deployment.vault.address]) + ) + ).to.be.reverted; + }); + + it("Performs validation and emits the event", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const timestamp = await currentBlockTimestamp(); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + // Set the observation + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + const updateTx = await deployment.accumulator.update( + ethers.utils.defaultAbiCoder.encode( + ["address", "uint256", "uint256"], + [deployment.vault.address, price, timestamp] + ) + ); + const receipt = await updateTx.wait(); + + const updateTimestamp = await blockTimestamp(receipt.blockNumber); + + // Expect it to emit ValidationPerformed + expect(receipt) + .to.emit(deployment.accumulator, "ValidationPerformed") + .withArgs(deployment.vault.address, price, price, updateTimestamp, timestamp, true); + + // Expect it to emit Updated + expect(receipt).to.emit(deployment.accumulator, "Updated").withArgs(deployment.vault.address, price); + }); + + it("Doesn't update if time validation fails", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const timestamp = await currentBlockTimestamp(); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + // Set the observation + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + const providedTimestamp = 1; + + const updateTx = await deployment.accumulator.update( + ethers.utils.defaultAbiCoder.encode( + ["address", "uint256", "uint256"], + [deployment.vault.address, price, providedTimestamp] + ) + ); + const receipt = await updateTx.wait(); + + const updateTimestamp = await blockTimestamp(receipt.blockNumber); + + // Expect it to emit ValidationPerformed + expect(receipt) + .to.emit(deployment.accumulator, "ValidationPerformed") + .withArgs(deployment.vault.address, price, price, updateTimestamp, providedTimestamp, false); + + // Expect it to not emit Updated + expect(receipt).to.not.emit(deployment.accumulator, "Updated"); + }); + + it("Doesn't update if price validation fails", async function () { + const quoteTokenDecimals = await deployment.accumulator.quoteTokenDecimals(); + + await token.approve(deployment.vault.address, ethers.utils.parseUnits("1000", quoteTokenDecimals)); + await deployment.vault.deposit(ethers.utils.parseUnits("1000", quoteTokenDecimals), signer.address); + + const timestamp = await currentBlockTimestamp(); + + const price = ethers.utils.parseUnits("1.23", quoteTokenDecimals); + + // Set the observation + await deployment.oracle.stubSetObservation(token.address, price, 3, 5, timestamp); + + const providedPrice = 1; + + const updateTx = await deployment.accumulator.update( + ethers.utils.defaultAbiCoder.encode( + ["address", "uint256", "uint256"], + [deployment.vault.address, providedPrice, timestamp] + ) + ); + const receipt = await updateTx.wait(); + + const updateTimestamp = await blockTimestamp(receipt.blockNumber); + + // Expect it to emit ValidationPerformed + expect(receipt) + .to.emit(deployment.accumulator, "ValidationPerformed") + .withArgs(deployment.vault.address, price, providedPrice, updateTimestamp, timestamp, false); + + // Expect it to not emit Updated + expect(receipt).to.not.emit(deployment.accumulator, "Updated"); + }); + }); +}); From f2170d4b6d80b8391d2633fec10b57067725fe3f Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Tue, 30 Jul 2024 15:50:58 -0700 Subject: [PATCH 05/10] Add Adapter vault price consulation script --- scripts/consult-adapter-vault.js | 164 +++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 scripts/consult-adapter-vault.js diff --git a/scripts/consult-adapter-vault.js b/scripts/consult-adapter-vault.js new file mode 100644 index 0000000..efa7ccd --- /dev/null +++ b/scripts/consult-adapter-vault.js @@ -0,0 +1,164 @@ +const hre = require("hardhat"); +const { currentBlockTimestamp } = require("../src/time"); + +const ethers = hre.ethers; + +const adapterEzEthVault = "0x945f0cf0DDb3A20a4737d3e1f3cA43DE9C185440"; +const adapterRswEthVault = "0xe6cD0b7800cA3e297b8fBd7697Df9E9F6A27f0F5"; + +const ezEthAddress = "0xbf5495Efe5DB9ce00f80364C8B423567e58d2110"; +const rswEthAddress = "0xFAe103DC9cf190eD75350761e95403b7b8aFa6c0"; + +const ezEthFeedAddress = "0x636A000262F6aA9e1F094ABF0aD8f645C44f641C"; // Chainlink ezETH/ETH +const rswEthFeedAddress = "0xb613CfebD0b6e95abDDe02677d6bC42394FdB857"; + +const wethAddress = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; +const wethFeedAddress = "0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419"; + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function createContract(name, ...deploymentArgs) { + const contractFactory = await ethers.getContractFactory(name); + + const contract = await contractFactory.deploy(...deploymentArgs); + + await contract.deployed(); + + return contract; +} + +async function createChainlinkOracleView(feedAddress, tokenAddress, quoteTokenAddress) { + const oracle = await createContract("ChainlinkOracleView", feedAddress, tokenAddress, quoteTokenAddress); + + return oracle; +} + +async function createSAVPriceAccumulator(underlyingOracle, averagingStrategy, quoteTokenAddress) { + const updateTheshold = 2000000; // 2% change -> update + const minUpdateDelay = 5; // At least 5 seconds between every update + const maxUpdateDelay = 24 * 60 * 60; // 24 hour heartbeat + + const priceAccumulator = await createContract( + "SAVPriceAccumulator", + underlyingOracle, + averagingStrategy, + quoteTokenAddress, + updateTheshold, + minUpdateDelay, + maxUpdateDelay + ); + + return priceAccumulator; +} + +async function main() { + const vaultAddress = adapterRswEthVault; + const underlyingAsset = rswEthAddress; + const quoteToken = wethAddress; + + const underlyingOracle = await createChainlinkOracleView(rswEthFeedAddress, underlyingAsset, quoteToken); + + const underlyingOracleDecimals = await underlyingOracle.quoteTokenDecimals(); + + const underlyingAssetContract = await ethers.getContractAt("ERC20", underlyingAsset); + const underlyingAssetSymbol = await underlyingAssetContract.symbol(); + const underlyingAssetDecimals = await underlyingAssetContract.decimals(); + + const averagingStrategy = await createContract("GeometricAveraging"); + + const savPriceOracle = await createSAVPriceAccumulator( + underlyingOracle.address, + averagingStrategy.address, + quoteToken + ); + + const savPriceOracleDecimals = await savPriceOracle.quoteTokenDecimals(); + const savPriceOracleSymbol = await savPriceOracle.quoteTokenSymbol(); + + const vault = await ethers.getContractAt("ERC4626", vaultAddress); + const vaultDecimals = await vault.decimals(); + const vaultSymbol = await vault.symbol(); + + const checkUpdateData = ethers.utils.defaultAbiCoder.encode(["address"], [vaultAddress]); + + while (true) { + const underlyingAssetPrice = await underlyingOracle["consultPrice(address)"](underlyingAsset); + + // Convert to a human-readable format + const underlyingAssetPriceFormatted = ethers.utils.commify( + ethers.utils.formatUnits(underlyingAssetPrice, underlyingOracleDecimals) + ); + + console.log( + "\u001b[" + 32 + "m" + "Price(%s) = %s" + "\u001b[0m", + underlyingAssetSymbol, + underlyingAssetPriceFormatted + ); + + try { + // Update the price accumulator + if (await savPriceOracle.canUpdate(checkUpdateData)) { + const price = await savPriceOracle["consultPrice(address,uint256)"](vaultAddress, 0); + const currentTime = await currentBlockTimestamp(); + + const paUpdateData = ethers.utils.defaultAbiCoder.encode( + ["address", "uint", "uint"], + [vaultAddress, price, currentTime] + ); + + const updateTx = await savPriceOracle.update(paUpdateData); + const updateReceipt = await updateTx.wait(); + + console.log( + "\u001b[" + + 93 + + "m" + + "Price accumulator updated. Gas used = " + + updateReceipt["gasUsed"] + + "\u001b[0m" + ); + } + } catch (e) {} + + try { + const vaultSharePrice = await savPriceOracle["consultPrice(address)"](vaultAddress); + + // Convert to a human-readable format + const vaultSharePriceFormatted = ethers.utils + .commify(ethers.utils.formatUnits(vaultSharePrice, savPriceOracleDecimals)) + .concat(" ", savPriceOracleSymbol); + + console.log("\u001b[" + 32 + "m" + "Price(%s) = %s" + "\u001b[0m", vaultSymbol, vaultSharePriceFormatted); + + const vaultTotalSupply = await vault.totalSupply(); + const vaultDecimals = await vault.decimals(); + + const vaultTvl = vaultTotalSupply.mul(vaultSharePrice); + + const tvlDecimals = savPriceOracleDecimals + vaultDecimals; + + // Convert to a human-readable format + const vaultTvlFormatted = ethers.utils + .commify(ethers.utils.formatUnits(vaultTvl, tvlDecimals)) + .concat(" ", savPriceOracleSymbol); + + console.log("\u001b[" + 32 + "m" + "TVL = %s" + "\u001b[0m", vaultTvlFormatted); + } catch (e) {} + + await sleep(1000); + + // Keep mining blocks so that block.timestamp updates + await hre.network.provider.send("evm_mine"); + } +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); From 464f89e84733a22ebf02bf50250c33792f8b3e2a Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Fri, 2 Aug 2024 15:18:33 -0700 Subject: [PATCH 06/10] Rename underlyingOracle to underlyingAssetOracle --- .../proto/erc4626/SAVPriceAccumulator.sol | 12 ++++++------ test/price-accumulator/sav-price-accumulator.js.js | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/accumulators/proto/erc4626/SAVPriceAccumulator.sol b/contracts/accumulators/proto/erc4626/SAVPriceAccumulator.sol index 3110dac..510d432 100644 --- a/contracts/accumulators/proto/erc4626/SAVPriceAccumulator.sol +++ b/contracts/accumulators/proto/erc4626/SAVPriceAccumulator.sol @@ -18,7 +18,7 @@ import {SimpleQuotationMetadata} from "../../../utils/SimpleQuotationMetadata.so contract SAVPriceAccumulator is PriceAccumulator { using SafeCast for uint256; - IPriceOracle internal immutable _underlyingOracle; + IPriceOracle internal immutable _underlyingAssetOracle; error InvalidAveragingStrategy(address strategy); @@ -46,7 +46,7 @@ contract SAVPriceAccumulator is PriceAccumulator { revert InvalidQuoteToken(quoteToken_); } - _underlyingOracle = underlyingOracle_; + _underlyingAssetOracle = underlyingOracle_; } /// @inheritdoc PriceAccumulator @@ -66,7 +66,7 @@ contract SAVPriceAccumulator is PriceAccumulator { return false; } - uint256 timeSinceLastUpdate = IPriceOracle(underlyingOracle()).timeSinceLastUpdate(assetData); + uint256 timeSinceLastUpdate = IPriceOracle(underlyingAssetOracle()).timeSinceLastUpdate(assetData); uint256 heartbeat = _heartbeat(); if (timeSinceLastUpdate > heartbeat) { return false; @@ -75,8 +75,8 @@ contract SAVPriceAccumulator is PriceAccumulator { return super.canUpdate(data); } - function underlyingOracle() public view virtual returns (IPriceOracle) { - return _underlyingOracle; + function underlyingAssetOracle() public view virtual returns (IPriceOracle) { + return _underlyingAssetOracle; } function fetchPrice(bytes memory data) internal view virtual override returns (uint112) { @@ -87,7 +87,7 @@ contract SAVPriceAccumulator is PriceAccumulator { uint256 totalAssets = vault.totalAssets(); - IPriceOracle oracle = underlyingOracle(); + IPriceOracle oracle = underlyingAssetOracle(); uint256 assetPrice = oracle.consultPrice(asset, _heartbeat()); uint256 priceDecimals = oracle.quoteTokenDecimals(); uint256 assetDecimals = IERC20Metadata(asset).decimals(); diff --git a/test/price-accumulator/sav-price-accumulator.js.js b/test/price-accumulator/sav-price-accumulator.js.js index 8f33223..ddc2b72 100644 --- a/test/price-accumulator/sav-price-accumulator.js.js +++ b/test/price-accumulator/sav-price-accumulator.js.js @@ -83,7 +83,7 @@ describe("SAVPriceAccumulator#constructor", function () { expect(await accumulator.updateThreshold()).to.equal(updateThreshold); expect(await accumulator.updateDelay()).to.equal(updateDelay); expect(await accumulator.heartbeat()).to.equal(heartbeat); - expect(await accumulator.underlyingOracle()).to.equal(oracle.address); + expect(await accumulator.underlyingAssetOracle()).to.equal(oracle.address); }); it("Reverts if the averaging strategy address is zero", async function () { From dc05282b6f2f413d0c770524dcd1678f3552ef69 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Fri, 2 Aug 2024 15:32:35 -0700 Subject: [PATCH 07/10] Update version to v4.9.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a11cd7c..891f1ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adrastia-oracle/adrastia-core", - "version": "4.8.0", + "version": "4.9.0", "main": "index.js", "author": "TRILEZ SOFTWARE INC.", "license": "BUSL-1.1", From c74c9821a91f775ee3e958b660fa16033a9d3ee7 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Fri, 2 Aug 2024 15:32:45 -0700 Subject: [PATCH 08/10] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22454a5..b6055db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v4.9.0 +### Accumulators +- Add SAVPriceAccumulator: An accumulator that tracks ERC4626 vault share prices. + ## v4.8.0 ### Accumulators - Add AdrastiaUtilizationAndErrorAccumulator: An accumulator that tracks and accumulates utilization and error based on an Adrastia supply & borrow oracle. From 6a223b976b516fe8611a917ab37de3b7113a7b50 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Fri, 2 Aug 2024 15:39:36 -0700 Subject: [PATCH 09/10] Update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6055db..5746d03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## v4.9.0 +### Dependencies +- Upgrade OpenZeppelin Contracts to v4.9.6 + ### Accumulators - Add SAVPriceAccumulator: An accumulator that tracks ERC4626 vault share prices. From 90f5b47df86edaf09cfbf3b2872e3a28ac2907a6 Mon Sep 17 00:00:00 2001 From: Tyler Loewen Date: Fri, 2 Aug 2024 20:47:11 -0700 Subject: [PATCH 10/10] Change default hardfork to Cancun --- hardhat.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardhat.config.js b/hardhat.config.js index 9e44cdc..f576da5 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -50,7 +50,7 @@ module.exports = { networks: { hardhat: { gas: 10000000, - hardfork: process.env.HARDHAT_HARDFORK || "shanghai", + hardfork: process.env.HARDHAT_HARDFORK || "cancun", forking: { url: process.env.ETHEREUM_URL || "", blockNumber: 20370000,