Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SC-1127][SC-1210] Update CurveOracle | Curve Rate Provider logic #169

Merged
merged 5 commits into from
Aug 8, 2024
Merged
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
11 changes: 11 additions & 0 deletions contracts/interfaces/ICurveMetaregistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.23;

// solhint-disable func-name-mixedcase

interface ICurveMetaregistry {
function find_pools_for_coins(address srcToken, address dstToken) external view returns (address[] memory);
function get_coin_indices(address _pool, address _from, address _to) external view returns (int128, int128, bool);
function get_underlying_balances(address _pool) external view returns (uint256[8] memory);
}
22 changes: 22 additions & 0 deletions contracts/interfaces/ICurvePool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.23;

// solhint-disable func-name-mixedcase

interface ICurvePool {
function allowed_extra_profit() external view returns (uint256);
function get_rate_mul() external view returns (uint256);
}

interface IStableSwapMeta {
function get_dy_underlying(int128,int128,uint256) external view returns (uint256);
}

interface IStableSwap {
function get_dy(int128,int128,uint256) external view returns (uint256);
}

interface ICryptoSwap {
function get_dy(uint256,uint256,uint256) external view returns (uint256);
}
27 changes: 0 additions & 27 deletions contracts/interfaces/ICurveRegistry.sol

This file was deleted.

17 changes: 0 additions & 17 deletions contracts/interfaces/ICurveSwap.sol

This file was deleted.

207 changes: 68 additions & 139 deletions contracts/oracles/CurveOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,165 +6,94 @@ import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "../interfaces/IOracle.sol";
import "../interfaces/ICurveProvider.sol";
import "../interfaces/ICurveRegistry.sol";
import "../interfaces/ICurveSwap.sol";
import "../interfaces/ICurveMetaregistry.sol";
import "../interfaces/ICurvePool.sol";
import "../libraries/OraclePrices.sol";
import "../helpers/Blacklist.sol";

contract CurveOracle is IOracle, Blacklist {
contract CurveOracle is IOracle {
using OraclePrices for OraclePrices.Data;
using Math for uint256;

enum CurveRegistryType {
MAIN_REGISTRY,
METAPOOL_FACTORY,
CRYPTOSWAP_REGISTRY,
CRYPTOPOOL_FACTORY,
METAREGISTRY,
CRVUSD_PLAIN_POOLS,
CURVE_TRICRYPTO_FACTORY,
STABLESWAP_FACTORY,
L2_FACTORY,
CRYPTO_FACTORY
}
IERC20 private constant _NONE = IERC20(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF);
IERC20 private constant _ETH = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);
uint256 private constant _METAREGISTRY_ID = 7;

struct FunctionSelectorsInfo {
bytes4 balanceFunc;
bytes4 dyFuncInt128;
bytes4 dyFuncUint256;
ICurveMetaregistry public immutable CURVE_METAREGISTRY;
uint256 public immutable MAX_POOLS;

constructor(ICurveProvider curveProvider, uint256 maxPools) {
CURVE_METAREGISTRY = ICurveMetaregistry(curveProvider.get_address(_METAREGISTRY_ID));
MAX_POOLS = maxPools;
}

IERC20 private constant _NONE = IERC20(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF);
function _getPoolType(address pool) private view returns (uint8) {
// 0 for stableswap, 1 for cryptoswap, 2 for LLAMMA.

uint256 public immutable MAX_POOLS;
uint256 public immutable REGISTRIES_COUNT;
ICurveRegistry[11] public registries;
CurveRegistryType[11] public registryTypes;

constructor(
ICurveProvider _addressProvider,
uint256 _maxPools,
uint256[] memory _registryIds,
CurveRegistryType[] memory _registryTypes,
address[] memory initialBlacklist,
address owner
) Blacklist(initialBlacklist, owner) {
MAX_POOLS = _maxPools;
REGISTRIES_COUNT = _registryIds.length;
unchecked {
for (uint256 i = 0; i < REGISTRIES_COUNT; i++) {
registries[i] = ICurveRegistry(_addressProvider.get_address(_registryIds[i]));
registryTypes[i] = _registryTypes[i];
}
// check if cryptoswap
(bool success, bytes memory data) = pool.staticcall(abi.encodeWithSelector(ICurvePool.allowed_extra_profit.selector));
if (success && data.length >= 32) { // vyper could return redundant bytes
return 1;
}

// check if llamma
(success, data) = pool.staticcall(abi.encodeWithSelector(ICurvePool.get_rate_mul.selector));
if (success && data.length >= 32) { // vyper could return redundant bytes
return 2;
}

return 0;
}

function getRate(IERC20 srcToken, IERC20 dstToken, IERC20 connector, uint256 thresholdFilter) external view override returns (uint256 rate, uint256 weight) {
if(connector != _NONE) revert ConnectorShouldBeNone();

OraclePrices.Data memory ratesAndWeights = OraclePrices.init(MAX_POOLS);
FunctionSelectorsInfo memory info;
uint256 index = 0;
for (uint256 i = 0; i < REGISTRIES_COUNT && index < MAX_POOLS; i++) {
uint256 registryIndex = 0;
address pool = registries[i].find_pool_for_coins(address(srcToken), address(dstToken), registryIndex);
while (pool != address(0) && index < MAX_POOLS) {
if (blacklisted[pool]) {
pool = registries[i].find_pool_for_coins(address(srcToken), address(dstToken), ++registryIndex);
continue;
}
index++;
// call `get_coin_indices` and set (srcTokenIndex, dstTokenIndex, isUnderlying) variables
bool isUnderlying;
int128 srcTokenIndex;
int128 dstTokenIndex;
(bool success, bytes memory data) = address(registries[i]).staticcall(abi.encodeWithSelector(ICurveRegistry.get_coin_indices.selector, pool, address(srcToken), address(dstToken)));
if (success && data.length >= 64) {
if (
registryTypes[i] == CurveRegistryType.CRYPTOSWAP_REGISTRY ||
registryTypes[i] == CurveRegistryType.CRYPTOPOOL_FACTORY ||
registryTypes[i] == CurveRegistryType.CURVE_TRICRYPTO_FACTORY
) {
(srcTokenIndex, dstTokenIndex) = abi.decode(data, (int128, int128));
} else {
// registryTypes[i] == CurveRegistryType.MAIN_REGISTRY ||
// registryTypes[i] == CurveRegistryType.METAPOOL_FACTORY ||
// registryTypes[i] == CurveRegistryType.METAREGISTRY ||
// registryTypes[i] == CurveRegistryType.CRVUSD_PLAIN_POOLS ||
// registryTypes[i] == CurveRegistryType.STABLESWAP_FACTORY ||
// registryTypes[i] == CurveRegistryType.L2_FACTORY ||
// registryTypes[i] == CurveRegistryType.CRYPTO_FACTORY
(srcTokenIndex, dstTokenIndex, isUnderlying) = abi.decode(data, (int128, int128, bool));
}
} else {
pool = registries[i].find_pool_for_coins(address(srcToken), address(dstToken), ++registryIndex);
continue;
}
address[] memory pools = CURVE_METAREGISTRY.find_pools_for_coins(address(srcToken), address(dstToken));
if (pools.length == 0) {
return (0, 0);
}

if (!isUnderlying) {
info = FunctionSelectorsInfo({
balanceFunc: ICurveRegistry.get_balances.selector,
dyFuncInt128: ICurveSwapInt128.get_dy.selector,
dyFuncUint256: ICurveSwapUint256.get_dy.selector
});
} else {
info = FunctionSelectorsInfo({
balanceFunc: ICurveRegistry.get_underlying_balances.selector,
dyFuncInt128: ICurveSwapInt128.get_dy_underlying.selector,
dyFuncUint256: ICurveSwapUint256.get_dy_underlying.selector
});
}
uint256 amountIn;
if (srcToken == _ETH) {
amountIn = 10**18;
} else {
amountIn = 10**IERC20Metadata(address(srcToken)).decimals();
}

// call `balanceFunc` (`get_balances` or `get_underlying_balances`) and decode results
uint256[] memory balances;
(success, data) = address(registries[i]).staticcall(abi.encodeWithSelector(info.balanceFunc, pool));
if (success && data.length >= 64) {
// registryTypes[i] == CurveRegistryType.MAIN_REGISTRY ||
// registryTypes[i] == CurveRegistryType.CRYPTOSWAP_REGISTRY ||
// registryTypes[i] == CurveRegistryType.METAREGISTRY
uint256 length = 8;
if (!isUnderlying) {
if (
registryTypes[i] == CurveRegistryType.METAPOOL_FACTORY ||
registryTypes[i] == CurveRegistryType.CRVUSD_PLAIN_POOLS ||
registryTypes[i] == CurveRegistryType.STABLESWAP_FACTORY ||
registryTypes[i] == CurveRegistryType.L2_FACTORY ||
registryTypes[i] == CurveRegistryType.CRYPTO_FACTORY
) {
length = 4;
} else if (registryTypes[i] == CurveRegistryType.CURVE_TRICRYPTO_FACTORY) {
length = 3;
} else if (registryTypes[i] == CurveRegistryType.CRYPTOPOOL_FACTORY) {
length = 2;
}
}

assembly ("memory-safe") { // solhint-disable-line no-inline-assembly
balances := data
mstore(balances, length)
}
} else {
pool = registries[i].find_pool_for_coins(address(srcToken), address(dstToken), ++registryIndex);
continue;
}
OraclePrices.Data memory ratesAndWeights = OraclePrices.init(pools.length);
for (uint256 k = 0; k < pools.length && ratesAndWeights.size < MAX_POOLS; k++) {
// get coin indices
int128 i;
int128 j;
bool isUnderlying = false;
(i, j, isUnderlying) = CURVE_METAREGISTRY.get_coin_indices(pools[k], address(srcToken), address(dstToken));

// get balances
uint256[8] memory balances = CURVE_METAREGISTRY.get_underlying_balances(pools[k]);
// skip if pool is too small
if (balances[uint128(i)] <= amountIn || balances[uint128(j)] == 0) {
continue;
}

uint256 w = (balances[uint128(srcTokenIndex)] * balances[uint128(dstTokenIndex)]).sqrt();
uint256 b0 = balances[uint128(srcTokenIndex)] / 10000;
uint256 b1 = balances[uint128(dstTokenIndex)] / 10000;

if (b0 != 0 && b1 != 0) {
(success, data) = pool.staticcall(abi.encodeWithSelector(info.dyFuncInt128, srcTokenIndex, dstTokenIndex, b0));
if (!success || data.length < 32) {
(success, data) = pool.staticcall(abi.encodeWithSelector(info.dyFuncUint256, uint128(srcTokenIndex), uint128(dstTokenIndex), b0));
}
if (success && data.length >= 32) { // vyper could return redundant bytes
b1 = abi.decode(data, (uint256));
ratesAndWeights.append(OraclePrices.OraclePrice(Math.mulDiv(b1, 1e18, b0), w));
}
// choose the right abi:
uint8 poolType = _getPoolType(pools[k]);
bytes4 selector;
if (poolType == 0 && isUnderlying) {
selector = IStableSwapMeta.get_dy_underlying.selector;
} else if (poolType == 0 && !isUnderlying) {
selector = IStableSwap.get_dy.selector;
} else {
selector = ICryptoSwap.get_dy.selector;
}
(bool success, bytes memory data) = pools[k].staticcall(abi.encodeWithSelector(selector, uint128(i), uint128(j), amountIn));
if (success && data.length >= 32) { // vyper could return redundant bytes
uint256 amountOut = abi.decode(data, (uint256));
if (amountOut > 0) {
rate = amountOut * 1e18 / amountIn;
weight = (balances[uint128(i)] * balances[uint128(j)]).sqrt();
ratesAndWeights.append(OraclePrices.OraclePrice(rate, weight));
}
pool = registries[i].find_pool_for_coins(address(srcToken), address(dstToken), ++registryIndex);
}
}
(rate, weight) = ratesAndWeights.getRateAndWeight(thresholdFilter);
return ratesAndWeights.getRateAndWeight(thresholdFilter);
}
}
33 changes: 33 additions & 0 deletions test/OffchainOracle.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const hre = require('hardhat');
const fs = require('fs');
const { ethers } = hre;
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { expect, ether, assertRoughlyEqualValues, deployContract } = require('@1inch/solidity-utils');
Expand Down Expand Up @@ -262,5 +263,37 @@ describe('OffchainOracle', function () {
const rateReverse = await offchainOracle.getRateWithCustomConnectors(tokens.wstETH, tokens.WETH, true, [], thresholdFilter);
assertRoughlyEqualValues(rateForward * rateReverse / BigInt(1e18), BigInt(1e18), 5e-18);
});

it.skip('measure gas of current implementation', async function () {
// NOTE: This test is skipped because it is too slow. Change hardhat config `mocha > timeout > 360000` to run it.
const gasEstimator = await deployContract('GasEstimator');

const offchainOracleDeployment = JSON.parse(fs.readFileSync('deployments/mainnet/OffchainOracle.json', 'utf8'));
const offchainOracle = await ethers.getContractAt('OffchainOracle', offchainOracleDeployment.address);

// Uncomment and edit it to test with replaced oracles
// const [,account] = await ethers.getSigners();
// const ownerAddress = offchainOracleDeployment.args[5];
// const curveOracle = await deployContract('CurveOracle', [Curve.provider, Curve.maxPools]);
// 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'));
// await offchainOracle.connect(owner).removeOracle(curveOracleDeployment.address, '0');
// await offchainOracle.connect(owner).addOracle(curveOracle, '0');

const getRateToEthResult = await gasEstimator.gasCost(
await offchainOracle.getAddress(),
offchainOracle.interface.encodeFunctionData('getRateToEthWithThreshold', [tokens.DAI, true, thresholdFilter]),
);
console.log(`OffchainOracle getRateToEthWithThreshold(DAI,true,${thresholdFilter}): ${getRateToEthResult.gasUsed}`);
expect(getRateToEthResult.success).to.eq(true);

const getRateResult = await gasEstimator.gasCost(
await offchainOracle.getAddress(),
offchainOracle.interface.encodeFunctionData('getRateWithThreshold', [tokens.USDC, tokens.USDe, true, thresholdFilter]),
);
console.log(`OffchainOracle getRateWithThreshold(USDC,USDe,true,${thresholdFilter}): ${getRateResult.gasUsed}`);
expect(getRateResult.success).to.eq(true);
});
});
});
25 changes: 1 addition & 24 deletions test/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,6 @@ const contracts = {
chaiPot: '0x197E90f9FAD81970bA7976f33CbD77088E5D7cf7',
};

const CurveRegistryType = {
MAIN_REGISTRY: 0,
METAPOOL_FACTORY: 1,
CRYPTOSWAP_REGISTRY: 2,
CRYPTOPOOL_FACTORY: 3,
METAREGISTRY: 4,
CRVUSD_PLAIN_POOLS: 5,
CURVE_TRICRYPTO_FACTORY: 6,
STABLESWAP_FACTORY: 7,
L2_FACTORY: 8,
CRYPTO_FACTORY: 9,
};

const deployParams = {
AaveWrapperV2: {
lendingPool: '0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9',
Expand Down Expand Up @@ -129,18 +116,8 @@ const deployParams = {
initcodeHash: '0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f',
},
Curve: {
provider: '0x0000000022D53366457F9d5E68Ec105046FC4383',
provider: '0x5ffe7FB82894076ECB99A30D6A32e969e6e35E98',
maxPools: 100,
registryIds: [0, 3, 5, 6, 7, 8, 11],
registryTypes: [
CurveRegistryType.MAIN_REGISTRY,
CurveRegistryType.METAPOOL_FACTORY,
CurveRegistryType.CRYPTOSWAP_REGISTRY,
CurveRegistryType.CRYPTOPOOL_FACTORY,
CurveRegistryType.METAREGISTRY,
CurveRegistryType.CRVUSD_PLAIN_POOLS,
CurveRegistryType.CURVE_TRICRYPTO_FACTORY,
],
},
Dodo: {
dodoZoo: '0x3A97247DF274a17C59A3bd12735ea3FcDFb49950',
Expand Down
Loading
Loading