diff --git a/contracts/helpers/ConnectorManager.sol b/contracts/helpers/ConnectorManager.sol index cf4dc26..95e2a1c 100644 --- a/contracts/helpers/ConnectorManager.sol +++ b/contracts/helpers/ConnectorManager.sol @@ -3,23 +3,27 @@ pragma solidity 0.8.23; import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /** * @title ConnectorManager * @notice Contract is used to support only specific connectors in the oracle. */ contract ConnectorManager is Ownable { - event ConnectorUpdated(address connector, bool isSupported); + event ConnectorUpdated(IERC20 connector, bool isSupported); - mapping(address => bool) public connectorSupported; + IERC20 internal constant _NONE = IERC20(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF); - constructor(address[] memory connectors, address owner) Ownable(owner) { + mapping(IERC20 => bool) public connectorSupported; + + constructor(IERC20[] memory connectors, address owner) Ownable(owner) { + connectorSupported[_NONE] = true; for (uint256 i = 0; i < connectors.length; i++) { connectorSupported[connectors[i]] = true; } } - function toggleConnectorSupport(address connector) external onlyOwner { + function toggleConnectorSupport(IERC20 connector) external onlyOwner { connectorSupported[connector] = !connectorSupported[connector]; emit ConnectorUpdated(connector, connectorSupported[connector]); } diff --git a/contracts/oracles/CurveOracle.sol b/contracts/oracles/CurveOracle.sol index 78d6d68..1b69b6a 100644 --- a/contracts/oracles/CurveOracle.sol +++ b/contracts/oracles/CurveOracle.sol @@ -9,19 +9,23 @@ import "../interfaces/ICurveProvider.sol"; import "../interfaces/ICurveMetaregistry.sol"; import "../interfaces/ICurvePool.sol"; import "../libraries/OraclePrices.sol"; +import "../helpers/ConnectorManager.sol"; -contract CurveOracle is IOracle { +contract CurveOracle is IOracle, ConnectorManager { using OraclePrices for OraclePrices.Data; using Math for uint256; - IERC20 private constant _NONE = IERC20(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF); + error ConnectorNotSupported(); + IERC20 private constant _ETH = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); uint256 private constant _METAREGISTRY_ID = 7; ICurveMetaregistry public immutable CURVE_METAREGISTRY; uint256 public immutable MAX_POOLS; - constructor(ICurveProvider curveProvider, uint256 maxPools) { + constructor(ICurveProvider curveProvider, uint256 maxPools, IERC20[] memory connectors, address owner) + ConnectorManager(connectors, owner) + { CURVE_METAREGISTRY = ICurveMetaregistry(curveProvider.get_address(_METAREGISTRY_ID)); MAX_POOLS = maxPools; } @@ -45,8 +49,19 @@ contract CurveOracle is IOracle { } function getRate(IERC20 srcToken, IERC20 dstToken, IERC20 connector, uint256 thresholdFilter) external view override returns (uint256 rate, uint256 weight) { - if(connector != _NONE) revert ConnectorShouldBeNone(); + if (!connectorSupported[connector]) revert ConnectorNotSupported(); + + if (connector == _NONE) { + (rate, weight) = _getRate(srcToken, dstToken, thresholdFilter); + } else { + (uint256 rate1, uint256 weight1) = _getRate(srcToken, connector, thresholdFilter); + (uint256 rate2, uint256 weight2) = _getRate(connector, dstToken, thresholdFilter); + rate = Math.mulDiv(rate1, rate2, 1e18); + weight = Math.min(weight1, weight2).sqrt(); + } + } + function _getRate(IERC20 srcToken, IERC20 dstToken, uint256 thresholdFilter) internal view returns (uint256 rate, uint256 weight) { address[] memory pools = CURVE_METAREGISTRY.find_pools_for_coins(address(srcToken), address(dstToken)); if (pools.length == 0) { return (0, 0); diff --git a/contracts/oracles/CurveOracleCRP.sol b/contracts/oracles/CurveOracleCRP.sol index 227e64d..4dfeeeb 100644 --- a/contracts/oracles/CurveOracleCRP.sol +++ b/contracts/oracles/CurveOracleCRP.sol @@ -7,25 +7,41 @@ import "@openzeppelin/contracts/utils/math/Math.sol"; import "../interfaces/IOracle.sol"; import "../interfaces/ICurveProvider.sol"; import "../libraries/OraclePrices.sol"; +import "../helpers/ConnectorManager.sol"; -contract CurveOracleCRP is IOracle { +contract CurveOracleCRP is IOracle, ConnectorManager { using OraclePrices for OraclePrices.Data; using Math for uint256; - IERC20 private constant _NONE = IERC20(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF); + error ConnectorNotSupported(); + IERC20 private constant _ETH = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); uint256 private constant _RATE_PROVIDER_ID = 18; ICurveRateProvider public immutable CURVE_RATE_PROVIDER; uint256 public immutable MAX_POOLS; - constructor(ICurveProvider curveProvider, uint256 maxPools) { + constructor(ICurveProvider curveProvider, uint256 maxPools, IERC20[] memory connectors, address owner) + ConnectorManager(connectors, owner) + { CURVE_RATE_PROVIDER = ICurveRateProvider(curveProvider.get_address(_RATE_PROVIDER_ID)); MAX_POOLS = maxPools; } function getRate(IERC20 srcToken, IERC20 dstToken, IERC20 connector, uint256 thresholdFilter) external view override returns (uint256 rate, uint256 weight) { - if(connector != _NONE) revert ConnectorShouldBeNone(); + if (!connectorSupported[connector]) revert ConnectorNotSupported(); + + if (connector == _NONE) { + (rate, weight) = _getRate(srcToken, dstToken, thresholdFilter); + } else { + (uint256 rate1, uint256 weight1) = _getRate(srcToken, connector, thresholdFilter); + (uint256 rate2, uint256 weight2) = _getRate(connector, dstToken, thresholdFilter); + rate = Math.mulDiv(rate1, rate2, 1e18); + weight = Math.min(weight1, weight2).sqrt(); + } + } + + function _getRate(IERC20 srcToken, IERC20 dstToken, uint256 thresholdFilter) private view returns (uint256 rate, uint256 weight) { uint256 amountIn; if (srcToken == _ETH) { amountIn = 10**18; diff --git a/hardhat.config.js b/hardhat.config.js index f7bec36..3e8f522 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -41,7 +41,7 @@ module.exports = { deploy: 'deploy/commands', }, mocha: { - timeout: 180000, + timeout: 240000, }, tracer: { enableAllOpcodes: true, diff --git a/test/OffchainOracle.js b/test/OffchainOracle.js index 318cb96..c88c81e 100644 --- a/test/OffchainOracle.js +++ b/test/OffchainOracle.js @@ -272,9 +272,10 @@ describe('OffchainOracle', function () { const offchainOracle = await ethers.getContractAt('OffchainOracle', offchainOracleDeployment.address); // Uncomment and edit it to test with replaced oracles + // const { deployParams: { Curve } } = require('./helpers.js'); // const [,account] = await ethers.getSigners(); // const ownerAddress = offchainOracleDeployment.args[5]; - // const curveOracle = await deployContract('CurveOracle', [Curve.provider, Curve.maxPools]); + // const curveOracle = await deployContract('CurveOracle', [Curve.provider, Curve.maxPools, [tokens.USDC], account.address]); // await account.sendTransaction({ to: ownerAddress, value: ether('100') }); // const owner = await ethers.getImpersonatedSigner(ownerAddress); // const curveOracleDeployment = JSON.parse(fs.readFileSync(`deployments/mainnet/CurveOracle.json`, 'utf8')); @@ -284,6 +285,7 @@ describe('OffchainOracle', function () { const getRateToEthResult = await gasEstimator.gasCost( await offchainOracle.getAddress(), offchainOracle.interface.encodeFunctionData('getRateToEthWithThreshold', [tokens.DAI, true, thresholdFilter]), + // { gasLimit: 300e6 } ); console.log(`OffchainOracle getRateToEthWithThreshold(DAI,true,${thresholdFilter}): ${getRateToEthResult.gasUsed}`); expect(getRateToEthResult.success).to.eq(true); diff --git a/test/helpers.js b/test/helpers.js index 0565d8f..d5c1280 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -16,6 +16,9 @@ const tokens = { '1INCH': '0x111111111117dC0aa78b770fA6A738034120C302', USDT: '0xdAC17F958D2ee523a2206206994597C13D831ec7', LINK: '0x514910771AF9Ca656af840dff83E8264EcF986CA', + // The tokens listed above are used as connectors in `expensiveOffchainOracle` within OffchainOracle tests + USDM: '0x59D9356E565Ab3A36dD77763Fc0d87fEaf85508C', + EURS: '0xdB25f211AB05b1c97D595516F45794528a807ad8', YFI: '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e', BZRX: '0x56d811088235F11C8920698a204A5010a788f4b3', MKR: '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', diff --git a/test/oracles/CurveOracle.js b/test/oracles/CurveOracle.js index 4699fb4..a339ce7 100644 --- a/test/oracles/CurveOracle.js +++ b/test/oracles/CurveOracle.js @@ -1,6 +1,6 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { ethers } = require('hardhat'); -const { expect, deployContract } = require('@1inch/solidity-utils'); +const { expect, deployContract, getEthPrice } = require('@1inch/solidity-utils'); const { tokens, deployParams: { AaveWrapperV2, Curve, Uniswap, UniswapV2, UniswapV3 }, @@ -18,13 +18,15 @@ describe('CurveOracle', function () { async function deployCurveOracle () { const { uniswapV3Oracle } = await deployUniswapV3(); - const curveOracle = await deployContract('CurveOracle', [Curve.provider, Curve.maxPools]); + const [owner] = await ethers.getSigners(); + const curveOracle = await deployContract('CurveOracle', [Curve.provider, Curve.maxPools, [tokens.USDC], owner.address]); return { curveOracle, uniswapV3Oracle }; } async function deployCurveOracleCRP () { const { uniswapV3Oracle } = await deployUniswapV3(); - const curveOracle = await deployContract('CurveOracleCRP', [Curve.provider, Curve.maxPools]); + const [owner] = await ethers.getSigners(); + const curveOracle = await deployContract('CurveOracleCRP', [Curve.provider, Curve.maxPools, [tokens.USDC], owner.address]); return { curveOracle, uniswapV3Oracle }; } @@ -113,6 +115,28 @@ describe('CurveOracle', function () { const rate = await curveOracle.getRate(tokens.BEAN, tokens['3CRV'], tokens.NONE, thresholdFilter); expect(rate.rate).to.gt('0'); }); + + it('should revert for unsupported connector', async function () { + const { curveOracle } = await loadFixture(fixture); + expect(await curveOracle.connectorSupported(tokens.WBTC)).to.be.false; + await expect(curveOracle.getRate(tokens.USDT, tokens.USDC, tokens.WBTC, thresholdFilter)) + .to.be.revertedWithCustomError(curveOracle, 'ConnectorNotSupported()'); + }); + + it('EURS -> USDC -> USDT', async function () { + // This test uses `getEthPrice` method because EURS liquidity exists only in Curve + const { curveOracle } = await loadFixture(fixture); + const eursDecimals = 2n; + const usdtDecimals = 6n; + const rate = await getEthPrice('EURS') * 10n ** usdtDecimals / 10n ** eursDecimals; + await testRate(tokens.EURS, tokens.USDT, tokens.USDC, curveOracle, { getRate: async () => ({ rate }) }); + }); + + it('USDM -> USDC -> WETH', async function () { + const { curveOracle } = await loadFixture(fixture); + const rate = await curveOracle.getRate(tokens.USDM, tokens.WETH, tokens.USDC, thresholdFilter); + expect(rate.rate).to.gt('0'); + }); }; function shouldShowMeasureGas (fixture) { @@ -133,6 +157,11 @@ describe('CurveOracle', function () { await measureGas(await curveOracle.getFunction('getRate').send(tokens.WBTC, tokens.WETH, tokens.NONE, thresholdFilter), 'CurveOracle wbtc -> weth'); await measureGas(await uniswapV3Oracle.getFunction('getRate').send(tokens.WBTC, tokens.WETH, tokens.NONE, thresholdFilter), 'UniswapV3Oracle wbtc -> weth'); }); + + it('EURS -> USDC -> WETH', async function () { + const { curveOracle } = await loadFixture(fixture); + await measureGas(await curveOracle.getFunction('getRate').send(tokens.EURS, tokens.WETH, tokens.USDC, thresholdFilter), 'CurveOracle eurs -> usdc -> weth'); + }); }; function shouldNotRuintRate (fixture) {