diff --git a/.gas-snapshot b/.gas-snapshot index 83eadf6..7b35183 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,24 +1,22 @@ -AggorTest:test_Deployment() (gas: 22277) -AggorTest:test_latestAnswer_IsTollProtected() (gas: 11829) -AggorTest:test_latestRoundData_IsTollProtected() (gas: 12872) -AggorTest:test_poke_ChainlinkDecimalConversion() (gas: 181275) -AggorTest:test_readWithAge_FailsIfValIsZero() (gas: 13444) -AggorTest:test_readWithAge_IsTollProtected() (gas: 11621) -AggorTest:test_read_FailsIfValIsZero() (gas: 12873) -AggorTest:test_read_IsTollProtected() (gas: 12377) -AggorTest:test_setSpread_IsAuthProtected() (gas: 12118) -AggorTest:test_setStalenessThreshold_FailsIf_IsZero() (gas: 11348) -AggorTest:test_setStalenessThreshold_IsAuthProtected() (gas: 11901) -AggorTest:test_setUniSecondsAgo_IsAuthProtected() (gas: 12253) -AggorTest:test_setUniswap_FromZeroAddress() (gas: 91741) -AggorTest:test_setUniswap_IsAuthProtected() (gas: 12222) -AggorTest:test_setUniswap_ToZeroAddress() (gas: 69117) -AggorTest:test_tryReadWithAge_IsTollProtected() (gas: 12790) -AggorTest:test_tryReadWithAge_ReturnsFalseIfValIsZero() (gas: 11181) -AggorTest:test_tryRead_IsTollProtected() (gas: 11984) -AggorTest:test_tryRead_ReturnsFalseIfValIsZero() (gas: 10068) +AggorTest:test_Deployment() (gas: 20366) +AggorTest:test_latestAnswer_IsTollProtected() (gas: 11841) +AggorTest:test_latestRoundData_IsTollProtected() (gas: 12839) +AggorTest:test_poke_ChainlinkDecimalConversion() (gas: 156430) +AggorTest:test_readWithAge_FailsIfValIsZero() (gas: 13424) +AggorTest:test_readWithAge_IsTollProtected() (gas: 11614) +AggorTest:test_read_FailsIfValIsZero() (gas: 12893) +AggorTest:test_read_IsTollProtected() (gas: 12388) +AggorTest:test_setSpread_IsAuthProtected() (gas: 12109) +AggorTest:test_setStalenessThreshold_FailsIf_IsZero() (gas: 11320) +AggorTest:test_setStalenessThreshold_IsAuthProtected() (gas: 11889) +AggorTest:test_setUniSecondsAgo_IsAuthProtected() (gas: 12244) +AggorTest:test_tryReadWithAge_IsTollProtected() (gas: 12777) +AggorTest:test_tryReadWithAge_ReturnsFalseIfValIsZero() (gas: 11157) +AggorTest:test_tryRead_IsTollProtected() (gas: 11952) +AggorTest:test_tryRead_ReturnsFalseIfValIsZero() (gas: 10070) +AggorTest:test_useUniswap_IsAuthProtected() (gas: 12326) +AggorTest:test_useUniswap_NotConfigured() (gas: 2345964) LibCalcTest:test_distance() (gas: 305) LibCalcTest:test_pctDiff() (gas: 439) LibCalcTest:test_scale_basic() (gas: 240) -LibCalcTest:test_scale_revert() (gas: 3398) -MainnetIntegrationTest:testIntegration_Mainnet() (gas: 214755) \ No newline at end of file +LibCalcTest:test_scale_revert() (gas: 3398) \ No newline at end of file diff --git a/.github/workflows/gas.yml b/.github/workflows/gas.yml index 486272d..e08c1a8 100644 --- a/.github/workflows/gas.yml +++ b/.github/workflows/gas.yml @@ -22,4 +22,4 @@ jobs: - name: Check gas snapshots run: | - forge snapshot --nmt "Fuzz|Mainnet" --check + forge snapshot --nmt "Fuzz|Integration" --check diff --git a/Makefile b/Makefile index 8fa0c56..d60e38a 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ gas_report: ## Print gas report .PHONY: snapshot snapshot: ## Update forge's snapshot file - forge snapshot --nmt "Fuzz" + forge snapshot --nmt "Fuzz|Integration" .PHONY: help help: ## Help command diff --git a/docs/Aggor.md b/docs/Aggor.md index 5f68809..d4e5c70 100644 --- a/docs/Aggor.md +++ b/docs/Aggor.md @@ -35,36 +35,50 @@ The following `IChainlinkAggregatorV3` functions are provided: - `latestAnswer()` - `decimals()` +## Low liquidity Uniswap pools + +If a low liquidity pool is used in Aggor, the cost of price manipulation of the internal uniswap oracle is drastically reduced. However, because of the technicalities required, an attacker will at least need to control two consecutive blocks (potentially more) to make their manipulation succesful, which mitigates the risk. High liquidity pools will always be sought first as well, and deployments will only be done after a thorough risk analysis. + +## Price jump + +The spread limit guards against single outliers in one of the price feeds, making the aggregate price generally less volatile than the sources. However, if the values of the two feeds diverge slowly, the price will experience a sudden jump of approximately `spread/2` when the difference is greater than `spread`. + +## Price manipulation + +There is an inherent trust assumption in Aggor that the oracle *sources* are difficult to manipulate, that is, designed in and of themselves to be manipulation resistant, especially for a significant amount of time. + +Aggor’s `spread` minimizes this risk further by preventing large deviations in a single update. Effectively, customers do not need to trust a given oracle system at any point in time. However, over some period of time trust in both oracles is required. + +For example, if one oracle ceases to update, but the other does not, Aggor price will (in time) "go stale" and give the price that was last agreed upon when both oracle systems were reporting. This timeframe should allow enough time for an operational reaction. Good monitoring and contingencies will be necessary, and in place to further guard against price manipulation. + ## Benchmarks A gas report for the `poke()` function can be created via `make gas_report`. -```bash -$ make gas_report - -Gas report taken Wed Jun 7 10:45:35 UTC 2023 +``` +Gas report taken Thu Jul 20 18:23:19 UTC 2023 forge t --gas-report --match-test poke_basic [⠒] Compiling... No files changed, compilation skipped Running 1 test for test/Runner.t.sol:AggorTest -[PASS] testFuzz_poke_basic(uint128,uint128,uint256,uint256,uint256) (runs: 256, μ: 167561, ~: 167689) -Test result: ok. 1 passed; 0 failed; finished in 49.07ms +[PASS] testFuzz_poke_basic(uint128,uint128,uint256,uint256,uint256) (runs: 256, μ: 165830, ~: 165956) +Test result: ok. 1 passed; 0 failed; finished in 46.77ms | src/Aggor.sol:Aggor contract | | | | | | |------------------------------|-----------------|-------|--------|-------|---------| | Deployment Cost | Deployment Size | | | | | -| 2334115 | 12122 | | | | | +| 2200182 | 12072 | | | | | | Function Name | min | avg | median | max | # calls | -| chainlink | 678 | 678 | 678 | 678 | 1 | -| chronicle | 722 | 722 | 722 | 722 | 1 | -| kiss | 69382 | 69382 | 69382 | 69382 | 1 | -| latestAnswer | 769 | 769 | 769 | 769 | 1 | -| latestRoundData | 1283 | 1283 | 1283 | 1283 | 1 | -| poke | 33036 | 33036 | 33036 | 33036 | 1 | -| read | 810 | 810 | 810 | 810 | 1 | -| readWithAge | 707 | 707 | 707 | 707 | 1 | -| spread | 630 | 630 | 630 | 630 | 1 | -| stalenessThreshold | 856 | 1856 | 1856 | 2856 | 2 | -| tryRead | 525 | 1525 | 1525 | 2525 | 2 | -| tryReadWithAge | 1181 | 1181 | 1181 | 1181 | 1 | +| chainlink | 700 | 700 | 700 | 700 | 1 | +| chronicle | 744 | 744 | 744 | 744 | 1 | +| kiss | 69377 | 69377 | 69377 | 69377 | 1 | +| latestAnswer | 819 | 819 | 819 | 819 | 1 | +| latestRoundData | 1311 | 1311 | 1311 | 1311 | 1 | +| poke | 31005 | 31005 | 31005 | 31005 | 1 | +| read | 860 | 860 | 860 | 860 | 1 | +| readWithAge | 735 | 735 | 735 | 735 | 1 | +| spread | 674 | 674 | 674 | 674 | 1 | +| stalenessThreshold | 878 | 1878 | 1878 | 2878 | 2 | +| tryRead | 553 | 1553 | 1553 | 2553 | 2 | +| tryReadWithAge | 1209 | 1209 | 1209 | 1209 | 1 | ``` diff --git a/script/Aggor.s.sol b/script/Aggor.s.sol index eab6548..c6556a2 100644 --- a/script/Aggor.s.sol +++ b/script/Aggor.s.sol @@ -25,16 +25,24 @@ import {Aggor} from "src/Aggor.sol"; */ contract AggorScript is Script { /// @dev You'll want to adjust this addresses before deployment. - /// Note that deployment fails if addresses are zero. + /// Note that deployment fails if addresses are zero (with the + // exception of Uniswap, which is optional). address internal constant ORACLE_CHRONICLE = address(0); address internal constant ORACLE_CHAINLINK = address(0); + address internal constant ORACLE_UNISWAP = address(0); + bool internal constant UNI_USE_TOKEN0_AS_BASE = false; /// @dev You'll want to adjust this address if Aggor is already deployed. IAggor internal aggor = IAggor(address(0)); function deploy() public returns (IAggor) { vm.startBroadcast(); - aggor = new Aggor(ORACLE_CHRONICLE, ORACLE_CHAINLINK); + aggor = new Aggor( + ORACLE_CHRONICLE, + ORACLE_CHAINLINK, + ORACLE_UNISWAP, + UNI_USE_TOKEN0_AS_BASE + ); vm.stopBroadcast(); return aggor; diff --git a/src/Aggor.sol b/src/Aggor.sol index 82224d7..4529b76 100644 --- a/src/Aggor.sol +++ b/src/Aggor.sol @@ -45,19 +45,19 @@ contract Aggor is IAggor, Auth, Toll { address public immutable chainlink; /// @inheritdoc IAggor - address public uniPool; + address public immutable uniPool; /// @inheritdoc IAggor - address public uniBasePair; + address public immutable uniBasePair; /// @inheritdoc IAggor - address public uniQuotePair; + address public immutable uniQuotePair; /// @inheritdoc IAggor - uint8 public uniBaseDec; + uint8 public immutable uniBaseDec; /// @inheritdoc IAggor - uint8 public uniQuoteDec; + uint8 public immutable uniQuoteDec; /// @inheritdoc IAggor uint32 public uniSecondsAgo; @@ -68,11 +68,26 @@ contract Aggor is IAggor, Auth, Toll { /// @inheritdoc IAggor uint16 public spread; + /// @inheritdoc IAggor + bool public uniswapSelected; + // This is the last agreed upon mean price. uint128 private _val; uint32 private _age; - constructor(address chronicle_, address chainlink_) { + /// @notice You only get once chance per deploy to setup Uniswap. If it + /// will not be used, just pass in address(0) for uniPool_. + /// @param chronicle_ Address of Chronicle oracle + /// @param chainlink_ Address of Chainlink oracle + /// @param uniPool_ Address of Uniswap oracle (optional) + /// @param uniUseToken0AsBase If true, selects Pool.token0 as base pair, if not, + // it uses Pool.token1 as the base pair. + constructor( + address chronicle_, + address chainlink_, + address uniPool_, + bool uniUseToken0AsBase + ) { require(chronicle_ != address(0)); require(chainlink_ != address(0)); @@ -82,9 +97,45 @@ contract Aggor is IAggor, Auth, Toll { // Note that IChronicle::wat() is constant and save to cache. wat = IChronicle(chronicle_).wat(); + // Optionally initialize Uniswap. + address uniPoolInitializer; + address uniBasePairInitializer; + address uniQuotePairInitializer; + uint8 uniBaseDecInitializer; + uint8 uniQuoteDecInitializer; + + if (uniPool_ != address(0)) { + uniPoolInitializer = uniPool_; + + if (uniUseToken0AsBase) { + uniBasePairInitializer = + IUniswapV3PoolImmutables(uniPoolInitializer).token0(); + uniQuotePairInitializer = + IUniswapV3PoolImmutables(uniPoolInitializer).token1(); + } else { + uniBasePairInitializer = + IUniswapV3PoolImmutables(uniPoolInitializer).token1(); + uniQuotePairInitializer = + IUniswapV3PoolImmutables(uniPoolInitializer).token0(); + } + + uniBaseDecInitializer = IERC20(uniBasePairInitializer).decimals(); + uniQuoteDecInitializer = IERC20(uniQuotePairInitializer).decimals(); + } + + uniPool = uniPoolInitializer; + uniBasePair = uniBasePairInitializer; + uniQuotePair = uniQuotePairInitializer; + uniBaseDec = uniBaseDecInitializer; + uniQuoteDec = uniQuoteDecInitializer; + + // Default config values setStalenessThreshold(1 days); setSpread(500); // 5% - setUniSecondsAgo(5 minutes); + + if (uniPool != address(0)) { + setUniSecondsAgo(5 minutes); + } } /// @inheritdoc IAggor @@ -111,15 +162,17 @@ contract Aggor is IAggor, Auth, Toll { // assert(valChronicle != 0); // assert(valChronicle <= type(uint128).max); - // Read second oracle, i.e. either Chainlink or Uniswap TWAP. + // Read second oracle, either Chainlink or Uniswap TWAP. uint valOther; - if (uniPool == address(0)) { + if (!uniswapSelected) { // Read Chainlink. (ok, valOther) = _tryReadChainlink(); if (!ok) { revert OracleReadFailed(chainlink); } } else { + // assert(uniPool != address(0)); + // Read Uniswap. (ok, valOther) = _tryReadUniswap(); if (!ok) { @@ -133,7 +186,7 @@ contract Aggor is IAggor, Auth, Toll { uint diff = LibCalc.pctDiff(uint128(valChronicle), uint128(valOther), _pscale); - if (diff != 0 && diff > spread) { + if (diff > spread) { // If difference is bigger than acceptable spread, let _val be the // oracle's value with less difference to the current _val. // forgefmt: disable-next-item @@ -233,30 +286,26 @@ contract Aggor is IAggor, Auth, Toll { } /// @inheritdoc IAggor - function setUniswap(address uniPool_) public auth { - if (uniPool == uniPool_) return; + function useUniswap(bool selected) external auth { + // Uniswap pool must be configured + require(uniPool != address(0)); - // Update Uniswap pool variable. - emit UniswapUpdated(msg.sender, uniPool, uniPool_); - uniPool = uniPool_; + // Revert unless there is something to change + require(uniswapSelected != selected); - if (uniPool_ != address(0)) { - // Set other Uniswap variables. - uniBasePair = IUniswapV3PoolImmutables(uniPool).token0(); - uniQuotePair = IUniswapV3PoolImmutables(uniPool).token1(); - uniBaseDec = IERC20(uniBasePair).decimals(); - uniQuoteDec = IERC20(uniQuotePair).decimals(); - } else { - // Delete other Uniswap variables. - delete uniBasePair; - delete uniQuotePair; - delete uniBaseDec; - delete uniQuoteDec; - } + emit UniswapSelectedUpdated({ + caller: msg.sender, + oldValue: uniswapSelected, + newValue: selected + }); + + uniswapSelected = selected; } /// @inheritdoc IAggor function setUniSecondsAgo(uint32 uniSecondsAgo_) public auth { + // Uniswap is optional, make sure it's configured + require(uniPool != address(0)); require(uniSecondsAgo_ >= minUniSecondsAgo); if (uniSecondsAgo != uniSecondsAgo_) { @@ -265,11 +314,15 @@ contract Aggor is IAggor, Auth, Toll { ); uniSecondsAgo = uniSecondsAgo_; } + + // Ensure that the pool works within the desired "lookback" period. + (bool ok,) = _tryReadUniswap(); + require(ok); } // -- Private Helpers -- - function _tryReadUniswap() internal returns (bool, uint) { + function _tryReadUniswap() internal view returns (bool, uint) { // assert(uniPool != address(0)); uint val = LibUniswapOracles.readOracle( @@ -283,14 +336,18 @@ contract Aggor is IAggor, Auth, Toll { // Fail if value is zero. if (val == 0) { - emit UniswapValueZero(); + return (false, 0); + } + + // Also fail if could cause overflow. + if (val > type(uint128).max) { return (false, 0); } return (true, val); } - function _tryReadChronicle() internal returns (bool, uint) { + function _tryReadChronicle() internal view returns (bool, uint) { bool ok; uint val; uint age; @@ -300,14 +357,13 @@ contract Aggor is IAggor, Auth, Toll { // Fail if value stale. uint diff = block.timestamp - age; if (diff > stalenessThreshold) { - emit ChronicleValueStale(age, block.timestamp); return (false, 0); } return (ok, val); } - function _tryReadChainlink() internal returns (bool, uint) { + function _tryReadChainlink() internal view returns (bool, uint) { int answer; uint updatedAt; (, answer,, updatedAt,) = @@ -316,13 +372,11 @@ contract Aggor is IAggor, Auth, Toll { // Fail if value stale. uint diff = block.timestamp - updatedAt; if (diff > stalenessThreshold) { - emit ChainlinkValueStale(updatedAt, block.timestamp); return (false, 0); } // Fail if value negative. if (answer < 0) { - emit ChainlinkValueNegative(answer); return (false, 0); } @@ -335,7 +389,6 @@ contract Aggor is IAggor, Auth, Toll { // Fail if value is zero. if (val == 0) { - emit ChainlinkValueZero(); return (false, 0); } diff --git a/src/IAggor.sol b/src/IAggor.sol index bff24ef..52719c8 100644 --- a/src/IAggor.sol +++ b/src/IAggor.sol @@ -8,14 +8,6 @@ interface IAggor is IChronicle { /// @param oracle The oracle address which read's failed. error OracleReadFailed(address oracle); - /// @notice Emitted when Uniswap TWAP pool updated. - /// @param caller The caller's address. - /// @param oldUniswapPool The old Uniswap pool address. - /// @param newUniswapPool The new Uniswap pool address. - event UniswapUpdated( - address indexed caller, address oldUniswapPool, address newUniswapPool - ); - /// @notice Emitted when staleness threshold updated. /// @param caller The caller's address. /// @param oldStalenessThreshold The old staleness threshold. @@ -26,6 +18,14 @@ interface IAggor is IChronicle { uint32 newStalenessThreshold ); + /// @notice Emitted when Uniswap is selected or deselected + /// @param caller The caller's address. + /// @param oldValue The previous value. + /// @param newValue The updated value. + event UniswapSelectedUpdated( + address indexed caller, bool oldValue, bool newValue + ); + /// @notice Emitted when spread is updated. /// @param caller The caller's address. /// @param oldSpread The old spread value. @@ -44,26 +44,9 @@ interface IAggor is IChronicle { uint32 newUniswapSecondsAgo ); - /// @notice Emitted when Chronicle's oracle delivered a stale value. - /// @param age The age of Chronicle's oracle value. - /// @param timestamp The timestamp when the Chronicle oracle was read. - event ChronicleValueStale(uint age, uint timestamp); - - /// @notice Emitted when Chainlink's oracle delivered a stale value. - /// @param age The age of Chainlink's oracle value. - /// @param timestamp The timestamp when the Chainlink oracle was read. - event ChainlinkValueStale(uint age, uint timestamp); - - /// @notice Emitted when Chainlink's oracle delivered a negative value. - /// @param value The value the Chainlink oracle delivered. - event ChainlinkValueNegative(int value); - /// @notice Emitted when Chainlink's oracle delivered a zero value. event ChainlinkValueZero(); - /// @notice Emitted when Uniswap's oracle delivered a zero value. - event UniswapValueZero(); - /// @notice The Chronicle oracle to aggregate. /// @return The address of the Chronicle oracle being aggregated. function chronicle() external view returns (address); @@ -90,6 +73,10 @@ interface IAggor is IChronicle { /// @notice The time in seconds to "look back" per TWAP. function uniSecondsAgo() external view returns (uint32); + /// @notice Determines which secondary oracle is selected. If false + // (default), use Chainlink. + function uniswapSelected() external view returns (bool); + /// @notice The minimum allowed lookback period for the Uniswap TWAP. /// @dev Value is constant and save to cache. /// @return The minimum allowed value for uniSecondsAgo. @@ -164,9 +151,9 @@ interface IAggor is IChronicle { /// @notice Switch from default oracle (Chainlink) to alt (Uniswap), /// and back. /// @dev Only callable by auth'ed address. - /// @param uniPool Provide the address to the Uniswap pool. If set to - // address(0) Uniswap will not be used. - function setUniswap(address uniPool) external; + /// @param select If true will swap to Uniswap. If false will select + /// Chainlink (default). + function useUniswap(bool select) external; /// @notice Set the Uniswap TWAP lookback period. If never called, default // is 5m. diff --git a/src/libs/LibCalc.sol b/src/libs/LibCalc.sol index 2ace50d..c0c2f37 100644 --- a/src/libs/LibCalc.sol +++ b/src/libs/LibCalc.sol @@ -40,7 +40,6 @@ library LibCalc { if (n == 0) return 0; require(dec > 0 && destDec > 0); - require(n > dec && n > destDec); return destDec > dec ? n * (10 ** (destDec - dec)) // Scale up diff --git a/test/IAggorTest.sol b/test/IAggorTest.sol index 9fef9d9..d32e206 100644 --- a/test/IAggorTest.sol +++ b/test/IAggorTest.sol @@ -8,11 +8,11 @@ import {IToll} from "chronicle-std/toll/IToll.sol"; import {LibCalc} from "src/libs/LibCalc.sol"; import {IAggor} from "src/IAggor.sol"; +import {Aggor} from "src/Aggor.sol"; import {MockIChronicle} from "./mocks/MockIChronicle.sol"; import {MockIChainlinkAggregatorV3} from "./mocks/MockIChainlinkAggregatorV3.sol"; -import {MockUniswapPool} from "./mocks/MockUniswapPool.sol"; import {MockIERC20} from "./mocks/MockIERC20.sol"; abstract contract IAggorTest is Test { @@ -20,9 +20,6 @@ abstract contract IAggorTest is Test { MockIChronicle chronicle; MockIChainlinkAggregatorV3 chainlink; - MockUniswapPool uniPool; - MockIERC20 uniPoolToken0; - MockIERC20 uniPoolToken1; /// @dev Must match the value in Aggor.sol uint16 internal constant _pscale = 10_000; @@ -44,10 +41,6 @@ abstract contract IAggorTest is Test { uint32 oldUniswapSecondsAgo, uint32 newUniswapSecondsAgo ); - event ChronicleValueStale(uint age, uint timestamp); - event ChainlinkValueStale(uint age, uint timestamp); - event ChainlinkValueNegative(int value); - event ChainlinkValueZero(); function setUp(IAggor aggor_) internal { aggor = aggor_; @@ -55,13 +48,6 @@ abstract contract IAggorTest is Test { chronicle = MockIChronicle(aggor.chronicle()); chainlink = MockIChainlinkAggregatorV3(aggor.chainlink()); - uniPoolToken0 = - new MockIERC20("Uniswap Pool Token 0", "UniToken0", uint8(18)); - uniPoolToken1 = - new MockIERC20("Uniswap Pool Token 1", "UniToken1", uint8(18)); - uniPool = - new MockUniswapPool(address(uniPoolToken0), address(uniPoolToken1)); - // Toll address(this). IToll(address(aggor)).kiss(address(this)); } @@ -250,9 +236,6 @@ abstract contract IAggorTest is Test { ); chronicle.setAge(chronicleAge); - vm.expectEmit(); - emit ChronicleValueStale(chronicleAge, block.timestamp); - vm.expectRevert( abi.encodeWithSelector( IAggor.OracleReadFailed.selector, address(chronicle) @@ -268,9 +251,6 @@ abstract contract IAggorTest is Test { // Let chainlink's val be zero. chainlink.setAnswer(0); - vm.expectEmit(); - emit ChainlinkValueZero(); - vm.expectRevert( abi.encodeWithSelector( IAggor.OracleReadFailed.selector, address(chainlink) @@ -294,9 +274,6 @@ abstract contract IAggorTest is Test { ); chainlink.setUpdatedAt(chainlinkAge); - vm.expectEmit(); - emit ChainlinkValueStale(chainlinkAge, block.timestamp); - vm.expectRevert( abi.encodeWithSelector( IAggor.OracleReadFailed.selector, address(chainlink) @@ -316,9 +293,6 @@ abstract contract IAggorTest is Test { int chainlinkVal = bound(chainlinkValSeed, type(int).min, -1); chainlink.setAnswer(chainlinkVal); - vm.expectEmit(); - emit ChainlinkValueNegative(chainlinkVal); - vm.expectRevert( abi.encodeWithSelector( IAggor.OracleReadFailed.selector, address(chainlink) @@ -491,6 +465,16 @@ abstract contract IAggorTest is Test { aggor.setSpread(1); } + function test_useUniswap_IsAuthProtected() public { + vm.prank(address(0xbeef)); + vm.expectRevert( + abi.encodeWithSelector( + IAuth.NotAuthorized.selector, address(0xbeef) + ) + ); + aggor.useUniswap(true); + } + function testFuzz_setUniSecondsAgo(uint32 uniSecondsAgo) public { vm.assume(uniSecondsAgo >= aggor.minUniSecondsAgo()); @@ -524,42 +508,11 @@ abstract contract IAggorTest is Test { aggor.setUniSecondsAgo(1); } - function test_setUniswap_FromZeroAddress() public { - vm.expectEmit(); - emit UniswapUpdated(address(this), address(0), address(uniPool)); - - aggor.setUniswap(address(uniPool)); - - assertEq(aggor.uniPool(), address(uniPool)); - assertEq(aggor.uniBasePair(), address(uniPoolToken0)); - assertEq(aggor.uniQuotePair(), address(uniPoolToken1)); - assertEq(aggor.uniBaseDec(), uint8(18)); - assertEq(aggor.uniQuoteDec(), uint8(18)); - } - - function test_setUniswap_ToZeroAddress() public { - aggor.setUniswap(address(uniPool)); - - vm.expectEmit(); - emit UniswapUpdated(address(this), address(uniPool), address(0)); - - aggor.setUniswap(address(0)); - - assertEq(aggor.uniPool(), address(0)); - assertEq(aggor.uniBasePair(), address(0)); - assertEq(aggor.uniQuotePair(), address(0)); - assertEq(aggor.uniBaseDec(), uint8(0)); - assertEq(aggor.uniQuoteDec(), uint8(0)); - } - - function test_setUniswap_IsAuthProtected() public { - vm.prank(address(0xbeef)); - vm.expectRevert( - abi.encodeWithSelector( - IAuth.NotAuthorized.selector, address(0xbeef) - ) - ); - aggor.setUniswap(address(0)); + function test_useUniswap_NotConfigured() public { + IAggor aggor_ = new Aggor( + aggor.chronicle(), aggor.chainlink(), address(0), false); + vm.expectRevert(); + aggor_.useUniswap(true); } // -- Private Helpers -- diff --git a/test/LibCalcTest.sol b/test/LibCalcTest.sol index eea6231..2243d00 100644 --- a/test/LibCalcTest.sol +++ b/test/LibCalcTest.sol @@ -11,6 +11,12 @@ abstract contract LibCalcTest is Test { assertEq(scaled, 14_645_946); scaled = LibCalc.scale(14_645_946, 6, 18); assertEq(scaled, 14_645_946_000_000_000_000); + + assertEq(1 ether, LibCalc.scale(1 ether, 999, 999)); + assertEq( + 10_000_000_000_000_000_000_000_000_000_000_000_000_000, + LibCalc.scale(1, 10, 50) + ); } function test_scale_revert() public { @@ -24,20 +30,12 @@ abstract contract LibCalcTest is Test { vm.expectRevert(); LibCalc.scale(1, 1, 0); - // Value can't be less than or equal to either decimal - vm.expectRevert(); - LibCalc.scale(999, 999, 1000); - - vm.expectRevert(); - LibCalc.scale(999, 1000, 900); - - // Makes no sense to call when decimals are the same - vm.expectRevert(); - LibCalc.scale(1 ether, 999, 999); - // Overflow/underflow vm.expectRevert(); LibCalc.scale(12_345, 99, 1); + + vm.expectRevert(); + LibCalc.scale(100, 1000, 10_000); } function test_distance() public { diff --git a/test/MainnetIntegration.t.sol b/test/MainnetIntegration.t.sol index 84707f5..63bc0a7 100644 --- a/test/MainnetIntegration.t.sol +++ b/test/MainnetIntegration.t.sol @@ -84,7 +84,8 @@ contract MainnetIntegrationTest is Test { vm.prank(PAUSE_PROXY); IToll(address(MEDIAN_ETHUSD)).kiss(address(medianWrapper)); - aggor = new Aggor(address(medianWrapper), CHAINLINK_ETHUSD); + aggor = new Aggor( + address(medianWrapper), CHAINLINK_ETHUSD, UNI_POOL_WETHUSDT, true); IToll(address(aggor)).kiss(address(this)); } @@ -140,7 +141,7 @@ contract MainnetIntegrationTest is Test { ); // Switch to Uniswap - aggor.setUniswap(UNI_POOL_WETHUSDT); + aggor.useUniswap(true); // Test mean with Uni aggor.setSpread(uint16(spreadUni) + 1); @@ -159,5 +160,23 @@ contract MainnetIntegrationTest is Test { ? chronval : unival ); + + // Switch back to Chainlink and verify state has changed. + aggor.useUniswap(false); + assertTrue(!aggor.uniswapSelected()); + } + + // Ensure that Aggor will revert if dev is requesting a lookback period + // that is too far. + function testIntegration_UniswapLookback() public { + aggor.useUniswap(true); + assertTrue(aggor.uniswapSelected()); + + aggor.poke(); + + vm.expectRevert(); + aggor.setUniSecondsAgo(type(uint32).max); + + aggor.poke(); } } diff --git a/test/Runner.t.sol b/test/Runner.t.sol index 55cbfd6..bc677ee 100644 --- a/test/Runner.t.sol +++ b/test/Runner.t.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.16; import {MockIChronicle} from "./mocks/MockIChronicle.sol"; import {MockIChainlinkAggregatorV3} from "./mocks/MockIChainlinkAggregatorV3.sol"; +import {MockUniswapPool} from "./mocks/MockUniswapPool.sol"; +import {MockIERC20} from "./mocks/MockIERC20.sol"; // -- Aggor Tests -- @@ -12,11 +14,24 @@ import {Aggor} from "src/Aggor.sol"; import {IAggorTest} from "./IAggorTest.sol"; contract AggorTest is IAggorTest { + MockUniswapPool uniPool; + MockIERC20 uniPoolToken0; + MockIERC20 uniPoolToken1; + function setUp() public { + uniPoolToken0 = + new MockIERC20("Uniswap Pool Token 0", "UniToken0", uint8(18)); + uniPoolToken1 = + new MockIERC20("Uniswap Pool Token 1", "UniToken1", uint8(18)); + uniPool = + new MockUniswapPool(address(uniPoolToken0), address(uniPoolToken1)); + setUp( new Aggor( address(new MockIChronicle()), - address(new MockIChainlinkAggregatorV3()) + address(new MockIChainlinkAggregatorV3()), + address(uniPool), + true ) ); } diff --git a/test/mocks/MockUniswapPool.sol b/test/mocks/MockUniswapPool.sol index 7fe7035..fb6dac7 100644 --- a/test/mocks/MockUniswapPool.sol +++ b/test/mocks/MockUniswapPool.sol @@ -4,10 +4,22 @@ pragma solidity ^0.8.16; contract MockUniswapPool { address internal _token0; // Should be of type IERC20 address internal _token1; // Should be of type IERC20 + int56[] internal tickCumulatives; + uint160[] internal secondsPerLiquidityCumulativeX128s; constructor(address token0_, address token1_) { _token0 = token0_; _token1 = token1_; + + // @todo For better test coverage we should add the ability to + // configure these values. Currently we're just using these + // static values obtained from an actual Uniswap pool. + tickCumulatives = + [int56(-13_916_113_000_655), int56(-13_916_173_262_387)]; + secondsPerLiquidityCumulativeX128s = [ + uint160(765_477_856_542_428_689_888_050_014_397), + uint160(765_477_865_195_500_996_853_711_067_727) + ]; } // -- IUniswapV3PoolImmutables Functionality -- @@ -19,4 +31,12 @@ contract MockUniswapPool { function token1() external view returns (address) { return _token1; } + + function observe(uint32[] calldata) + external + view + returns (int56[] memory, uint160[] memory) + { + return (tickCumulatives, secondsPerLiquidityCumulativeX128s); + } }