Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions contracts/helpers/ConnectorManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand Down
23 changes: 19 additions & 4 deletions contracts/oracles/CurveOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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);
Expand Down
24 changes: 20 additions & 4 deletions contracts/oracles/CurveOracleCRP.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ module.exports = {
deploy: 'deploy/commands',
},
mocha: {
timeout: 180000,
timeout: 240000,
},
tracer: {
enableAllOpcodes: true,
Expand Down
4 changes: 3 additions & 1 deletion test/OffchainOracle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand All @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions test/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
35 changes: 32 additions & 3 deletions test/oracles/CurveOracle.js
Original file line number Diff line number Diff line change
@@ -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 },
Expand All @@ -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 };
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down