From bd3aa0fca9b4f9b92f90d24d0c9d53742b615226 Mon Sep 17 00:00:00 2001 From: simplyoptimistic <111120814+simplyoptimistic@users.noreply.github.com> Date: Thu, 4 Apr 2024 19:30:40 +1100 Subject: [PATCH 1/4] chore: fetch pools without ext call and fetch reserves directly --- contracts/oracles/VelodromeV2Oracle.sol | 65 +++++++++++++------------ test/helpers.js | 4 +- test/oracles/VelodromeV2Oracle.js | 2 +- 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/contracts/oracles/VelodromeV2Oracle.sol b/contracts/oracles/VelodromeV2Oracle.sol index 73921ad6..48ed2167 100644 --- a/contracts/oracles/VelodromeV2Oracle.sol +++ b/contracts/oracles/VelodromeV2Oracle.sol @@ -6,64 +6,67 @@ pragma solidity 0.8.23; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../interfaces/IOracle.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts/proxy/Clones.sol"; import "../libraries/OraclePrices.sol"; -interface IVelodramoV2Router { - function getReserves(address tokenA, address tokenB, bool stable, address _factory) external view returns (uint256 reserveA, uint256 reserveB); +interface IVelodromeV2Pool { + function getReserves() external view returns (uint256 _reserve0, uint256 _reserve1, uint256 _timestampLast); + function token0() external view returns (address); } -interface IVelodramoV2Registry { - function poolFactories() external view returns (address[] memory); -} - - contract VelodromeV2Oracle is IOracle { using OraclePrices for OraclePrices.Data; using Math for uint256; IERC20 private constant _NONE = IERC20(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF); - IVelodramoV2Router public immutable ROUTER; - IVelodramoV2Registry public immutable REGISTRY; + address public immutable POOL_FACTORY; + address public immutable POOL_IMPLEMENTATION; - constructor(IVelodramoV2Router _router, IVelodramoV2Registry _registry) { - ROUTER = _router; - REGISTRY = _registry; + constructor(address _poolFactory, address _poolImplementation) { + POOL_FACTORY = _poolFactory; + POOL_IMPLEMENTATION = _poolImplementation; } function getRate(IERC20 srcToken, IERC20 dstToken, IERC20 connector, uint256 thresholdFilter) external view override returns (uint256 rate, uint256 weight) { - address[] memory factories = REGISTRY.poolFactories(); if (connector == _NONE) { - (rate, weight) = _getWeightedRate(srcToken, dstToken, factories, thresholdFilter); + (rate, weight) = _getWeightedRate(srcToken, dstToken, thresholdFilter); } else { - (uint256 rateC0, uint256 weightC0) = _getWeightedRate(srcToken, connector, factories, thresholdFilter); - (uint256 rateC1, uint256 weightC1) = _getWeightedRate(connector, dstToken, factories, thresholdFilter); + (uint256 rateC0, uint256 weightC0) = _getWeightedRate(srcToken, connector, thresholdFilter); + (uint256 rateC1, uint256 weightC1) = _getWeightedRate(connector, dstToken, thresholdFilter); rate = rateC0 * rateC1 / 1e18; weight = Math.min(weightC0, weightC1); } } - function _getWeightedRate(IERC20 srcToken, IERC20 dstToken, address[] memory factories, uint256 thresholdFilter) internal view returns (uint256 rate, uint256 weight) { - uint256 factoriesLength = factories.length; - OraclePrices.Data memory ratesAndWeights = OraclePrices.init(2 * factoriesLength); + function _getWeightedRate(IERC20 srcToken, IERC20 dstToken, uint256 thresholdFilter) internal view returns (uint256 rate, uint256 weight) { + OraclePrices.Data memory ratesAndWeights = OraclePrices.init(2); uint256 b0; uint256 b1; - for (uint256 i = 0; i < factoriesLength; i++) { - (b0, b1) = _getReserves(srcToken, dstToken, true, factories[i]); - if (b0 > 0) { - ratesAndWeights.append(OraclePrices.OraclePrice(Math.mulDiv(b1, 1e18, b0), (b0 * b1).sqrt())); - } - (b0, b1) = _getReserves(srcToken, dstToken, false, factories[i]); - if (b0 > 0) { - ratesAndWeights.append(OraclePrices.OraclePrice(Math.mulDiv(b1, 1e18, b0), (b0 * b1).sqrt())); - } + (b0, b1) = _getReserves(srcToken, dstToken, true); + if (b0 > 0) { + ratesAndWeights.append(OraclePrices.OraclePrice(Math.mulDiv(b1, 1e18, b0), (b0 * b1).sqrt())); + } + (b0, b1) = _getReserves(srcToken, dstToken, false); + if (b0 > 0) { + ratesAndWeights.append(OraclePrices.OraclePrice(Math.mulDiv(b1, 1e18, b0), (b0 * b1).sqrt())); } (rate, weight) = ratesAndWeights.getRateAndWeight(thresholdFilter); } - function _getReserves(IERC20 srcToken, IERC20 dstToken, bool stable, address factory) internal view returns (uint256 reserveSrc, uint256 reserveDst) { - try ROUTER.getReserves(address(srcToken), address(dstToken), stable, factory) returns (uint256 reserveSrc_, uint256 reserveDst_) { - (reserveSrc, reserveDst) = (reserveSrc_, reserveDst_); + function _getReserves(IERC20 srcToken, IERC20 dstToken, bool stable) internal view returns (uint256 reserveSrc, uint256 reserveDst) { + (IERC20 token0, IERC20 token1) = srcToken < dstToken ? (srcToken, dstToken) : (dstToken, srcToken); + address pool = Clones.predictDeterministicAddress({ + implementation: POOL_IMPLEMENTATION, + salt: keccak256(abi.encodePacked(address(token0), address(token1), stable)), + deployer: POOL_FACTORY + }); + try IVelodromeV2Pool(pool).getReserves() returns (uint256 reserve0, uint256 reserve1, uint256) { + if (srcToken == token0) { + (reserveSrc, reserveDst) = (reserve0, reserve1); + } else { + (reserveSrc, reserveDst) = (reserve1, reserve0); + } } catch {} // solhint-disable-line no-empty-blocks } } diff --git a/test/helpers.js b/test/helpers.js index 30266800..6bf27176 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -152,8 +152,8 @@ const deployParams = { initcodeHash: '0x0ccd005ee58d5fb11632ef5c2e0866256b240965c62c8e990c0f84a97f311879', }, VelodromeV2: { // optimistic network - router: '0xa062aE8A9c5e11aaA026fc2670B0D65cCc8B2858', - registry: '0xF4c67CdEAaB8360370F41514d06e32CcD8aA1d7B', + poolFactory: '0xF1046053aa5682b4F9a81b5481394DA16BE5FF5a', + poolImplementation: '0x95885Af5492195F0754bE71AD1545Fe81364E531', }, }; diff --git a/test/oracles/VelodromeV2Oracle.js b/test/oracles/VelodromeV2Oracle.js index fa4c80dc..54496527 100644 --- a/test/oracles/VelodromeV2Oracle.js +++ b/test/oracles/VelodromeV2Oracle.js @@ -20,7 +20,7 @@ describe('VelodromeV2Oracle', function () { }); async function initContracts () { - const velodromeV2Oracle = await deployContract('VelodromeV2Oracle', [VelodromeV2.router, VelodromeV2.registry]); + const velodromeV2Oracle = await deployContract('VelodromeV2Oracle', [VelodromeV2.poolFactory, VelodromeV2.poolImplementation]); const uniswapV3Oracle = await deployContract('UniswapV3LikeOracle', [UniswapV3.factory, UniswapV3.initcodeHash, UniswapV3.fees]); return { velodromeV2Oracle, uniswapV3Oracle }; } From be8f0d15985484c61d16a8c11ee2fceedcbf13bd Mon Sep 17 00:00:00 2001 From: Denis Date: Wed, 17 Apr 2024 14:26:40 +0100 Subject: [PATCH 2/4] Different logic for different factories in the same oracle --- contracts/oracles/VelodromeV2Oracle.sol | 104 ++++++++++++++++-------- test/helpers.js | 13 ++- test/oracles/VelodromeV2Oracle.js | 58 ++++++++++++- 3 files changed, 138 insertions(+), 37 deletions(-) diff --git a/contracts/oracles/VelodromeV2Oracle.sol b/contracts/oracles/VelodromeV2Oracle.sol index 48ed2167..4135da5c 100644 --- a/contracts/oracles/VelodromeV2Oracle.sol +++ b/contracts/oracles/VelodromeV2Oracle.sol @@ -6,12 +6,18 @@ pragma solidity 0.8.23; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../interfaces/IOracle.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; -import "@openzeppelin/contracts/proxy/Clones.sol"; import "../libraries/OraclePrices.sol"; +interface IVelodramoV2Router { + function getReserves(address tokenA, address tokenB, bool stable, address _factory) external view returns (uint256 reserveA, uint256 reserveB); +} + +interface IVelodramoV2Registry { + function poolFactories() external view returns (address[] memory); +} + interface IVelodromeV2Pool { function getReserves() external view returns (uint256 _reserve0, uint256 _reserve1, uint256 _timestampLast); - function token0() external view returns (address); } contract VelodromeV2Oracle is IOracle { @@ -20,53 +26,87 @@ contract VelodromeV2Oracle is IOracle { IERC20 private constant _NONE = IERC20(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF); - address public immutable POOL_FACTORY; - address public immutable POOL_IMPLEMENTATION; + IVelodramoV2Router public immutable ROUTER; + IVelodramoV2Registry public immutable REGISTRY; - constructor(address _poolFactory, address _poolImplementation) { - POOL_FACTORY = _poolFactory; - POOL_IMPLEMENTATION = _poolImplementation; + struct FactoryDescription { + address factory; + bytes32 initcodeHash; + } + address public immutable CL_FACTORY_ADDRESS; // SlipstreamOracle + address public immutable POOL_FACTORY_ADDRESS; + bytes32 public immutable POOL_FACTORY_INITCODE_HASH; + + constructor(IVelodramoV2Router _router, IVelodramoV2Registry _registry, FactoryDescription memory _poolFactory, address _skipFactory) { + ROUTER = _router; + REGISTRY = _registry; + POOL_FACTORY_ADDRESS = _poolFactory.factory; + POOL_FACTORY_INITCODE_HASH = _poolFactory.initcodeHash; + CL_FACTORY_ADDRESS = _skipFactory; } function getRate(IERC20 srcToken, IERC20 dstToken, IERC20 connector, uint256 thresholdFilter) external view override returns (uint256 rate, uint256 weight) { + address[] memory factories = REGISTRY.poolFactories(); if (connector == _NONE) { - (rate, weight) = _getWeightedRate(srcToken, dstToken, thresholdFilter); + (rate, weight) = _getWeightedRate(srcToken, dstToken, factories, thresholdFilter); } else { - (uint256 rateC0, uint256 weightC0) = _getWeightedRate(srcToken, connector, thresholdFilter); - (uint256 rateC1, uint256 weightC1) = _getWeightedRate(connector, dstToken, thresholdFilter); + (uint256 rateC0, uint256 weightC0) = _getWeightedRate(srcToken, connector, factories, thresholdFilter); + (uint256 rateC1, uint256 weightC1) = _getWeightedRate(connector, dstToken, factories, thresholdFilter); rate = rateC0 * rateC1 / 1e18; weight = Math.min(weightC0, weightC1); } } - function _getWeightedRate(IERC20 srcToken, IERC20 dstToken, uint256 thresholdFilter) internal view returns (uint256 rate, uint256 weight) { - OraclePrices.Data memory ratesAndWeights = OraclePrices.init(2); + function _getWeightedRate(IERC20 srcToken, IERC20 dstToken, address[] memory factories, uint256 thresholdFilter) internal view returns (uint256 rate, uint256 weight) { + uint256 factoriesLength = factories.length; + OraclePrices.Data memory ratesAndWeights = OraclePrices.init(2 * factoriesLength); uint256 b0; uint256 b1; - (b0, b1) = _getReserves(srcToken, dstToken, true); - if (b0 > 0) { - ratesAndWeights.append(OraclePrices.OraclePrice(Math.mulDiv(b1, 1e18, b0), (b0 * b1).sqrt())); - } - (b0, b1) = _getReserves(srcToken, dstToken, false); - if (b0 > 0) { - ratesAndWeights.append(OraclePrices.OraclePrice(Math.mulDiv(b1, 1e18, b0), (b0 * b1).sqrt())); + for (uint256 i = 0; i < factoriesLength; i++) { + if (factories[i] == CL_FACTORY_ADDRESS) { + continue; + } + (b0, b1) = _getReserves(srcToken, dstToken, true, factories[i]); + if (b0 > 0) { + ratesAndWeights.append(OraclePrices.OraclePrice(Math.mulDiv(b1, 1e18, b0), (b0 * b1).sqrt())); + } + (b0, b1) = _getReserves(srcToken, dstToken, false, factories[i]); + if (b0 > 0) { + ratesAndWeights.append(OraclePrices.OraclePrice(Math.mulDiv(b1, 1e18, b0), (b0 * b1).sqrt())); + } } (rate, weight) = ratesAndWeights.getRateAndWeight(thresholdFilter); } - function _getReserves(IERC20 srcToken, IERC20 dstToken, bool stable) internal view returns (uint256 reserveSrc, uint256 reserveDst) { - (IERC20 token0, IERC20 token1) = srcToken < dstToken ? (srcToken, dstToken) : (dstToken, srcToken); - address pool = Clones.predictDeterministicAddress({ - implementation: POOL_IMPLEMENTATION, - salt: keccak256(abi.encodePacked(address(token0), address(token1), stable)), - deployer: POOL_FACTORY - }); - try IVelodromeV2Pool(pool).getReserves() returns (uint256 reserve0, uint256 reserve1, uint256) { - if (srcToken == token0) { - (reserveSrc, reserveDst) = (reserve0, reserve1); - } else { - (reserveSrc, reserveDst) = (reserve1, reserve0); + function _getReserves(IERC20 srcToken, IERC20 dstToken, bool stable, address factory) internal view returns (uint256 reserveSrc, uint256 reserveDst) { + if (factory == POOL_FACTORY_ADDRESS) { + (IERC20 token0, IERC20 token1) = srcToken < dstToken ? (srcToken, dstToken) : (dstToken, srcToken); + address pool = _getPool(token0, token1, stable, POOL_FACTORY_ADDRESS, POOL_FACTORY_INITCODE_HASH); + + (bool success, bytes memory data) = pool.staticcall(abi.encodeWithSelector(IVelodromeV2Pool.getReserves.selector, pool)); + if (success && data.length >= 64) { + (reserveSrc, reserveDst) = abi.decode(data, (uint256, uint256)); + if (srcToken == token1) { + (reserveSrc, reserveDst) = (reserveDst, reserveSrc); + } } - } catch {} // solhint-disable-line no-empty-blocks + } else { + try ROUTER.getReserves(address(srcToken), address(dstToken), stable, factory) returns (uint256 reserveSrc_, uint256 reserveDst_) { + (reserveSrc, reserveDst) = (reserveSrc_, reserveDst_); + } catch {} // solhint-disable-line no-empty-blocks + } + } + + function _getPool(IERC20 token0, IERC20 token1, bool stable, address factory, bytes32 initcodeHash) private pure returns (address) { + return address(uint160(uint256( + keccak256( + abi.encodePacked( + hex'ff', + factory, + keccak256(abi.encodePacked(token0, token1, stable)), + initcodeHash + ) + ) + ))); } } diff --git a/test/helpers.js b/test/helpers.js index 6bf27176..2941b8e9 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -152,8 +152,17 @@ const deployParams = { initcodeHash: '0x0ccd005ee58d5fb11632ef5c2e0866256b240965c62c8e990c0f84a97f311879', }, VelodromeV2: { // optimistic network - poolFactory: '0xF1046053aa5682b4F9a81b5481394DA16BE5FF5a', - poolImplementation: '0x95885Af5492195F0754bE71AD1545Fe81364E531', + router: '0xa062aE8A9c5e11aaA026fc2670B0D65cCc8B2858', + registry: '0xF4c67CdEAaB8360370F41514d06e32CcD8aA1d7B', + poolFactory: { + factory: '0xF1046053aa5682b4F9a81b5481394DA16BE5FF5a', + initcodeHash: '0xc0629f1c7daa09624e54d4f711ba99922a844907cce02997176399e4cc7e8fcf', + }, + }, + Slipstream: { // optimistic network + factory: '0x548118C7E0B865C2CfA94D15EC86B666468ac758', + initcodeHash: '0x3e17c3f6d9f39d14b65192404b8d70a2f921655d3f7f5e7481ab3fcf0756e8ea', + tickSpacings: [1, 50, 100, 200, 2_000], }, }; diff --git a/test/oracles/VelodromeV2Oracle.js b/test/oracles/VelodromeV2Oracle.js index 54496527..2403e70a 100644 --- a/test/oracles/VelodromeV2Oracle.js +++ b/test/oracles/VelodromeV2Oracle.js @@ -1,10 +1,10 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { network } = require('hardhat'); +const { network, ethers } = require('hardhat'); const { deployContract } = require('@1inch/solidity-utils'); const { resetHardhatNetworkFork } = require('@1inch/solidity-utils/hardhat-setup'); const { tokens, - deployParams: { VelodromeV2, UniswapV3 }, + deployParams: { Slipstream, VelodromeV2, UniswapV3 }, defaultValues: { thresholdFilter }, testRate, measureGas, @@ -20,7 +20,7 @@ describe('VelodromeV2Oracle', function () { }); async function initContracts () { - const velodromeV2Oracle = await deployContract('VelodromeV2Oracle', [VelodromeV2.poolFactory, VelodromeV2.poolImplementation]); + const velodromeV2Oracle = await deployContract('VelodromeV2Oracle', [VelodromeV2.router, VelodromeV2.registry, VelodromeV2.poolFactory, Slipstream.factory]); const uniswapV3Oracle = await deployContract('UniswapV3LikeOracle', [UniswapV3.factory, UniswapV3.initcodeHash, UniswapV3.fees]); return { velodromeV2Oracle, uniswapV3Oracle }; } @@ -41,6 +41,58 @@ describe('VelodromeV2Oracle', function () { }); describe('Measure gas', function () { + async function initContractsForOffchainOracle () { + const { velodromeV2Oracle } = await loadFixture(initContracts); + + const [wallet] = await ethers.getSigners(); + const velodromeV2OraclePrevVersion = await ethers.getContractAt('VelodromeV2Oracle', '0x41674e58F339fE1caB03CA8DF095D46B998E6125'); + const multiWrapper = await deployContract('MultiWrapper', [[], wallet.address]); + const offchainOracle = await deployContract('OffchainOracle', [ + multiWrapper.target, + [velodromeV2OraclePrevVersion], + ['0'], + [tokens.NONE], + tokens.optimistic.WETH, + wallet.address, + ]); + const offchainOracleNew = await deployContract('OffchainOracle', [ + multiWrapper.target, + [velodromeV2Oracle], + ['0'], + [tokens.NONE], + tokens.optimistic.WETH, + wallet.address, + ]); + return { offchainOracle, offchainOracleNew }; + } + + it('OffchainOracle with 1 connector', async function () { + const { offchainOracle, offchainOracleNew } = await loadFixture(initContractsForOffchainOracle); + await measureGas( + await offchainOracle.getFunction('getRateWithThreshold').send(tokens.optimistic.WETH, tokens.optimistic.USDC, tokens.NONE, thresholdFilter), + 'offchainOracle (1 connector) WETH -> USDC', + ); + await measureGas( + await offchainOracleNew.getFunction('getRateWithThreshold').send(tokens.optimistic.WETH, tokens.optimistic.USDC, tokens.NONE, thresholdFilter), + 'offchainOracleNew (1 connector) WETH -> USDC', + ); + }); + + it('OffchainOracle with 2 connectors', async function () { + const { offchainOracle, offchainOracleNew } = await loadFixture(initContractsForOffchainOracle); + await offchainOracle.addConnector(tokens.optimistic.OP); + await offchainOracleNew.addConnector(tokens.optimistic.OP); + + await measureGas( + await offchainOracle.getFunction('getRateWithThreshold').send(tokens.optimistic.WETH, tokens.optimistic.USDC, tokens.NONE, thresholdFilter), + 'offchainOracle (2 connectors) WETH -> USDC', + ); + await measureGas( + await offchainOracleNew.getFunction('getRateWithThreshold').send(tokens.optimistic.WETH, tokens.optimistic.USDC, tokens.NONE, thresholdFilter), + 'offchainOracleNew (2 connector) WETH -> USDC', + ); + }); + it('WETH -> USDC', async function () { const { velodromeV2Oracle, uniswapV3Oracle } = await loadFixture(initContracts); await measureGas( From c2fe356e216e7bc0107988059e1cd53c7a81ac11 Mon Sep 17 00:00:00 2001 From: Denis Date: Wed, 17 Apr 2024 14:34:29 +0100 Subject: [PATCH 3/4] Fix linter --- test/helpers.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/helpers.js b/test/helpers.js index 0eb466bb..6f45d5d3 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -171,11 +171,6 @@ const deployParams = { initcodeHash: '0x3e17c3f6d9f39d14b65192404b8d70a2f921655d3f7f5e7481ab3fcf0756e8ea', tickSpacings: [1, 50, 100, 200, 2_000], }, - Slipstream: { // optimistic network - factory: '0x548118C7E0B865C2CfA94D15EC86B666468ac758', - initcodeHash: '0x3e17c3f6d9f39d14b65192404b8d70a2f921655d3f7f5e7481ab3fcf0756e8ea', - tickSpacings: [1, 50, 100, 200, 2_000], - }, }; async function measureGas (tx, comment) { From 4b4df7b71fc7deb8b49e159ae4c2c8f88eb00983 Mon Sep 17 00:00:00 2001 From: Denis Date: Wed, 17 Apr 2024 14:47:28 +0100 Subject: [PATCH 4/4] Patch comment --- contracts/oracles/VelodromeV2Oracle.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/oracles/VelodromeV2Oracle.sol b/contracts/oracles/VelodromeV2Oracle.sol index 4135da5c..917a8b30 100644 --- a/contracts/oracles/VelodromeV2Oracle.sol +++ b/contracts/oracles/VelodromeV2Oracle.sol @@ -33,7 +33,7 @@ contract VelodromeV2Oracle is IOracle { address factory; bytes32 initcodeHash; } - address public immutable CL_FACTORY_ADDRESS; // SlipstreamOracle + address public immutable CL_FACTORY_ADDRESS; // Slipstream used as separate Oracle address public immutable POOL_FACTORY_ADDRESS; bytes32 public immutable POOL_FACTORY_INITCODE_HASH;