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-1037] VelodromeV2 #109

Merged
merged 11 commits into from
Jan 31, 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
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- run: yarn test:ci
env:
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
RPC_AUTH_HEADER: ${{ secrets.RPC_AUTH_HEADER }}
OPTIMISTIC_RPC_URL: ${{ secrets.OPTIMISTIC_RPC_URL }}

coverage:
runs-on: ubuntu-latest
Expand All @@ -31,7 +31,7 @@ jobs:
- run: yarn coverage
env:
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
RPC_AUTH_HEADER: ${{ secrets.RPC_AUTH_HEADER }}
OPTIMISTIC_RPC_URL: ${{ secrets.OPTIMISTIC_RPC_URL }}
- uses: codecov/codecov-action@v3

single-price-example:
Expand Down
69 changes: 69 additions & 0 deletions contracts/oracles/VelodromeV2Oracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: MIT
// solhint-disable one-contract-per-file

pragma solidity 0.8.23;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../interfaces/IOracle.sol";
import "@openzeppelin/contracts/utils/math/Math.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);
}


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;

constructor(IVelodramoV2Router _router, IVelodramoV2Registry _registry) {
ROUTER = _router;
REGISTRY = _registry;
}

function getRate(IERC20 srcToken, IERC20 dstToken, IERC20 connector, uint256 thresholdFilter) external view override returns (uint256 rate, uint256 weight) {
zZoMROT marked this conversation as resolved.
Show resolved Hide resolved
address[] memory factories = REGISTRY.poolFactories();
if (connector == _NONE) {
(rate, weight) = _getWeightedRate(srcToken, dstToken, factories, thresholdFilter);
} else {
(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, 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;
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()));
}
}
(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_);
} catch {} // solhint-disable-line no-empty-blocks
}
}
1 change: 0 additions & 1 deletion hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ require('hardhat-gas-reporter');
require('hardhat-tracer');
require('solidity-coverage');

require('dotenv').config();
const { Networks, getNetwork } = require('@1inch/solidity-utils/hardhat-setup');

const { networks, etherscan } = (new Networks(true, 'mainnet', true)).registerAll();
Expand Down
20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,32 @@
},
"devDependencies": {
"@1inch/solidity-utils": "3.7.1",
"@matterlabs/hardhat-zksync-deploy": "1.1.1",
"@matterlabs/hardhat-zksync-solc": "1.0.5",
"@matterlabs/hardhat-zksync-verify": "1.2.1",
"@nomicfoundation/hardhat-chai-matchers": "2.0.2",
"@matterlabs/hardhat-zksync-deploy": "1.1.2",
"@matterlabs/hardhat-zksync-solc": "1.1.2",
"@matterlabs/hardhat-zksync-verify": "1.3.0",
"@nomicfoundation/hardhat-chai-matchers": "2.0.3",
"@nomicfoundation/hardhat-ethers": "3.0.5",
"@nomicfoundation/hardhat-network-helpers": "1.0.10",
"@nomicfoundation/hardhat-verify": "2.0.2",
"chai": "4.4.0",
"chai-bn": "0.3.1",
"dotenv": "16.3.1",
"dotenv": "16.4.1",
"dotenv-cli": "7.3.0",
"eslint": "8.56.0",
"eslint-config-standard": "17.1.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-n": "16.4.0",
"eslint-plugin-n": "16.6.2",
"eslint-plugin-promise": "6.1.1",
"ethers": "6.9.0",
"hardhat": "2.19.2",
"ethers": "6.10.0",
"hardhat": "2.19.4",
"hardhat-dependency-compiler": "1.1.3",
"hardhat-deploy": "0.11.45",
"hardhat-gas-reporter": "1.0.9",
"hardhat-tracer": "2.7.0",
"rimraf": "5.0.5",
"solhint": "3.6.2",
"solidity-coverage": "0.8.5",
"zksync-ethers": "6.0.0",
"solidity-coverage": "0.8.6",
"zksync-ethers": "6.1.0",
"zksync-web3": "0.17.1"
},
"scripts": {
Expand Down
4 changes: 2 additions & 2 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The script compares the prices of a list of tokens in two different instances of

The script's behavior is controlled by environment variables, which are specified as follows:
- `SCRIPT_ETH_PRICE` (required) - The current price of ETH in USD.
- `SCRIPT_TOKENLIST` (required) - Path to a file with list of tokens to be checked. The list can either be an array of token addresses or an object with token addresses as keys.
- `SCRIPT_TOKENLIST` (required) - A stringified json array of tokens or a path to a file with list of tokens to be checked. The list in the file can either be an array of token addresses or an object with token addresses as keys.
- Example: result of `https://token-prices.1inch.io/v1.1/1`
- `SCRIPT_NETWORK_NAME` (optional) - Specifies the network from which OffchainOracle's address is obtained during deployment. If not specified, the script defaults to the mainnet.
- `SCRIPT_SKIP_ORACLES` (optional) - Comma-separated list of oracle addresses that should not be added from a deployed oracle.
Expand All @@ -41,4 +41,4 @@ The script's behavior is controlled by environment variables, which are specifie
SCRIPT_ADD_ORACLES=UniswapV3Oracle:0:|KyberDmmOracle:0:["0x833e4083b7ae46cea85695c4f7ed25cdad8886de"]|UniswapV2LikeOracle:0:["0x115934131916c8b277dd010ee02de363c09d037c","0x65d1a3b1e46c6e4f1be1ad5f99ef14dc488ae0549dc97db9b30afe2241ce1c7a"]
```

Note that the `SCRIPT_TOKENLIST` must be a valid path to a file containing the list of tokens to be checked. The script uses the require function to import the file, so it must be either a JavaScript or JSON file.
Note that when `SCRIPT_TOKENLIST` is used to specify a file path, it must lead to a valid file containing the list of tokens to be checked. The script uses the require function to import the file, so it must be either a JavaScript or JSON file.
2 changes: 1 addition & 1 deletion scripts/check-token.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { ethers, deployments } = require('hardhat');
const { tokens } = require('../test/helpers');

const usdPrice = (ethPrice, srcTokenDecimals) => { return parseFloat(ethPrice * 10 ** srcTokenDecimals / 1e18 / 1e18 * parseFloat(process.env.SCRIPT_ETH_PRICE)).toFixed(2); };
const usdPrice = (ethPrice, srcTokenDecimals) => { return parseFloat(parseFloat(ethPrice) * 10 ** srcTokenDecimals / 1e18 / 1e18 * parseFloat(process.env.SCRIPT_ETH_PRICE)).toFixed(2); };

async function main () {
if (!process.env.SCRIPT_ETH_PRICE) {
Expand Down
8 changes: 6 additions & 2 deletions scripts/check-tokens-prices.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ async function main () {
if (!process.env.SCRIPT_ETH_PRICE) {
throw new Error('Specify SCRIPT_ETH_PRICE');
}
const tokenlistPath = process.env.SCRIPT_TOKENLIST;
let tokenlist = process.env.SCRIPT_TOKENLIST;
const networkName = process.env.SCRIPT_NETWORK_NAME || 'mainnet';
const skipOracles = (process.env.SCRIPT_SKIP_ORACLES || '').split(',');
const addOracles = (process.env.SCRIPT_ADD_ORACLES || '').split('|');
const thresholdFilter = 10;

let tokenlist = require(tokenlistPath);
try {
tokenlist = JSON.parse(tokenlist);
} catch {
tokenlist = require(tokenlist);
}
if (!Array.isArray(tokenlist)) {
tokenlist = Object.keys(tokenlist);
}
Expand Down
33 changes: 33 additions & 0 deletions test/helpers.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
const { network } = require('hardhat');
const { Networks } = require('@1inch/solidity-utils/hardhat-setup');

const defaultValues = {
thresholdFilter: 10,
};
Expand Down Expand Up @@ -55,6 +58,11 @@ const tokens = {
axlUSDC: '0xEB466342C4d449BC9f53A865D5Cb90586f405215',
axlUSDT: '0x7f5373AE26c3E8FfC4c77b7255DF7eC1A9aF52a6',
},
optimistic: {
WETH: '0x4200000000000000000000000000000000000006',
USDC: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607',
OP: '0x4200000000000000000000000000000000000042',
},
};

const contracts = {
Expand Down Expand Up @@ -137,11 +145,36 @@ const deployParams = {
factory: '0xe21aac7f113bd5dc2389e4d8a8db854a87fd6951',
initcodeHash: '0x0ccd005ee58d5fb11632ef5c2e0866256b240965c62c8e990c0f84a97f311879',
},
VelodromeV2: { // optimistic network
router: '0xa062aE8A9c5e11aaA026fc2670B0D65cCc8B2858',
registry: '0xF4c67CdEAaB8360370F41514d06e32CcD8aA1d7B',
},
};

const resetHardhatNetworkFork = async function (networkName) {
if (networkName.toLowerCase() === 'hardhat') {
await network.provider.request({ // reset to local network
method: 'hardhat_reset',
params: [],
});
} else {
const { url, authKeyHttpHeader } = (new Networks())._parseRpcEnv(process.env[`${networkName.toUpperCase()}_RPC_URL`]);
await network.provider.request({ // reset to networkName fork
method: 'hardhat_reset',
params: [{
forking: {
jsonRpcUrl: url,
httpHeaders: authKeyHttpHeader ? { 'auth-key': authKeyHttpHeader } : undefined,
},
}],
});
}
};

module.exports = {
defaultValues,
tokens,
contracts,
deployParams,
resetHardhatNetworkFork,
};
88 changes: 88 additions & 0 deletions test/oracles/VelodromeV2Oracle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { assertRoughlyEqualValues, deployContract } = require('@1inch/solidity-utils');
const {
tokens,
deployParams: { VelodromeV2, UniswapV3 },
defaultValues: { thresholdFilter },
resetHardhatNetworkFork,
} = require('../helpers.js');

describe('VelodromeV2Oracle', function () {
before(async function () {
await resetHardhatNetworkFork('optimistic');
});

after(async function () {
await resetHardhatNetworkFork('mainnet');
});

async function initContracts () {
const velodromeV2Oracle = await deployContract('VelodromeV2Oracle', [VelodromeV2.router, VelodromeV2.registry]);
const uniswapV3Oracle = await deployContract('UniswapV3LikeOracle', [UniswapV3.factory, UniswapV3.initcodeHash, UniswapV3.fees]);
return { velodromeV2Oracle, uniswapV3Oracle };
}

it('WETH -> USDC', async function () {
const { velodromeV2Oracle, uniswapV3Oracle } = await loadFixture(initContracts);
await testRate(tokens.optimistic.WETH, tokens.optimistic.USDC, tokens.NONE, velodromeV2Oracle, uniswapV3Oracle);
});

it('USDC -> WETH', async function () {
const { velodromeV2Oracle, uniswapV3Oracle } = await loadFixture(initContracts);
await testRate(tokens.optimistic.USDC, tokens.optimistic.WETH, tokens.NONE, velodromeV2Oracle, uniswapV3Oracle);
});

it('WETH -> OP -> USDC', async function () {
const { velodromeV2Oracle, uniswapV3Oracle } = await loadFixture(initContracts);
await testRate(tokens.optimistic.WETH, tokens.optimistic.USDC, tokens.optimistic.OP, velodromeV2Oracle, uniswapV3Oracle);
});

const testRate = async function (srcToken, dstToken, connector, velodromeV2Oracle, uniswapV3Oracle) {
const velodromeV2Result = await velodromeV2Oracle.getRate(srcToken, dstToken, connector, thresholdFilter);
const v3Result = await uniswapV3Oracle.getRate(srcToken, dstToken, connector, thresholdFilter);
assertRoughlyEqualValues(v3Result.rate, velodromeV2Result.rate, 0.05);
};

describe('Measure gas', function () {
it('WETH -> USDC', async function () {
const { velodromeV2Oracle, uniswapV3Oracle } = await loadFixture(initContracts);
await measureGas(
await velodromeV2Oracle.getFunction('getRate').send(tokens.optimistic.WETH, tokens.optimistic.USDC, tokens.NONE, thresholdFilter),
'VelodromeV2Oracle WETH -> USDC',
);
await measureGas(
await uniswapV3Oracle.getFunction('getRate').send(tokens.optimistic.WETH, tokens.optimistic.USDC, tokens.NONE, thresholdFilter),
'UniswapV3Oracle WETH -> USDC',
);
});

it('USDC -> WETH', async function () {
const { velodromeV2Oracle, uniswapV3Oracle } = await loadFixture(initContracts);
await measureGas(
await velodromeV2Oracle.getFunction('getRate').send(tokens.optimistic.USDC, tokens.optimistic.WETH, tokens.NONE, thresholdFilter),
'VelodromeV2Oracle USDC -> WETH',
);
await measureGas(
await uniswapV3Oracle.getFunction('getRate').send(tokens.optimistic.USDC, tokens.optimistic.WETH, tokens.NONE, thresholdFilter),
'UniswapV3Oracle USDC -> WETH',
);
});

it('WETH -> OP -> USDC', async function () {
const { velodromeV2Oracle, uniswapV3Oracle } = await loadFixture(initContracts);
await measureGas(
await velodromeV2Oracle.getFunction('getRate').send(tokens.optimistic.WETH, tokens.optimistic.USDC, tokens.optimistic.OP, thresholdFilter),
'VelodromeV2Oracle WETH -> OP -> USDC',
);
await measureGas(
await uniswapV3Oracle.getFunction('getRate').send(tokens.optimistic.WETH, tokens.optimistic.USDC, tokens.optimistic.OP, thresholdFilter),
'UniswapV3Oracle WETH -> OP -> USDC',
);
});

async function measureGas (tx, comment) {
const receipt = await tx.wait();
console.log('gasUsed', comment, receipt.gasUsed.toString());
}
});
});
Loading
Loading