diff --git a/contracts/external/IQuoter.sol b/contracts/external/IQuoter.sol new file mode 100644 index 00000000..4da3ba17 --- /dev/null +++ b/contracts/external/IQuoter.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.15; +pragma abicoder v2; + +/// @title QuoterV2 Interface +/// @notice Supports quoting the calculated amounts from exact input or exact output swaps. +/// @notice For each pool also tells you the number of initialized ticks crossed and the sqrt price of the pool after the swap. +/// @dev These functions are not marked view because they rely on calling non-view functions and reverting +/// to compute the result. They are also not gas efficient and should not be called on-chain. +interface IQuoter { + /// @notice Returns the amount out received for a given exact input swap without executing the swap + /// @param path The path of the swap, i.e. each token pair and the pool fee + /// @param amountIn The amount of the first token to swap + /// @return amountOut The amount of the last token that would be received + /// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path + /// @return initializedTicksCrossedList List of number of initialized ticks loaded + function quoteExactInput( + bytes memory path, + uint256 amountIn + ) + external + view + returns ( + uint256 amountOut, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksCrossedList, + uint256 gasEstimate + ); + + struct QuoteExactInputSingleWithPoolParams { + address tokenIn; + address tokenOut; + uint256 amountIn; + address pool; + uint24 fee; + uint160 sqrtPriceLimitX96; + } + + /// @notice Returns the amount out received for a given exact input but for a swap of a single pool + /// @param params The params for the quote, encoded as `quoteExactInputSingleWithPool` + /// tokenIn The token being swapped in + /// amountIn The desired input amount + /// tokenOut The token being swapped out + /// fee The fee of the pool to consider for the pair + /// pool The address of the pool to consider for the pair + /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap + /// @return amountOut The amount of `tokenOut` that would be received + /// @return sqrtPriceX96After The sqrt price of the pool after the swap + /// @return initializedTicksCrossed The number of initialized ticks loaded + function quoteExactInputSingleWithPool( + QuoteExactInputSingleWithPoolParams memory params + ) + external + view + returns ( + uint256 amountOut, + uint160 sqrtPriceX96After, + uint32 initializedTicksCrossed, + uint256 gasEstimate + ); + + struct QuoteExactInputSingleParams { + address tokenIn; + address tokenOut; + uint256 amountIn; + uint24 fee; + uint160 sqrtPriceLimitX96; + } + + /// @notice Returns the amount out received for a given exact input but for a swap of a single pool + /// @param params The params for the quote, encoded as `QuoteExactInputSingleParams` + /// tokenIn The token being swapped in + /// amountIn The desired input amount + /// tokenOut The token being swapped out + /// fee The fee of the token pool to consider for the pair + /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap + /// @return amountOut The amount of `tokenOut` that would be received + /// @return sqrtPriceX96After The sqrt price of the pool after the swap + /// @return initializedTicksCrossed The number of initialized ticks loaded + function quoteExactInputSingle( + QuoteExactInputSingleParams memory params + ) + external + view + returns ( + uint256 amountOut, + uint160 sqrtPriceX96After, + uint32 initializedTicksCrossed, + uint256 gasEstimate + ); + + struct QuoteExactOutputSingleWithPoolParams { + address tokenIn; + address tokenOut; + uint256 amount; + uint24 fee; + address pool; + uint160 sqrtPriceLimitX96; + } + + /// @notice Returns the amount in required to receive the given exact output amount but for a swap of a single pool + /// @param params The params for the quote, encoded as `QuoteExactOutputSingleWithPoolParams` + /// tokenIn The token being swapped in + /// tokenOut The token being swapped out + /// amount The desired output amount + /// fee The fee of the token pool to consider for the pair + /// pool The address of the pool to consider for the pair + /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap + /// @return amountIn The amount required as the input for the swap in order to receive `amountOut` + /// @return sqrtPriceX96After The sqrt price of the pool after the swap + /// @return initializedTicksCrossed The number of initialized ticks loaded + function quoteExactOutputSingleWithPool( + QuoteExactOutputSingleWithPoolParams memory params + ) + external + view + returns ( + uint256 amountIn, + uint160 sqrtPriceX96After, + uint32 initializedTicksCrossed, + uint256 gasEstimate + ); + + struct QuoteExactOutputSingleParams { + address tokenIn; + address tokenOut; + uint256 amount; + uint24 fee; + uint160 sqrtPriceLimitX96; + } + + /// @notice Returns the amount in required to receive the given exact output amount but for a swap of a single pool + /// @param params The params for the quote, encoded as `QuoteExactOutputSingleParams` + /// tokenIn The token being swapped in + /// tokenOut The token being swapped out + /// amountOut The desired output amount + /// fee The fee of the token pool to consider for the pair + /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap + /// @return amountIn The amount required as the input for the swap in order to receive `amountOut` + /// @return sqrtPriceX96After The sqrt price of the pool after the swap + /// @return initializedTicksCrossed The number of initialized ticks loaded + function quoteExactOutputSingle( + QuoteExactOutputSingleParams memory params + ) + external + view + returns ( + uint256 amountIn, + uint160 sqrtPriceX96After, + uint32 initializedTicksCrossed, + uint256 gasEstimate + ); + + /// @notice Returns the amount in required for a given exact output swap without executing the swap + /// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order + /// @param amountOut The amount of the last token to receive + /// @return amountIn The amount of first token required to be paid + /// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path + /// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path + function quoteExactOutput( + bytes memory path, + uint256 amountOut + ) + external + view + returns ( + uint256 amountIn, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksCrossedList, + uint256 gasEstimate + ); +} diff --git a/contracts/l2/UniswapPriceOracle.sol b/contracts/l2/UniswapPriceOracle.sol new file mode 100644 index 00000000..40ad0736 --- /dev/null +++ b/contracts/l2/UniswapPriceOracle.sol @@ -0,0 +1,94 @@ +// Copyright (C) 2020-2024 SubQuery Pte Ltd authors & contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity 0.8.15; + +import '@openzeppelin/contracts/access/Ownable.sol'; +import '../external/IQuoter.sol'; +import '../interfaces/IPriceOracle.sol'; + +interface IUniswapV3Pool { + function slot0() + external + view + returns ( + uint160 sqrtPriceX96, + int24 tick, + uint16 observationIndex, + uint16 observationCardinality, + uint16 observationCardinalityNext, + uint8 feeProtocol, + bool unlocked + ); + + function token0() external view returns (address); + function token1() external view returns (address); +} + +contract UniswapPriceOracle is Ownable, IPriceOracle { + IQuoter public quoter; // 0x222ca98f00ed15b1fae10b61c277703a194cf5d2, https://github.com/Uniswap/view-quoter-v3/tree/master + mapping(bytes32 => uint24) public poolFees; + + constructor(address _quoterAddress) Ownable() { + quoter = IQuoter(_quoterAddress); + } + + function setQuoter(address _quoterAddress) external onlyOwner { + quoter = IQuoter(_quoterAddress); + } + + function setPoolFee(address fromToken, address toToken, uint24 _poolFee) external onlyOwner { + bytes32 poolkey = _concatAddresses(fromToken, toToken); + poolFees[poolkey] = _poolFee; + } + + function getPoolFee(address fromToken, address toToken) public view returns (uint24) { + bytes32 poolkey = _concatAddresses(fromToken, toToken); + return poolFees[poolkey] == 0 ? 3000 : poolFees[poolkey]; + } + + function getAssetPrice( + address fromToken, + address toToken + ) external view override returns (uint256) { + return _convertPrice(fromToken, toToken, 1); + } + + function convertPrice( + address fromToken, + address toToken, + uint256 amount + ) external view override returns (uint256) { + require(amount > 0, 'Amount must be greater than 0'); + return _convertPrice(fromToken, toToken, amount); + } + + // usdc: 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 + // sqt: 0x858c50c3af1913b0e849afdb74617388a1a5340d + + function _convertPrice( + address fromToken, + address toToken, + uint256 amount + ) internal view returns (uint256) { + // Simulate the swap using the QuoterV2 contract + IQuoter.QuoteExactInputSingleParams memory params = IQuoter.QuoteExactInputSingleParams({ + tokenIn: fromToken, + tokenOut: toToken, + amountIn: amount, + fee: getPoolFee(fromToken, toToken), + sqrtPriceLimitX96: 0 // No price limit + }); + + (uint256 amountOut, , , ) = quoter.quoteExactInputSingle(params); + + return amountOut; + } + + function _concatAddresses(address addr1, address addr2) internal pure returns (bytes32) { + if (addr1 > addr2) { + (addr1, addr2) = (addr2, addr1); + } + return (bytes32(bytes20(addr1)) << 96) | bytes32(bytes20(addr2)); + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 6c7f9c9c..9dc4d2be 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -398,6 +398,11 @@ task('publishChild', 'verify and publish contracts on etherscan') address: deployment.L2Vesting.innerAddress, constructorArguments: [], }); + //UniswapPriceOracle + await hre.run('verify:verify', { + address: deployment.UniswapPriceOracle.address, + constructorArguments: contractsConfig[taskArgs.networkpair].UniswapPriceOracle, + }); } catch (err) { console.log(err); } diff --git a/publish/mainnet.json b/publish/mainnet.json index aa1c6032..2382e09c 100644 --- a/publish/mainnet.json +++ b/publish/mainnet.json @@ -217,6 +217,12 @@ "address": "0x915FbbfF55775B6243A3edB42BE08554C44DE45a", "bytecodeHash": "5994844f015a7b599b6e5a10a50cec76be5e60fa20439f0fdd2cfd49df3a15ed", "lastUpdate": "Tue, 16 Apr 2024 05:16:47 GMT" + }, + "UniswapPriceOracle": { + "innerAddress": "", + "address": "0xDACAfD570e55eD179f2A37c6A1B20fD4FceC62AE", + "bytecodeHash": "4f491154f798bcad9bb40e8a538e43bab1e6a2cce5468504d776a44d3926e721", + "lastUpdate": "Wed, 19 Mar 2025 01:21:59 GMT" } } } \ No newline at end of file diff --git a/scripts/config/contracts.config.ts b/scripts/config/contracts.config.ts index da343597..a20488e6 100644 --- a/scripts/config/contracts.config.ts +++ b/scripts/config/contracts.config.ts @@ -22,6 +22,7 @@ export default { RewardsBooster: [utils.parseEther('6.3419584'), utils.parseEther('1000')], // _issuancePerBlock, _minimumDeploymentBooster L2SQToken: ['0x4200000000000000000000000000000000000010', '0x09395a2A58DB45db0da254c7EAa5AC469D8bDc85'], // l2bridge, l1token PriceOracle: [10, 3600], + UniswapPriceOracle: ['0x222ca98f00ed15b1fae10b61c277703a194cf5d2'], }, kepler: { InflationController: [0, '0x34c35136ECe9CBD6DfDf2F896C6e29be01587c0C'], // inflationRate, inflationDestination @@ -64,6 +65,7 @@ export default { // base: 2s a block, 31536000/2 = 15768000 blocks a year, 1% rewards = 100000000 / 15768000 = about 6.3419584 SQT per block RewardsBooster: [utils.parseEther('6.34'), utils.parseEther('10000')], // _issuancePerBlock, _minimumDeploymentBooster L2SQToken: ['0x4200000000000000000000000000000000000010', '0xE6E15Ffc71AbDAe8D34D65bB695959fbd6c15435'], // l2bridge, l1token + UniswapPriceOracle: ['0x222ca98f00ed15b1fae10b61c277703a194cf5d2'], }, local: { InflationController: [1000, '0x4ae8fcdddc859e2984ce0b8f4ef490d61a7a9b7f'], // inflationRate, inflationDestination diff --git a/scripts/deployContracts.ts b/scripts/deployContracts.ts index d4dd5339..0f8921ba 100644 --- a/scripts/deployContracts.ts +++ b/scripts/deployContracts.ts @@ -57,6 +57,7 @@ import { L2SQToken, AirdropperLite, L2Vesting, + UniswapPriceOracle, } from '../src'; import { Config, ContractConfig, Contracts, UPGRADEBAL_CONTRACTS } from './contracts'; import { l1StandardBridge } from './L1StandardBridge'; @@ -522,7 +523,11 @@ export async function deployContracts( proxyAdmin, initConfig: [settingsAddress], }); - logger?.info('🤞 L2Vesting'); + + //deploy uniswapPriceOracle contract + const uniswapPriceOracle = await deployContract('UniswapPriceOracle', 'child', { + deployConfig: [...config['UniswapPriceOracle']], + }); // Register addresses on settings contract logger?.info('🤞 Set settings addresses'); diff --git a/src/contracts.ts b/src/contracts.ts index d018dbec..e685b79b 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -36,6 +36,7 @@ import SQTRedeem from './artifacts/contracts/SQTRedeem.sol/SQTRedeem.json'; import L2SQToken from './artifacts/contracts/l2/L2SQToken.sol/L2SQToken.json'; import AirdropperLite from './artifacts/contracts/root/AirdropperLite.sol/AirdropperLite.json'; import L2Vesting from './artifacts/contracts/l2/L2Vesting.sol/L2Vesting.json'; +import UniswapPriceOracle from './artifacts/contracts/l2/UniswapPriceOracle.sol/UniswapPriceOracle.json'; export default { Settings, @@ -73,4 +74,5 @@ export default { L2SQToken, AirdropperLite, L2Vesting, + UniswapPriceOracle, }; diff --git a/src/types.ts b/src/types.ts index c1ec80e8..92259c01 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,6 +43,7 @@ import { L2SQToken__factory, AirdropperLite__factory, L2Vesting__factory, + UniswapPriceOracle__factory, } from './typechain'; export type SubqueryNetwork = 'testnet' | 'testnet-mumbai' | 'mainnet' | 'local'; @@ -137,6 +138,7 @@ export const CONTRACT_FACTORY: Record = { L2SQToken: L2SQToken__factory, AirdropperLite: AirdropperLite__factory, L2Vesting: L2Vesting__factory, + UniswapPriceOracle: UniswapPriceOracle__factory, }; export enum SQContracts { diff --git a/test/setup.ts b/test/setup.ts index 4b0ee463..6b3559e8 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -42,6 +42,7 @@ export const deployContracts = async (wallet: Wallet, wallet1: Wallet, treasury ConsumerRegistry: [], PriceOracle: [], RewardsBooster: [utils.parseEther('10').toString(), utils.parseEther('10000').toString()], // _issuancePerBlock, _minimumDeploymentBooster + UniswapPriceOracle: ['0x222ca98f00ed15b1fae10b61c277703a194cf5d2'], }); await contracts.settings.setContractAddress(SQContracts.Treasury, treasury.address);