diff --git a/contracts/oracles/VelodromeV2Oracle.sol b/contracts/oracles/VelodromeV2Oracle.sol index 73921ad6..917a8b30 100644 --- a/contracts/oracles/VelodromeV2Oracle.sol +++ b/contracts/oracles/VelodromeV2Oracle.sol @@ -16,6 +16,9 @@ interface IVelodramoV2Registry { function poolFactories() external view returns (address[] memory); } +interface IVelodromeV2Pool { + function getReserves() external view returns (uint256 _reserve0, uint256 _reserve1, uint256 _timestampLast); +} contract VelodromeV2Oracle is IOracle { using OraclePrices for OraclePrices.Data; @@ -26,9 +29,20 @@ contract VelodromeV2Oracle is IOracle { IVelodramoV2Router public immutable ROUTER; IVelodramoV2Registry public immutable REGISTRY; - constructor(IVelodramoV2Router _router, IVelodramoV2Registry _registry) { + struct FactoryDescription { + address factory; + bytes32 initcodeHash; + } + address public immutable CL_FACTORY_ADDRESS; // Slipstream used as separate Oracle + 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) { @@ -49,6 +63,9 @@ contract VelodromeV2Oracle is IOracle { uint256 b0; uint256 b1; 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())); @@ -62,8 +79,34 @@ contract VelodromeV2Oracle is IOracle { } 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_); - } catch {} // solhint-disable-line no-empty-blocks + 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); + } + } + } 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 8825c8b5..6f45d5d3 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -161,6 +161,10 @@ const deployParams = { VelodromeV2: { // optimistic network router: '0xa062aE8A9c5e11aaA026fc2670B0D65cCc8B2858', registry: '0xF4c67CdEAaB8360370F41514d06e32CcD8aA1d7B', + poolFactory: { + factory: '0xF1046053aa5682b4F9a81b5481394DA16BE5FF5a', + initcodeHash: '0xc0629f1c7daa09624e54d4f711ba99922a844907cce02997176399e4cc7e8fcf', + }, }, Slipstream: { // optimistic network factory: '0x548118C7E0B865C2CfA94D15EC86B666468ac758', diff --git a/test/oracles/VelodromeV2Oracle.js b/test/oracles/VelodromeV2Oracle.js index fa4c80dc..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.router, VelodromeV2.registry]); + 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(