diff --git a/audits/ABDK@v1.0.1.pdf b/audits/ABDK@v1.0.1.pdf new file mode 100644 index 0000000..02669cd Binary files /dev/null and b/audits/ABDK@v1.0.1.pdf differ diff --git a/audits/ChainSecurity@v0.1.1.pdf b/audits/ChainSecurity@v0.1.1.pdf new file mode 100644 index 0000000..0f765ed Binary files /dev/null and b/audits/ChainSecurity@v0.1.1.pdf differ diff --git a/script/Aggor.s.sol b/script/Aggor.s.sol index 3a4582c..88405d7 100644 --- a/script/Aggor.s.sol +++ b/script/Aggor.s.sol @@ -25,6 +25,7 @@ contract AggorScript is Script { address uniswapBaseToken, address uniswapQuoteToken, uint8 uniswapBaseTokenDecimals, + uint8 uniswapQuoteTokenDecimals, uint32 uniswapLookback, uint128 agreementDistance, uint32 ageThreshold @@ -40,6 +41,7 @@ contract AggorScript is Script { uniswapBaseToken, uniswapQuoteToken, uniswapBaseTokenDecimals, + uniswapQuoteTokenDecimals, uniswapLookback, agreementDistance, ageThreshold diff --git a/src/Aggor.sol b/src/Aggor.sol index 57293eb..ed5b528 100644 --- a/src/Aggor.sol +++ b/src/Aggor.sol @@ -19,7 +19,7 @@ import {LibMedian} from "./libs/LibMedian.sol"; /** * @title Aggor - * @custom:version v1.0.0 + * @custom:version v1.0.1 * * @notice Oracle aggregator distributing trust among different oracle providers * @@ -74,6 +74,8 @@ contract Aggor is IAggor, IToll, Auth { /// @inheritdoc IAggor uint8 public immutable uniswapBaseTokenDecimals; /// @inheritdoc IAggor + uint8 public immutable uniswapQuoteTokenDecimals; + /// @inheritdoc IAggor uint32 public immutable uniswapLookback; // -- Mutable Configurations -- @@ -103,6 +105,7 @@ contract Aggor is IAggor, IToll, Auth { address uniswapBaseToken_, address uniswapQuoteToken_, uint8 uniswapBaseTokenDecimals_, + uint8 uniswapQuoteTokenDecimals_, uint32 uniswapLookback_, uint128 agreementDistance_, uint32 ageThreshold_ @@ -113,6 +116,7 @@ contract Aggor is IAggor, IToll, Auth { uniswapBaseToken_, uniswapQuoteToken_, uniswapBaseTokenDecimals_, + uniswapQuoteTokenDecimals_, uniswapLookback_ ); @@ -124,6 +128,7 @@ contract Aggor is IAggor, IToll, Auth { uniswapBaseToken = uniswapBaseToken_; uniswapQuoteToken = uniswapQuoteToken_; uniswapBaseTokenDecimals = uniswapBaseTokenDecimals_; + uniswapQuoteTokenDecimals = uniswapQuoteTokenDecimals_; uniswapLookback = uniswapLookback_; // Emit events indicating address(0) and _bud are tolled. @@ -142,6 +147,7 @@ contract Aggor is IAggor, IToll, Auth { address uniswapBaseToken_, address uniswapQuoteToken_, uint8 uniswapBaseTokenDecimals_, + uint8 uniswapQuoteTokenDecimals_, uint32 uniswapLookback_ ) internal view { require(uniswapPool_ != address(0), "Uniswap pool must not be zero"); @@ -163,7 +169,7 @@ contract Aggor is IAggor, IToll, Auth { "Uniswap quote token mismatch" ); - // Verify base token's decimals. + // Verify token decimals. require( uniswapBaseTokenDecimals_ == IERC20(uniswapBaseToken_).decimals(), "Uniswap base token decimals mismatch" @@ -172,6 +178,10 @@ contract Aggor is IAggor, IToll, Auth { uniswapBaseTokenDecimals_ <= _MAX_UNISWAP_BASE_DECIMALS, "Uniswap base token decimals too high" ); + require( + uniswapQuoteTokenDecimals_ == IERC20(uniswapQuoteToken_).decimals(), + "Uniswap quote token decimals mismatch" + ); // Verify TWAP is initialized. // Specifically, verify that the TWAP's oldest observation is older @@ -338,6 +348,14 @@ contract Aggor is IAggor, IToll, Auth { uniswapLookback ); + if (uniswapQuoteTokenDecimals <= decimals) { + // Scale up + twap *= 10 ** (decimals - uniswapQuoteTokenDecimals); + } else { + // Scale down + twap /= 10 ** (uniswapQuoteTokenDecimals - decimals); + } + if (twap <= type(uint128).max) { return (true, uint128(twap)); } else { @@ -488,6 +506,7 @@ contract Aggor_BASE_QUOTE_COUNTER is Aggor { address uniswapBaseToken_, address uniswapQuoteToken_, uint8 uniswapBaseDec_, + uint8 uniswapQuoteDec_, uint32 uniswapLookback_, uint128 agreementDistance_, uint32 ageThreshold_ @@ -501,6 +520,7 @@ contract Aggor_BASE_QUOTE_COUNTER is Aggor { uniswapBaseToken_, uniswapQuoteToken_, uniswapBaseDec_, + uniswapQuoteDec_, uniswapLookback_, agreementDistance_, ageThreshold_ diff --git a/src/IAggor.sol b/src/IAggor.sol index 38efc03..818a1a4 100644 --- a/src/IAggor.sol +++ b/src/IAggor.sol @@ -103,6 +103,13 @@ interface IAggor { view returns (uint8 baseTokenDecimals); + /// @notice Returns the Uniswap pool's quote token's decimals. + /// @return quoteTokenDecimals The Uniswap pool's quote token's decimals. + function uniswapQuoteTokenDecimals() + external + view + returns (uint8 quoteTokenDecimals); + /// @notice Returns the time in seconds to use as lookback for Uniswap Twap /// oracle. /// @return lookback The time in seconds to use as lookback. diff --git a/test/Aggor.t.sol b/test/Aggor.t.sol index be2ecae..ebd6175 100644 --- a/test/Aggor.t.sol +++ b/test/Aggor.t.sol @@ -34,11 +34,11 @@ contract AggorTest is Test { // Twap Provider: address uniswapPool = address(new UniswapPoolMock()); address uniswapBaseToken = address(new ERC20Mock("base", "base", 18)); - address uniswapQuoteToken = address(new ERC20Mock("quote", "quote", 18)); + address uniswapQuoteToken = address(new ERC20Mock("quote", "quote", 6)); uint32 uniswapLookback = 1 days; // For more info, see mocks/UniswapPoolMock::observe(). - uint valTwap = 999_902; + uint valTwap = 99_990_200; // Scaled for 10^Aggor.decimals() // Configurations: uint128 agreementDistance = 9e17; // = 0.9e18 = 10% @@ -58,6 +58,7 @@ contract AggorTest is Test { uniswapBaseToken, uniswapQuoteToken, IERC20(uniswapBaseToken).decimals(), + IERC20(uniswapQuoteToken).decimals(), uniswapLookback, agreementDistance, ageThreshold @@ -74,6 +75,7 @@ contract AggorTest is Test { uniswapBaseToken, uniswapQuoteToken, IERC20(uniswapBaseToken).decimals(), + IERC20(uniswapQuoteToken).decimals(), uniswapLookback, agreementDistance, ageThreshold @@ -85,6 +87,7 @@ contract AggorTest is Test { uniswapBaseToken, uniswapQuoteToken, IERC20(uniswapBaseToken).decimals(), + IERC20(uniswapQuoteToken).decimals(), uniswapLookback ); } @@ -93,6 +96,7 @@ contract AggorTest is Test { function test_Deployment_FailsIf_UniswapPoolZeroAddress() public { uint8 decimals = IERC20(uniswapBaseToken).decimals(); + uint8 quoteDecimals = IERC20(uniswapQuoteToken).decimals(); vm.expectRevert("Uniswap pool must not be zero"); new Aggor( @@ -104,6 +108,7 @@ contract AggorTest is Test { uniswapBaseToken, uniswapQuoteToken, decimals, + quoteDecimals, uniswapLookback, agreementDistance, ageThreshold @@ -115,12 +120,14 @@ contract AggorTest is Test { uniswapBaseToken, uniswapQuoteToken, decimals, + quoteDecimals, uniswapLookback ); } function test_Deployment_FailsIf_BaseTokenEqualsQuoteToken() public { uint8 decimals = IERC20(uniswapBaseToken).decimals(); + uint8 quoteDecimals = IERC20(uniswapQuoteToken).decimals(); vm.expectRevert("Uniswap tokens must not be equal"); new Aggor( @@ -132,6 +139,7 @@ contract AggorTest is Test { uniswapBaseToken, uniswapBaseToken, // <- ! decimals, + quoteDecimals, uniswapLookback, agreementDistance, ageThreshold @@ -143,6 +151,7 @@ contract AggorTest is Test { uniswapBaseToken, uniswapBaseToken, // <- ! decimals, + quoteDecimals, uniswapLookback ); } @@ -150,6 +159,7 @@ contract AggorTest is Test { function test_Deployment_FailsIf_BaseTokenNotPoolToken() public { address notPoolToken = address(new ERC20Mock("", "", 18)); uint8 decimals = IERC20(notPoolToken).decimals(); + uint8 quoteDecimals = IERC20(uniswapQuoteToken).decimals(); vm.expectRevert("Uniswap base token mismatch"); new Aggor( @@ -161,6 +171,7 @@ contract AggorTest is Test { notPoolToken, // <- ! uniswapQuoteToken, decimals, + quoteDecimals, uniswapLookback, agreementDistance, ageThreshold @@ -172,6 +183,7 @@ contract AggorTest is Test { notPoolToken, // <- ! uniswapQuoteToken, decimals, + quoteDecimals, uniswapLookback ); } @@ -179,6 +191,7 @@ contract AggorTest is Test { function test_Deployment_FailsIf_QuoteTokenNotPoolToken() public { address notPoolToken = address(new ERC20Mock("", "", 18)); uint8 decimals = IERC20(uniswapBaseToken).decimals(); + uint8 quoteDecimals = IERC20(uniswapQuoteToken).decimals(); vm.expectRevert("Uniswap quote token mismatch"); new Aggor( @@ -190,6 +203,7 @@ contract AggorTest is Test { uniswapBaseToken, notPoolToken, // <- ! decimals, + quoteDecimals, uniswapLookback, agreementDistance, ageThreshold @@ -201,12 +215,14 @@ contract AggorTest is Test { uniswapBaseToken, notPoolToken, // <- ! decimals, + quoteDecimals, uniswapLookback ); } function test_Deployment_FailsIf_BaseTokenDecimalsWrong() public { uint8 decimals = IERC20(uniswapBaseToken).decimals(); + uint8 quoteDecimals = IERC20(uniswapQuoteToken).decimals(); vm.expectRevert("Uniswap base token decimals mismatch"); new Aggor( @@ -218,6 +234,7 @@ contract AggorTest is Test { uniswapBaseToken, uniswapQuoteToken, decimals + 1, // <- ! + quoteDecimals, uniswapLookback, agreementDistance, ageThreshold @@ -229,6 +246,7 @@ contract AggorTest is Test { uniswapBaseToken, uniswapQuoteToken, decimals + 1, // <- ! + quoteDecimals, uniswapLookback ); } @@ -237,6 +255,7 @@ contract AggorTest is Test { ) public { uniswapBaseToken = address(new ERC20Mock("base", "base", 100)); // <- ! uint8 decimals = IERC20(uniswapBaseToken).decimals(); + uint8 quoteDecimals = IERC20(uniswapQuoteToken).decimals(); UniswapPoolMock(uniswapPool).setToken0(uniswapBaseToken); @@ -250,6 +269,7 @@ contract AggorTest is Test { uniswapBaseToken, uniswapQuoteToken, decimals, + quoteDecimals, uniswapLookback, agreementDistance, ageThreshold @@ -261,6 +281,7 @@ contract AggorTest is Test { uniswapBaseToken, uniswapQuoteToken, decimals, + quoteDecimals, uniswapLookback ); } @@ -268,6 +289,7 @@ contract AggorTest is Test { function test_Deployment_FailsIf_UniswapLookbackBiggerThanOldestObservation( ) public { uint8 decimals = IERC20(uniswapBaseToken).decimals(); + uint8 quoteDecimals = IERC20(uniswapQuoteToken).decimals(); vm.expectRevert("Uniswap lookback too high"); new Aggor( @@ -279,6 +301,7 @@ contract AggorTest is Test { uniswapBaseToken, uniswapQuoteToken, decimals, + quoteDecimals, type(uint32).max, // <- ! agreementDistance, ageThreshold @@ -290,6 +313,7 @@ contract AggorTest is Test { uniswapBaseToken, uniswapQuoteToken, decimals, + quoteDecimals, type(uint32).max ); } @@ -1048,6 +1072,7 @@ contract Aggor_VerifyTwapConfig is Aggor { address uniswapBaseToken_, address uniswapQuoteToken_, uint8 uniswapBaseTokenDecimals_, + uint8 uniswapQuoteTokenDecimals_, uint32 uniswapLookback_, uint128 agreementDistance_, uint32 ageThreshold_ @@ -1061,6 +1086,7 @@ contract Aggor_VerifyTwapConfig is Aggor { uniswapBaseToken_, uniswapQuoteToken_, uniswapBaseTokenDecimals_, + uniswapQuoteTokenDecimals_, uniswapLookback_, agreementDistance_, ageThreshold_ @@ -1072,6 +1098,7 @@ contract Aggor_VerifyTwapConfig is Aggor { address uniswapBaseToken_, address uniswapQuoteToken_, uint8 uniswapBaseTokenDecimals_, + uint8 uniswapQuoteTokenDecimals_, uint32 uniswapLookback_ ) public view { _verifyTwapConfig( @@ -1079,6 +1106,7 @@ contract Aggor_VerifyTwapConfig is Aggor { uniswapBaseToken_, uniswapQuoteToken_, uniswapBaseTokenDecimals_, + uniswapQuoteTokenDecimals_, uniswapLookback_ ); } diff --git a/test/integration/AggorIntegration_eth_ETH_USD.t.sol b/test/integration/AggorIntegration_eth_ETH_USD.t.sol index 76ab9c7..0042401 100644 --- a/test/integration/AggorIntegration_eth_ETH_USD.t.sol +++ b/test/integration/AggorIntegration_eth_ETH_USD.t.sol @@ -4,6 +4,10 @@ pragma solidity ^0.8.16; import {Test} from "forge-std/Test.sol"; import {console2 as console} from "forge-std/console2.sol"; +import {LibUniswapOracles} from "src/libs/LibUniswapOracles.sol"; +import {LibUniswapOraclesWrapper} from + "test/integration/LibUniswapOraclesIntegration_eth_USDC_DAI.t.sol"; + import {IAuth} from "chronicle-std/auth/IAuth.sol"; import {IToll} from "chronicle-std/toll/IToll.sol"; import {IChronicle} from "chronicle-std/IChronicle.sol"; @@ -19,6 +23,7 @@ import {IAggor} from "src/IAggor.sol"; * - tie breaker : Uniswap Twap USDC/WETH */ contract AggorIntegrationTest_eth_ETH_USD is Test { + LibUniswapOraclesWrapper wrapper; Aggor aggor; // Oracle Providers: @@ -30,14 +35,16 @@ contract AggorIntegrationTest_eth_ETH_USD is Test { // Twap Provider: Uniswap USDC/WETH pool address uniswapPool = address(0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640); - // Base token: USDC + // Base token: WETH address uniswapBaseToken = - address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); - // Quote token: WETH - address uniswapQuoteToken = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - // Base token decimals: USDC.decimals() - uint8 uniswapBaseDec = 6; + // Quote token: USDC + address uniswapQuoteToken = + address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + // Base token decimals: WETH.decimals() + uint8 uniswapBaseDec = 18; + // Quote token decimals: WETH.decimals() + uint8 uniswapQuoteDec = 6; // Twap lookback in seconds: 1 hour uint32 uniswapLookback = 1 hours; @@ -59,6 +66,7 @@ contract AggorIntegrationTest_eth_ETH_USD is Test { uniswapBaseToken, uniswapQuoteToken, uniswapBaseDec, + uniswapQuoteDec, uniswapLookback, agreementDistance, ageThreshold @@ -67,6 +75,8 @@ contract AggorIntegrationTest_eth_ETH_USD is Test { // Kiss aggor on chronicle oracle. vm.prank(IAuth(chronicle).authed()[0]); IToll(chronicle).kiss(address(aggor)); + + wrapper = new LibUniswapOraclesWrapper(); } function _setChronicle(uint128 val, uint32 age) internal { @@ -371,6 +381,89 @@ contract AggorIntegrationTest_eth_ETH_USD is Test { } } } + + function test_TwapScaleUp() public { + uint price = wrapper.readOracle( + uniswapPool, + uniswapBaseToken, + uniswapQuoteToken, + uniswapBaseDec, + uniswapLookback + ); + + // Scale price up from USDC.decimals() + uint128 wantVal = uint128(price * 10 ** (8 - 6)); + assertTrue(wantVal > price); + + // Make TWAP value the median. + uint128 chrVal = uint128(wantVal) - 1; + uint128 chlVal = uint128(wantVal) + 1; + + // Let oracles be stale + uint32 chlAge = 0; + uint32 chrAge = 0; + + // Set oracles. + _setChronicle(chrVal, chrAge); + _setChainlink(chlVal, chlAge); + + // Read aggor. + uint gotVal; + (gotVal,,) = aggor.readWithStatus(); + assertEq(gotVal, wantVal); + } + + // Note we reverse the default base/quote for this test + function test_TwapScaleDown() public { + uint price = wrapper.readOracle( + uniswapPool, + uniswapQuoteToken, + uniswapBaseToken, + uniswapQuoteDec, + uniswapLookback + ); + + // Scale price down from WETH.decimals() + uint128 wantVal = uint128(price / 10 ** (18 - 8)); + assertTrue(wantVal < price); + + // Redeploy aggor with Uniswap params reversed. + aggor = new Aggor( + address(this), + address(this), + chronicle, + chainlink, + uniswapPool, + uniswapQuoteToken, + uniswapBaseToken, + uniswapQuoteDec, + uniswapBaseDec, + uniswapLookback, + agreementDistance, + ageThreshold + ); + + // Kiss aggor on chronicle oracle. + vm.prank(IAuth(chronicle).authed()[0]); + IToll(chronicle).kiss(address(aggor)); + + // Make TWAP value the median. + uint128 chrVal = wantVal - 1; + uint128 chlVal = wantVal + 1; + + // Let oracles be stale + uint32 chlAge = 0; + uint32 chrAge = 0; + + // Set oracles. + _setChronicle(chrVal, chrAge); + _setChainlink(chlVal, chlAge); + + // Read aggor. + uint gotVal; + (gotVal,,) = aggor.readWithStatus(); + assertEq(gotVal, wantVal); + } } interface IChainlinkAggregatorV3_Aggregator {