diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa088b8..dcc449d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version-file: ".nvmrc" - run: npm ci @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - uses: foundry-rs/foundry-toolchain@v1 @@ -32,10 +32,22 @@ jobs: version: nightly - run: npm run test:unit + test-unit-large: # For large/complex tests that require via-ir compilation + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + - run: npm run test:unit-large + test-integration: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - uses: foundry-rs/foundry-toolchain@v1 @@ -47,7 +59,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - uses: foundry-rs/foundry-toolchain@v1 diff --git a/.nvmrc b/.nvmrc index 7950a44..42e31a0 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.17.0 +v20.14.0 diff --git a/foundry.toml b/foundry.toml index 3da5378..2df2a55 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,6 +1,5 @@ [profile.default] solc = "0.8.26" -#via_ir = true bytecode_hash = "ipfs" optimizer_runs = 1_000_000 libs = ['lib'] @@ -14,6 +13,13 @@ fs_permissions = [ { access = "read", path = "./out" } ] ignored_error_codes = [] +skip = ["test/large/*.sol"] + +[profile.large-test] +via_ir = true +match_path = "test/large/*.sol" +match_test = "testGetQuote_RandomizeAllParam_3HopRoute" +skip = [] [profile.integration] match_path = "test/integration/*.sol" diff --git a/package.json b/package.json index 027c5e7..baa1cc9 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,9 @@ "submodule:reset": "git submodule update --recursive", "test": "npm run test:unit", "test:all": "npm run test:unit && npm run test:integration", - "test:integration": "export FOUNDRY_PROFILE=integration && forge test ", - "test:unit": "forge test" + "test:integration": "export FOUNDRY_PROFILE=integration && forge test", + "test:unit": "forge test", + "test:unit-large": "export FOUNDRY_PROFILE=large-test && forge test" }, "devDependencies": { "markdownlint": "0.34.0", diff --git a/src/ReservoirPriceOracle.sol b/src/ReservoirPriceOracle.sol index c1c05e0..fc63e58 100644 --- a/src/ReservoirPriceOracle.sol +++ b/src/ReservoirPriceOracle.sol @@ -461,7 +461,11 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg. } } - function _useFallbackOracle(uint256 aAmount, address aBase, address aQuote, bool aIsGetQuotes) internal view returns (uint256 rBidOut, uint256 rAskOut) { + function _useFallbackOracle(uint256 aAmount, address aBase, address aQuote, bool aIsGetQuotes) + internal + view + returns (uint256 rBidOut, uint256 rAskOut) + { if (fallbackOracle == address(0)) revert OracleErrors.NoPath(); // We do not catch errors here so the fallback oracle will revert if it doesn't support the query. diff --git a/test/__fixtures/BaseTest.t.sol b/test/__fixtures/BaseTest.t.sol index e0ef5ab..68adf70 100644 --- a/test/__fixtures/BaseTest.t.sol +++ b/test/__fixtures/BaseTest.t.sol @@ -19,7 +19,8 @@ contract BaseTest is Test { GenericFactory internal _factory = new GenericFactory(); ReservoirPair internal _pair; - ReservoirPriceOracle internal _oracle = new ReservoirPriceOracle(0.02e18, 15 minutes, 500_000, PriceType.CLAMPED_PRICE); + ReservoirPriceOracle internal _oracle = + new ReservoirPriceOracle(0.02e18, 15 minutes, 500_000, PriceType.CLAMPED_PRICE); MintableERC20 internal _tokenA = MintableERC20(address(0x100)); MintableERC20 internal _tokenB = MintableERC20(address(0x200)); diff --git a/test/large/ReservoirPriceOracleLarge.t.sol b/test/large/ReservoirPriceOracleLarge.t.sol new file mode 100644 index 0000000..f30898b --- /dev/null +++ b/test/large/ReservoirPriceOracleLarge.t.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import { + ReservoirPriceOracleTest, + EnumerableSetLib, + MintableERC20, + ReservoirPair, + IERC20 +} from "test/unit/ReservoirPriceOracle.t.sol"; + +contract ReservoirPriceOracleLargeTest is ReservoirPriceOracleTest { + using EnumerableSetLib for EnumerableSetLib.AddressSet; + + function testGetQuote_RandomizeAllParam_3HopRoute( + uint256 aPrice1, + uint256 aPrice2, + uint256 aPrice3, + uint256 aAmtIn, + address aTokenAAddress, + address aTokenBAddress, + address aTokenCAddress, + address aTokenDAddress, + uint8 aTokenADecimal, + uint8 aTokenBDecimal, + uint8 aTokenCDecimal, + uint8 aTokenDDecimal + ) external { + // assume + vm.assume( + aTokenAAddress > ADDRESS_THRESHOLD && aTokenBAddress > ADDRESS_THRESHOLD + && aTokenCAddress > ADDRESS_THRESHOLD && aTokenDAddress > ADDRESS_THRESHOLD + ); + vm.assume( + _addressSet.add(aTokenAAddress) && _addressSet.add(aTokenBAddress) && _addressSet.add(aTokenCAddress) + && _addressSet.add(aTokenDAddress) + ); + uint256 lPrice1 = bound(aPrice1, 1e12, 1e24); + uint256 lPrice2 = bound(aPrice2, 1e12, 1e24); + uint256 lPrice3 = bound(aPrice3, 1e12, 1e24); + uint256 lAmtIn = bound(aAmtIn, 0, 1_000_000_000); + uint256 lTokenADecimal = bound(aTokenADecimal, 0, 18); + uint256 lTokenBDecimal = bound(aTokenBDecimal, 0, 18); + uint256 lTokenCDecimal = bound(aTokenCDecimal, 0, 18); + uint256 lTokenDDecimal = bound(aTokenDDecimal, 0, 18); + + // arrange + MintableERC20 lTokenA = MintableERC20(aTokenAAddress); + MintableERC20 lTokenB = MintableERC20(aTokenBAddress); + MintableERC20 lTokenC = MintableERC20(aTokenCAddress); + MintableERC20 lTokenD = MintableERC20(aTokenDAddress); + deployCodeTo("MintableERC20.sol", abi.encode("T", "T", uint8(lTokenADecimal)), aTokenAAddress); + deployCodeTo("MintableERC20.sol", abi.encode("T", "T", uint8(lTokenBDecimal)), aTokenBAddress); + deployCodeTo("MintableERC20.sol", abi.encode("T", "T", uint8(lTokenCDecimal)), aTokenCAddress); + deployCodeTo("MintableERC20.sol", abi.encode("T", "T", uint8(lTokenDDecimal)), aTokenDAddress); + + ReservoirPair lPair1 = ReservoirPair(_factory.createPair(IERC20(aTokenAAddress), IERC20(aTokenBAddress), 0)); + ReservoirPair lPair2 = ReservoirPair(_factory.createPair(IERC20(aTokenBAddress), IERC20(aTokenCAddress), 0)); + ReservoirPair lPair3 = ReservoirPair(_factory.createPair(IERC20(aTokenCAddress), IERC20(aTokenDAddress), 0)); + + _oracle.designatePair(aTokenAAddress, aTokenBAddress, lPair1); + _oracle.designatePair(aTokenBAddress, aTokenCAddress, lPair2); + _oracle.designatePair(aTokenCAddress, aTokenDAddress, lPair3); + + address[] memory lRoute = new address[](4); + if (lTokenA < lTokenD) { + lRoute[0] = aTokenAAddress; + lRoute[1] = aTokenBAddress; + lRoute[2] = aTokenCAddress; + lRoute[3] = aTokenDAddress; + } else { + lRoute[0] = aTokenDAddress; + lRoute[1] = aTokenCAddress; + lRoute[2] = aTokenBAddress; + lRoute[3] = aTokenAAddress; + } + + _oracle.setRoute(lRoute[0], lRoute[3], lRoute); + _writePriceCache( + lTokenA < lTokenB ? aTokenAAddress : aTokenBAddress, + lTokenA < lTokenB ? aTokenBAddress : aTokenAAddress, + lPrice1 + ); + _writePriceCache( + lTokenB < lTokenC ? aTokenBAddress : aTokenCAddress, + lTokenB < lTokenC ? aTokenCAddress : aTokenBAddress, + lPrice2 + ); + _writePriceCache( + lTokenC < lTokenD ? aTokenCAddress : aTokenDAddress, + lTokenC < lTokenD ? aTokenDAddress : aTokenCAddress, + lPrice3 + ); + + // act + uint256 lAmtDOut = _oracle.getQuote(lAmtIn * 10 ** lTokenADecimal, aTokenAAddress, aTokenDAddress); + + // assert + uint256 lExpectedAmtBOut = lTokenA < lTokenB + ? lAmtIn * 10 ** lTokenADecimal * lPrice1 * 10 ** lTokenBDecimal / 10 ** lTokenADecimal / WAD + : lAmtIn * 10 ** lTokenADecimal * WAD * 10 ** lTokenBDecimal / lPrice1 / 10 ** lTokenADecimal; + uint256 lExpectedAmtCOut = lTokenB < lTokenC + ? lExpectedAmtBOut * lPrice2 * 10 ** lTokenCDecimal / 10 ** lTokenBDecimal / WAD + : lExpectedAmtBOut * WAD * 10 ** lTokenCDecimal / lPrice2 / 10 ** lTokenBDecimal; + uint256 lExpectedAmtDOut = lTokenC < lTokenD + ? lExpectedAmtCOut * lPrice3 * 10 ** lTokenDDecimal / 10 ** lTokenCDecimal / WAD + : lExpectedAmtCOut * WAD * 10 ** lTokenDDecimal / lPrice3 / 10 ** lTokenCDecimal; + + assertEq(lAmtDOut, lExpectedAmtDOut); + } +} diff --git a/test/mock/MockFallbackOracle.sol b/test/mock/MockFallbackOracle.sol index 3138cd7..af355c0 100644 --- a/test/mock/MockFallbackOracle.sol +++ b/test/mock/MockFallbackOracle.sol @@ -12,11 +12,7 @@ contract MockFallbackOracle is IPriceOracle { out = amount; } - function getQuotes(uint256 amount, address, address) - external - pure - returns (uint256 bidOut, uint256 askOut) - { + function getQuotes(uint256 amount, address, address) external pure returns (uint256 bidOut, uint256 askOut) { (bidOut, askOut) = (amount, amount); } } diff --git a/test/mock/StubERC4626.sol b/test/mock/StubERC4626.sol index 3a200b4..ce3ca39 100644 --- a/test/mock/StubERC4626.sol +++ b/test/mock/StubERC4626.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.0; contract StubERC4626 { address public asset; uint256 private rate; - string revertMsg = "oops"; - bool doRevert; + string public revertMsg = "oops"; + bool public doRevert; constructor(address _asset, uint256 _rate) { asset = _asset; diff --git a/test/unit/ReservoirPriceOracle.t.sol b/test/unit/ReservoirPriceOracle.t.sol index fbfd4e3..111bdb4 100644 --- a/test/unit/ReservoirPriceOracle.t.sol +++ b/test/unit/ReservoirPriceOracle.t.sol @@ -33,7 +33,7 @@ contract ReservoirPriceOracleTest is BaseTest { event RewardGasAmount(uint256 newAmount); event Route(address token0, address token1, address[] route); - uint256 private constant WAD = 1e18; + uint256 internal constant WAD = 1e18; address internal constant ADDRESS_THRESHOLD = address(0x1000); @@ -356,89 +356,6 @@ contract ReservoirPriceOracleTest is BaseTest { assertEq(lAmtCOut, lExpectedAmtCOut); } - // function testGetQuote_RandomizeAllParam_3HopRoute( - // uint256 aPrice1, - // uint256 aPrice2, - // uint256 aPrice3, - // uint256 aAmtIn, - // address aTokenAAddress, - // address aTokenBAddress, - // address aTokenCAddress, - // address aTokenDAddress, - // uint8 aTokenADecimal, - // uint8 aTokenBDecimal, - // uint8 aTokenCDecimal, - // uint8 aTokenDDecimal - // ) external { - // // assume - // vm.assume( - // aTokenAAddress > ADDRESS_THRESHOLD && aTokenBAddress > ADDRESS_THRESHOLD - // && aTokenCAddress > ADDRESS_THRESHOLD && aTokenDAddress > ADDRESS_THRESHOLD - // ); - // vm.assume( - // _addressSet.add(aTokenAAddress) && _addressSet.add(aTokenBAddress) && _addressSet.add(aTokenCAddress) - // && _addressSet.add(aTokenDAddress) - // ); - // uint256 lPrice1 = bound(aPrice1, 1e12, 1e24); // need to bound price within this range as a price below this will go to zero as during the mul and div of prices - // uint256 lPrice2 = bound(aPrice2, 1e12, 1e24); - // uint256 lPrice3 = bound(aPrice3, 1e12, 1e24); - // uint256 lAmtIn = bound(aAmtIn, 0, 1_000_000_000); - // uint256 lTokenADecimal = bound(aTokenADecimal, 0, 18); - // uint256 lTokenBDecimal = bound(aTokenBDecimal, 0, 18); - // uint256 lTokenCDecimal = bound(aTokenCDecimal, 0, 18); - // uint256 lTokenDDecimal = bound(aTokenDDecimal, 0, 18); - // - // // arrange - // MintableERC20 lTokenA = MintableERC20(aTokenAAddress); - // MintableERC20 lTokenB = MintableERC20(aTokenBAddress); - // MintableERC20 lTokenC = MintableERC20(aTokenCAddress); - // MintableERC20 lTokenD = MintableERC20(aTokenDAddress); - // deployCodeTo("MintableERC20.sol", abi.encode("T", "T", uint8(lTokenADecimal)), address(lTokenA)); - // deployCodeTo("MintableERC20.sol", abi.encode("T", "T", uint8(lTokenBDecimal)), address(lTokenB)); - // deployCodeTo("MintableERC20.sol", abi.encode("T", "T", uint8(lTokenCDecimal)), address(lTokenC)); - // deployCodeTo("MintableERC20.sol", abi.encode("T", "T", uint8(lTokenDDecimal)), address(lTokenD)); - // - // ReservoirPair lPair1 = ReservoirPair(_factory.createPair(IERC20(address(lTokenA)), IERC20(address(lTokenB)), 0)); - // ReservoirPair lPair2 = ReservoirPair(_factory.createPair(IERC20(address(lTokenB)), IERC20(address(lTokenC)), 0)); - // ReservoirPair lPair3 = ReservoirPair(_factory.createPair(IERC20(address(lTokenC)), IERC20(address(lTokenD)), 0)); - // - // _oracle.designatePair(address(lTokenA), address(lTokenB), lPair1); - // _oracle.designatePair(address(lTokenB), address(lTokenC), lPair2); - // _oracle.designatePair(address(lTokenC), address(lTokenD), lPair3); - // - // address[] memory lRoute = new address[](4); - // (lRoute[0], lRoute[3]) = - // lTokenA < lTokenD ? (address(lTokenA), address(lTokenD)) : (address(lTokenD), address(lTokenA)); - // lRoute[1] = address(lTokenB); - // lRoute[2] = address(lTokenC); - // - // _oracle.setRoute(lRoute[0], lRoute[3], lRoute); - // _writePriceCache( - // lRoute[0] < lRoute[1] ? lRoute[0] : lRoute[1], lRoute[0] < lRoute[1] ? lRoute[1] : lRoute[0], lPrice1 - // ); - // _writePriceCache( - // address(lTokenB) < address(lTokenC) ? address(lTokenB) : address(lTokenC), - // address(lTokenB) < address(lTokenC) ? address(lTokenC) : address(lTokenB), - // lPrice2 - // ); - // _writePriceCache( - // lRoute[2] < lRoute[3] ? lRoute[2] : lRoute[3], lRoute[2] < lRoute[3] ? lRoute[3] : lRoute[2], lPrice3 - // ); - // - // // act - // uint256 lAmtDOut = _oracle.getQuote(lAmtIn * 10 ** lTokenADecimal, address(lTokenA), address(lTokenD)); - // - // // assert - // uint256 lPriceStartEnd = (lRoute[0] < lRoute[1] ? lPrice1 : lPrice1.invertWad()) - // * (lRoute[1] < lRoute[2] ? lPrice2 : lPrice2.invertWad()) / WAD - // * (lRoute[2] < lRoute[3] ? lPrice3 : lPrice3.invertWad()) / WAD; - // assertEq( - // lAmtDOut, - // lAmtIn * (lRoute[0] == address(lTokenA) ? lPriceStartEnd : lPriceStartEnd.invertWad()) - // * (10 ** lTokenDDecimal) / WAD - // ); - // } - function testGetQuotes(uint256 aPrice, uint256 aAmountIn) external { // assume uint256 lPrice = bound(aPrice, 1, 1e36);